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

来源:https://www.cnblogs.com/linxiaoxu/archive/2022/09/11/16683365.html
-Advertisement-
Play Games

一個菜鳥的設計模式之旅,本程式實現觀察者模式。使用C#、Go兩門語言分別進行實現。程式創建一個全局游戲死亡事件通知,5個玩家、1個Boss,當任意一方死亡時,在場存活者都能收到陣亡者的消息。 ...


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

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

程式介紹

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

您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 使用asp.net core 開發應用系統過程中,基本上都會涉及到用戶身份的認證,及授權訪問控制,因此瞭解認證和授權流程也相當重要,下麵通過分析asp.net core 框架中的認證和授權的源碼來分析認證、授權的原理及認證和授權的關係。 認證是什麼? 認證是應用系統識別當前訪問者的身份的一個過程,當 ...
  • 題目 中文 實現一個以 T 作為泛型參數的 IsNever類型. 如果 T 是never, 返回 true, 否則返回 false. 示例: type A = IsNever<never>; // expected to be true type B = IsNever<undefined>; // ...
  • 拉取一個centos鏡像 docker pull centos:centos7 運行一個容器 docker run -i -t -d --restart=always --name baota -p 1870:8888 -p 1871:3306 -p 1872:22 -p 1873:443 -p 1 ...
  • 最近工作需要,研究了一些Liunx性能工具,發現sar這個工具非常優秀。sar是一個非常全面的一個分析工具,對文件的讀寫,系統調用的使用情況,磁碟IO,CPU相關使用情況,記憶體使用情況,進程活動等都可以進行有效的分析。sar工具將對系統當前的狀態進行取樣,然後通過計算數據和比例來表達系統的當前運行狀 ...
  • 2022-09-10 MySQL中的自連接 何謂自連接? 自連接,即為自己查自己,本表查詢本表。 自連接一般使用於何種地方? 例如:如果在設計一張表中,表中的欄位名包含id(省份/市的郵政編碼),title(省份名/市級名),cid(如果前面title是省份名,那麼此處為空;如果前面title是市級 ...
  • 《Redis 7.x 入門和開發實戰》技術專欄通過基礎知識介紹入門-環境搭建-項目開發實踐,讓初學者快速掌握Redis。內部包括分散式緩存組件Redis 7.x的安裝配置部署、基本數據類型、常用命令、操作實踐、HyperLoglog數據結構、事務、慢日誌分析、Redis 集成 Spring Boot... ...
  • 1.視圖:view 視圖就是一張虛擬的表。表是真正存數據的,視圖只是顯示查詢結果。 視圖的作用:隱藏表的結構、簡化sql嵌套查詢操作 註意:視圖就是你要查詢數據的一個中間結果集,我們一般只用來做數據查詢的 創建視圖:create view view_name as 查詢語句 例如: mysql> c ...
  • 多用戶即時通訊系統 包含推消息 私聊 發文件 等功能 筆記目錄:(https://www.cnblogs.com/wenjie2000/p/16378441.html) 為什麼選擇這個項目 有趣 涉及到java各個方面的技術 ✔項目框架設計 ✔java面向對象編程 ✔網路編程 ✔多線程 ✔IO流 ✔ ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...