我的設計模式之旅 ⑦ 觀察者模式

一個菜鳥的設計模式之旅,文章可能會有不對的地方,懇請大佬指出錯誤。

編程旅途是漫長遙遠的,在不同時刻有不同的感悟,本文會一直更新下去。

程式介紹

本程式實現觀察者模式。使用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: