草庐IT

我的设计模式之旅、07 观察者模式

小能的博客 CanAngle's Blog 2023-03-28 原文

编程旅途是漫长遥远的,在不同时刻有不同的感悟,本文会一直更新下去。

程序介绍

本程序实现观察者模式。使用C#、Go两门语言分别进行实现。程序创建一个全局游戏死亡事件通知,5个玩家、1个Boss,当任意一方死亡时,在场存活者都能收到阵亡者的消息。

观察者模式
----------游戏回合开始----------
最终BOSS 击杀 二号玩家 !
一号玩家 知道 二号玩家 阵亡了!
三号玩家 知道 二号玩家 阵亡了!
四号玩家 知道 二号玩家 阵亡了!
五号玩家 知道 二号玩家 阵亡了!
最终BOSS 知道 二号玩家 阵亡了!
----------过了一段时间----------
最终BOSS 击杀 四号玩家 !
一号玩家 知道 四号玩家 阵亡了!
三号玩家 知道 四号玩家 阵亡了!
五号玩家 知道 四号玩家 阵亡了!
最终BOSS 知道 四号玩家 阵亡了!
----------过了一段时间----------
一号玩家 击杀 最终BOSS!
一号玩家 知道 最终BOSS 阵亡了!
三号玩家 知道 最终BOSS 阵亡了!
五号玩家 知道 最终BOSS 阵亡了!

C# 程序代码

observerOriginal.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace observer_original
{
    public abstract class Subject
    {
        private List<Observer> observers = new();

        public void Attach(Observer o)
        {
            observers.Add(o);
        }

        public void Detach(Observer o)
        {
            observers.Remove(o);
        }

        public void Notify()
        {
            foreach (Observer o in observers)
            {
                o.Update();
            }
        }
    }

    public class DeadSubject : Subject
    {
        public ICharacter? DeadEntity { get; set; }
    }

    public abstract class Observer
    {
        public abstract void Update();
    }

    public interface ICharacter
    {
        public string Name { get; }
        void Dead();
        void Kill(ICharacter who);
    }

    public class Player : Observer, ICharacter
    {
        private readonly DeadSubject? sub;
        public string Name { get; }

        public Player(string name)
        {
            sub = null;
            Name = name;
        }

        public Player(string name, DeadSubject subject)
        {
            sub = subject;
            Name = name;
        }

        public override void Update()
        {
            if (sub == null) return;
            Console.WriteLine($"{Name} 知道 {sub?.DeadEntity?.Name} 阵亡了!");
        }

        public void Dead()
        {
            if (sub == null) return;
            sub.DeadEntity = this;
            sub.Detach(this);
            sub.Notify();
        }

        public void Kill(ICharacter who)
        {
            Console.WriteLine($"{Name} 击杀 {who.Name}!");
            who.Dead();
        }
    }


    public class Boss : Observer, ICharacter
    {
        public string Name { get; }
        private DeadSubject? sub;

        public Boss(string name)
        {
            sub = null;
            Name = name;
        }

        public Boss(string name, DeadSubject subject)
        {
            sub = subject;
            Name = name;
        }

        public override void Update()
        {
            if (sub == null) return;
            Console.WriteLine($"{Name} 知道 {sub?.DeadEntity?.Name} 阵亡了!");
        }

        public void Dead()
        {
            if (sub == null) return;
            sub.DeadEntity = this;
            sub.Detach(this);
            sub.Notify();
        }

        public void Kill(ICharacter who)
        {
            Console.WriteLine($"{Name} 击杀 {who.Name} !");
            who.Dead();
        }
    }

    static class ObserverOriginal
    {
        public static void Start()
        {
            Console.WriteLine("观察者模式");
            DeadSubject sub = new DeadSubject();
            Boss boss = new Boss("最终BOSS", sub);
            Player p1 = new Player("一号玩家", sub);
            Player p2 = new Player("二号玩家", sub);
            Player p3 = new Player("三号玩家", sub);
            Player p4 = new Player("四号玩家", sub);
            Player p5 = new Player("五号玩家", sub);
            sub.Attach(boss);
            sub.Attach(p1);
            sub.Attach(p2);
            sub.Attach(p3);
            sub.Attach(p4);
            sub.Attach(p5);
            Console.WriteLine("----------游戏回合开始----------");
            boss.Kill(p2);
            Console.WriteLine("----------过了一段时间----------");
            boss.Kill(p4);
            Console.WriteLine("----------过了一段时间----------");
            p1.Kill(boss);
        }
    }
}

observerDelegate.cs

为什么使用事件委托

当观察者对象没有实现观察者接口的方法,而是各持一词,比如窗体的各个空间,方法已经写死无法添加,按原有设计通知者无法进行做到通知。这时候可以使用C#提供的事件委托功能,声明一个函数抽象,将各个观察者的同型函数进行类化,通过事件委托机制,通知各个函数的运行。原先的Obsever接口可以去除,Subject抽象类也不再需要AttachDetach方法,可以转变成接口,让具体通知者类去实现通知方法,具体通知类声明一个事件委托变量。

程序代码

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace observer_delegate
{
  public delegate void DeadEventHandler();
  public interface Subject
  {
    void Notify();
  }

  public class DeadSubject : Subject
  {
    public event DeadEventHandler? DeadEvent;
    public ICharacter? DeadEntity { get; set; }
    public void Notify()
    {
      DeadEvent?.Invoke();
    }
  }

  public interface ICharacter
  {
    public string Name { get; }
    void Dead();
    void Kill(ICharacter who);
  }

  public class Player : ICharacter
  {
    private readonly DeadSubject? sub;
    public string Name { get; }

    public Player(string name)
    {
      sub = null;
      Name = name;
    }

    public Player(string name, DeadSubject subject)
    {
      sub = subject;
      Name = name;
    }

    // 处理通知
    public void PlayerUpdate()
    {
      if (sub == null) return;
      Console.WriteLine($"{Name} 知道 {sub?.DeadEntity?.Name} 阵亡了!");
    }

    public void Dead()
    {
      if (sub == null) return;
      sub.DeadEntity = this;
      sub.DeadEvent -= PlayerUpdate;
      sub.Notify();
    }

    public void Kill(ICharacter who)
    {
      Console.WriteLine($"{Name} 击杀 {who.Name}!");
      who.Dead();
    }
  }


  public class Boss : ICharacter
  {
    public string Name { get; }
    private DeadSubject? sub;

    public Boss(string name)
    {
      sub = null;
      Name = name;
    }

    public Boss(string name, DeadSubject subject)
    {
      sub = subject;
      Name = name;
    }

    public void BossUpdate()
    {
      if (sub == null) return;
      Console.WriteLine($"{Name} 知道 {sub?.DeadEntity?.Name} 阵亡了!");
    }

    public void Dead()
    {
      if (sub == null) return;
      sub.DeadEntity = this;
      sub.DeadEvent -= BossUpdate;
      sub.Notify();
    }

    public void Kill(ICharacter who)
    {
      Console.WriteLine($"{Name} 击杀 {who.Name} !");
      who.Dead();
    }
  }

  static class ObserverDelegate
  {
    public static void Start()
    {
      Console.WriteLine("观察者模式");
      DeadSubject sub = new DeadSubject();
      Boss boss = new Boss("最终BOSS", sub);
      Player p1 = new Player("一号玩家", sub);
      Player p2 = new Player("二号玩家", sub);
      Player p3 = new Player("三号玩家", sub);
      Player p4 = new Player("四号玩家", sub);
      Player p5 = new Player("五号玩家", sub);
      sub.DeadEvent += p1.PlayerUpdate;
      sub.DeadEvent += p2.PlayerUpdate;
      sub.DeadEvent += p3.PlayerUpdate;
      sub.DeadEvent += p4.PlayerUpdate;
      sub.DeadEvent += p5.PlayerUpdate;
      sub.DeadEvent += boss.BossUpdate;
      Console.WriteLine("----------游戏回合开始----------");
      boss.Kill(p2);
      Console.WriteLine("----------过了一段时间----------");
      boss.Kill(p4);
      Console.WriteLine("----------过了一段时间----------");
      p1.Kill(boss);
    }
  }
}

Program.cs

Programusing System;
using observer_original;
using observer_delegate;

namespace observer
{
  class Program
  {
    public static void Main(string[] args)
    {
      // ObserverOriginal.Start();
      ObserverDelegate.Start();
    }
  }
}

Console

观察者模式
----------游戏回合开始----------
最终BOSS 击杀 二号玩家 !
一号玩家 知道 二号玩家 阵亡了!
三号玩家 知道 二号玩家 阵亡了!
四号玩家 知道 二号玩家 阵亡了!
五号玩家 知道 二号玩家 阵亡了!
最终BOSS 知道 二号玩家 阵亡了!
----------过了一段时间----------
最终BOSS 击杀 四号玩家 !
一号玩家 知道 四号玩家 阵亡了!
三号玩家 知道 四号玩家 阵亡了!
五号玩家 知道 四号玩家 阵亡了!
最终BOSS 知道 四号玩家 阵亡了!
----------过了一段时间----------
一号玩家 击杀 最终BOSS!
一号玩家 知道 最终BOSS 阵亡了!
三号玩家 知道 最终BOSS 阵亡了!
五号玩家 知道 最终BOSS 阵亡了!

Go 程序代码

observer.go

package main

import "fmt"

type IObserver interface {
	Update()
}

type ISubject interface {
	Attach(o IObserver)
	Detach(o IObserver)
	Notify()
}

type Subject struct {
	observers []IObserver
}

func (sub *Subject) Attach(o IObserver) {
	sub.observers = append(sub.observers, o)
}

func (sub *Subject) Detach(o IObserver) {
	obs := make([]IObserver, 0, len(sub.observers)-1)
	for _, v := range sub.observers {
		if v != o {
			obs = append(obs, v)
		}
	}
	sub.observers = obs
}

func (sub Subject) Notify() {
	for _, v := range sub.observers {
		v.Update()
	}
}

type ICharacter interface {
	Name() string
	Kill(who ICharacter)
	Dead()
}

type DeadSubject struct {
	*Subject
	Character ICharacter
}

type Character struct {
	name        string
	deadSubject *DeadSubject
}

// ^ 抽象角色共有的方法,表示属性
func (c Character) Name() string {
	return c.name
}

type Player struct {
	Character
}

func (p Player) Update() {
	fmt.Printf("%s 知道 %s 阵亡了\n", p.name, p.deadSubject.Character.Name())
}

func (p Player) Kill(who ICharacter) {
	fmt.Printf("%s 杀死 %s \n", p.name, who.Name())
	who.Dead()
}

// ^ *Player 获取真实实例而不是复制实例,确保Detach工作正常
func (p *Player) Dead() {
	p.deadSubject.Character = p
	p.deadSubject.Detach(p)
	p.deadSubject.Notify()
}

type Boss struct {
	Character
}

func (p Boss) Update() {
	fmt.Printf("%s 知道 %s 阵亡了\n", p.name, p.deadSubject.Character.Name())
}

func (p Boss) Kill(who ICharacter) {
	fmt.Printf("%s 杀死 %s \n", p.name, who.Name())
	who.Dead()
}

func (p *Boss) Dead() {
	p.deadSubject.Character = p
	p.deadSubject.Detach(p)
	p.deadSubject.Notify()
}

main.go

package main

import "fmt"

func main() {
	sub := &DeadSubject{
		&Subject{make([]IObserver, 0)},
		&Player{},
	}
	p1 := &Player{Character{"一号玩家", sub}}
	p2 := &Player{Character{"二号玩家", sub}}
	p3 := &Player{Character{"三号玩家", sub}}
	p4 := &Player{Character{"四号玩家", sub}}
	p5 := &Player{Character{"五号玩家", sub}}
	boss := &Boss{Character{"最终Boss", sub}}
	sub.Attach(p1)
	sub.Attach(p2)
	sub.Attach(p3)
	sub.Attach(p4)
	sub.Attach(p5)
	sub.Attach(boss)
	boss.Kill(p1)
	fmt.Println("-------过了一会-------")
	boss.Kill(p4)
	fmt.Println("-------过了一会-------")
	p2.Kill(boss)
}

Console

最终Boss 杀死 一号玩家 
二号玩家 知道 一号玩家 阵亡了
三号玩家 知道 一号玩家 阵亡了
四号玩家 知道 一号玩家 阵亡了
五号玩家 知道 一号玩家 阵亡了
最终Boss 知道 一号玩家 阵亡了
-------过了一会-------
最终Boss 杀死 四号玩家 
二号玩家 知道 四号玩家 阵亡了
三号玩家 知道 四号玩家 阵亡了
五号玩家 知道 四号玩家 阵亡了
最终Boss 知道 四号玩家 阵亡了
-------过了一会-------
二号玩家 杀死 最终Boss 
二号玩家 知道 最终Boss 阵亡了
三号玩家 知道 最终Boss 阵亡了
五号玩家 知道 最终Boss 阵亡了

思考总结

事件委托

委托是一种引用方法的类型。一旦委托分配了方法,委托将与该方法具有完全相同的行为。委托方法的使用可以像其他任何方法一样,具有参数和返回值。委托可以看作是对函数的抽象,是函数的类,是对函数的封装。委托的实例将代表一个具体的函数。

事件是委托的一种特殊形式,当发生有意义的事情时,事件对象处理通知过程。

  public delegate void DeadEventHandler(); //声明了一个特殊的“类”

  public class DeadSubject : Subject
  {
    // 声明了一个事件委托变量叫DeadEvent
    public event DeadEventHandler? DeadEvent;
    ...
  }
  ...
  // 创建委托的实例并搭载给事件委托变量
  sub.DeadEvent += new DeadEventHandler(p1.PlayerUpdate)  // 等同 sub.DeadEvent += p1.PlayerUpdate;	

一个事件委托变量可以搭载多个方法,所有方法被依次唤起。委托对象所搭载的方法并不需要属于同一个类。

委托对象所搭载的所有方法必须具有相同的原形和形式,也就是拥有相同的参数列表和返回值类型。

什么是观察者模式

当对象间存在一对多关系时,则使用观察者模式(Observer Pattern)。比如,当一个对象被修改时,则会自动通知依赖它的对象。观察者模式属于行为型模式。

观察者模式:定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。

由于对象间相互的依赖关系,很容易违背依赖倒转原则开放-封闭原则。因此需要我们对通知方和观察者之间进行解耦。让双方依赖抽象,而不是依赖于具体。从而使得各自的变化都不会影响另一边的变化。

主要解决:一个对象状态改变给其他对象通知的问题,而且要考虑到易用和低耦合,保证高度的协作。

何时使用:一个对象(目标对象)的状态发生改变,所有的依赖对象(观察者对象)都将得到通知,进行广播通知。

如何解决:使用面向对象技术,可以将这种依赖关系弱化。

关键代码:C#中,Subject抽象类里有一个 ArrayList 存放观察者们。Go中,使用切片存放观察者门。

应用实例:

  • 拍卖的时候,拍卖师观察最高标价,然后通知给其他竞价者竞价。
  • 西游记里面悟空请求菩萨降服红孩儿,菩萨洒了一地水招来一个老乌龟,这个乌龟就是观察者,他观察菩萨洒水这个动作。

优点:

  • 观察者和被观察者是抽象耦合的。
  • 建立一套触发机制。如事件驱动的表示层。

缺点:

  • 如果一个被观察者对象有很多的直接和间接的观察者的话,将所有的观察者都通知到会花费很多时间。
  • 如果在观察者和观察目标之间有循环依赖的话,观察目标会触发它们之间进行循环调用,可能导致系统崩溃。
  • 观察者模式没有相应的机制让观察者知道所观察的目标对象是怎么发生变化的,而仅仅只是知道观察目标发生了变化。只知道结果,不知道过程。

使用场景:

  • 一个抽象模型有两个方面,其中一个方面依赖于另一个方面。将这些方面封装在独立的对象中使它们可以各自独立地改变和复用。
  • 一个对象的改变将导致其他一个或多个对象也发生改变,而不知道具体有多少对象将发生改变,可以降低对象之间的耦合度。
  • 一个对象必须通知其他对象,而并不知道这些对象是谁。
  • 需要在系统中创建一个触发链,A对象的行为将影响B对象,B对象的行为将影响C对象……,可以使用观察者模式创建一种链式触发机制。

注意事项:

  • 避免循环引用。
  • 如果顺序执行,某一观察者错误会导致系统卡壳,一般采用异步方式

参考资料

  • 《Go语言核心编程》李文塔
  • 《Go语言高级编程》柴树彬、曹春辉
  • 《大话设计模式》程杰
  • 单例模式 | 菜鸟教程

有关我的设计模式之旅、07 观察者模式的更多相关文章

  1. ruby-on-rails - Rails - 子类化模型的设计模式是什么? - 2

    我有一个模型:classItem项目有一个属性“商店”基于存储的值,我希望Item对象对特定方法具有不同的行为。Rails中是否有针对此的通用设计模式?如果方法中没有大的if-else语句,这是如何干净利落地完成的? 最佳答案 通常通过Single-TableInheritance. 关于ruby-on-rails-Rails-子类化模型的设计模式是什么?,我们在StackOverflow上找到一个类似的问题: https://stackoverflow.co

  2. ruby - 解析 RDFa、微数据等的最佳方式是什么,使用统一的模式/词汇(例如 schema.org)存储和显示信息 - 2

    我主要使用Ruby来执行此操作,但到目前为止我的攻击计划如下:使用gemsrdf、rdf-rdfa和rdf-microdata或mida来解析给定任何URI的数据。我认为最好映射到像schema.org这样的统一模式,例如使用这个yaml文件,它试图描述数据词汇表和opengraph到schema.org之间的转换:#SchemaXtoschema.orgconversion#data-vocabularyDV:name:namestreet-address:streetAddressregion:addressRegionlocality:addressLocalityphoto:i

  3. ruby - 如何在续集中重新加载表模式? - 2

    鉴于我有以下迁移:Sequel.migrationdoupdoalter_table:usersdoadd_column:is_admin,:default=>falseend#SequelrunsaDESCRIBEtablestatement,whenthemodelisloaded.#Atthispoint,itdoesnotknowthatusershaveais_adminflag.#Soitfails.@user=User.find(:email=>"admin@fancy-startup.example")@user.is_admin=true@user.save!ende

  4. ruby-on-rails - 使用 rails 4 设计而不更新用户 - 2

    我将应用程序升级到Rails4,一切正常。我可以登录并转到我的编辑页面。也更新了观点。使用标准View时,用户会更新。但是当我添加例如字段:name时,它​​不会在表单中更新。使用devise3.1.1和gem'protected_attributes'我需要在设备或数据库上运行某种更新命令吗?我也搜索过这个地方,找到了许多不同的解决方案,但没有一个会更新我的用户字段。我没有添加任何自定义字段。 最佳答案 如果您想允许额外的参数,您可以在ApplicationController中使用beforefilter,因为Rails4将参数

  5. ruby-on-rails - 如何在我的 Rails 应用程序 View 中打印 ruby​​ 变量的内容? - 2

    我是一个Rails初学者,但我想从我的RailsView(html.haml文件)中查看Ruby变量的内容。我试图在ruby​​中打印出变量(认为它会在终端中出现),但没有得到任何结果。有什么建议吗?我知道Rails调试器,但更喜欢使用inspect来打印我的变量。 最佳答案 您可以在View中使用puts方法将信息输出到服务器控制台。您应该能够在View中的任何位置使用Haml执行以下操作:-puts@my_variable.inspect 关于ruby-on-rails-如何在我的R

  6. ruby - 我可以将我的 README.textile 以正确的格式放入我的 RDoc 中吗? - 2

    我喜欢使用Textile或Markdown为我的项目编写自述文件,但是当我生成RDoc时,自述文件被解释为RDoc并且看起来非常糟糕。有没有办法让RDoc通过RedCloth或BlueCloth而不是它自己的格式化程序运行文件?它可以配置为自动检测文件后缀的格式吗?(例如README.textile通过RedCloth运行,但README.mdown通过BlueCloth运行) 最佳答案 使用YARD直接代替RDoc将允许您包含Textile或Markdown文件,只要它们的文件后缀是合理的。我经常使用类似于以下Rake任务的东西:

  7. jquery - 我的 jquery AJAX POST 请求无需发送 Authenticity Token (Rails) - 2

    rails中是否有任何规定允许站点的所有AJAXPOST请求在没有authenticity_token的情况下通过?我有一个调用Controller方法的JqueryPOSTajax调用,但我没有在其中放置任何真实性代码,但调用成功。我的ApplicationController确实有'request_forgery_protection'并且我已经改变了config.action_controller.consider_all_requests_local在我的environments/development.rb中为false我还搜索了我的代码以确保我没有重载ajaxSend来发送

  8. ruby - 是否有用于序列化和反序列化各种格式的对象层次结构的模式? - 2

    给定一个复杂的对象层次结构,幸运的是它不包含循环引用,我如何实现支持各种格式的序列化?我不是来讨论实际实现的。相反,我正在寻找可能会派上用场的设计模式提示。更准确地说:我正在使用Ruby,我想解析XML和JSON数据以构建复杂的对象层次结构。此外,应该可以将该层次结构序列化为JSON、XML和可能的HTML。我可以为此使用Builder模式吗?在任何提到的情况下,我都有某种结构化数据-无论是在内存中还是文本中-我想用它来构建其他东西。我认为将序列化逻辑与实际业务逻辑分开会很好,这样我以后就可以轻松支持多种XML格式。 最佳答案 我最

  9. java - 我的模型类或其他类中应该有逻辑吗 - 2

    我只想对我一直在思考的这个问题有其他意见,例如我有classuser_controller和classuserclassUserattr_accessor:name,:usernameendclassUserController//dosomethingaboutanythingaboutusersend问题是我的User类中是否应该有逻辑user=User.newuser.do_something(user1)oritshouldbeuser_controller=UserController.newuser_controller.do_something(user1,user2)我

  10. LC滤波器设计学习笔记(一)滤波电路入门 - 2

    目录前言滤波电路科普主要分类实际情况单位的概念常用评价参数函数型滤波器简单分析滤波电路构成低通滤波器RC低通滤波器RL低通滤波器高通滤波器RC高通滤波器RL高通滤波器部分摘自《LC滤波器设计与制作》,侵权删。前言最近需要学习放大电路和滤波电路,但是由于只在之前做音乐频谱分析仪的时候简单了解过一点点运放,所以也是相当从零开始学习了。滤波电路科普主要分类滤波器:主要是从不同频率的成分中提取出特定频率的信号。有源滤波器:由RC元件与运算放大器组成的滤波器。可滤除某一次或多次谐波,最普通易于采用的无源滤波器结构是将电感与电容串联,可对主要次谐波(3、5、7)构成低阻抗旁路。无源滤波器:无源滤波器,又称

随机推荐