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

2022-09-11 06:00:59

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

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

程式介紹

本程式實現觀察者模式。使用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語言高階程式設計》柴樹彬、曹春輝
  • 《大話設計模式》程傑
  • 單例模式 | 菜鳥教學