一個菜鳥的設計模式之旅,文章可能會有不對的地方,懇請大佬指出錯誤。
程式設計旅途是漫長遙遠的,在不同時刻有不同的感悟,本文會一直更新下去。
本程式實現觀察者模式。使用C#、Go兩門語言分別進行實現。程式建立一個全域性遊戲死亡事件通知,5個玩家、1個Boss,當任意一方死亡時,在場存活者都能收到陣亡者的訊息。
觀察者模式
----------遊戲回合開始----------
最終BOSS 擊殺 二號玩家 !
一號玩家 知道 二號玩家 陣亡了!
三號玩家 知道 二號玩家 陣亡了!
四號玩家 知道 二號玩家 陣亡了!
五號玩家 知道 二號玩家 陣亡了!
最終BOSS 知道 二號玩家 陣亡了!
----------過了一段時間----------
最終BOSS 擊殺 四號玩家 !
一號玩家 知道 四號玩家 陣亡了!
三號玩家 知道 四號玩家 陣亡了!
五號玩家 知道 四號玩家 陣亡了!
最終BOSS 知道 四號玩家 陣亡了!
----------過了一段時間----------
一號玩家 擊殺 最終BOSS!
一號玩家 知道 最終BOSS 陣亡了!
三號玩家 知道 最終BOSS 陣亡了!
五號玩家 知道 最終BOSS 陣亡了!
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);
}
}
}
當觀察者物件沒有實現觀察者介面的方法,而是各持一詞,比如表單的各個空間,方法已經寫死無法新增,按原有設計通知者無法進行做到通知。這時候可以使用C#提供的事件委託功能,宣告一個函數抽象,將各個觀察者的同型函數進行類化,通過事件委託機制,通知各個函數的執行。原先的Obsever
介面可以去除,Subject
抽象類也不再需要Attach
、Detach
方法,可以轉變成介面,讓具體通知者類去實現通知方法,具體通知類宣告一個事件委託變數。
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);
}
}
}
Programusing System;
using observer_original;
using observer_delegate;
namespace observer
{
class Program
{
public static void Main(string[] args)
{
// ObserverOriginal.Start();
ObserverDelegate.Start();
}
}
}
觀察者模式
----------遊戲回合開始----------
最終BOSS 擊殺 二號玩家 !
一號玩家 知道 二號玩家 陣亡了!
三號玩家 知道 二號玩家 陣亡了!
四號玩家 知道 二號玩家 陣亡了!
五號玩家 知道 二號玩家 陣亡了!
最終BOSS 知道 二號玩家 陣亡了!
----------過了一段時間----------
最終BOSS 擊殺 四號玩家 !
一號玩家 知道 四號玩家 陣亡了!
三號玩家 知道 四號玩家 陣亡了!
五號玩家 知道 四號玩家 陣亡了!
最終BOSS 知道 四號玩家 陣亡了!
----------過了一段時間----------
一號玩家 擊殺 最終BOSS!
一號玩家 知道 最終BOSS 陣亡了!
三號玩家 知道 最終BOSS 陣亡了!
五號玩家 知道 最終BOSS 陣亡了!
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()
}
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)
}
最終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中,使用切片存放觀察者門。
應用範例:
優點:
缺點:
使用場景:
注意事項: