我的设计模式之旅 ⑦ 观察者模式

一个菜鸟的设计模式之旅,文章可能会有不对的地方,恳请大佬指出错误。

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

程序介绍

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

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

C# 程序代码

observerOriginal.cs

image-20220911023322689

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方法,可以转变成接口,让具体通知者类去实现通知方法,具体通知类声明一个事件委托变量。

程序代码

image-20220911025908731

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)。比如,当一个对象被修改时,则会自动通知依赖它的对象。观察者模式属于行为型模式。

image-20220911022822586

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

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

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

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

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

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

应用实例:

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

优点:

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

缺点:

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

使用场景:

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

注意事项:

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

参考资料

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