用自己的方式理解觀察者模式,並嘗試撰寫 C# 範例程式碼說明
物件導向程式設計中通常都會遵循單一職責等設計原則,所以通常會有一個一個的物件負責處理某一件事情,而在撰寫開發的時候,通常就會在程式內去直接 new 物件實體出來,這就導致了程式相依於該物件實體,因為寫死在裡面了,動不了。
舉例來說:在主程式內我們希望有一個 People,並且讓這個人去移動,程式碼如下
// Main
internal class Program
{
private static void Main(string[] args)
{
var people = new People("art");
people.Move();
}
}
// Lib
public class People
{
public string Name { get; }
public People(string name)
{
Name = name;
}
public void Move()
{
var myCar = new Car();
myCar.Drive(this);
}
}
internal class Car
{
public void Drive(People people)
{
Console.WriteLine($"{people.Name} Moving by car");
}
}
這當然違反了開放封閉原則,未來如果你想要更換別的物件實體,就必須要重新去修改那一段寫死的程式,而且是要修改 Lib ,也因此,通常都會利用一些技巧,讓程式內不要出現 new 這件事情,而其中一種方法,就是透過注入的方式去處理
將原本程式依賴某些物件的這個部分,把這個控制權從程式內部改為從外部傳進來,也就是程式依賴的物件實體改由注入取得。只要能夠達到這個目標,手段怎麼做那就是看情況、需求來調整,有透過建構式注入的,也有直接透過屬性注入的,當然也有透過方法來注入的。
以上面的例子來說,我們在主程式內先將交通工具準備好,再把交通工具交給人,接著讓人去移動。
// Main
internal class Program
{
private static void Main(string[] args)
{
var car = new Car();
var people = new People("art");
people.Drive(car);
people.Move();
Console.ReadKey();
}
}
// Lib
public class People
{
private Car _myCar;
public string Name { get; }
public People(string name)
{
Name = name;
}
public void Move()
{
_myCar.Drive(this);
}
public void Drive(Car car)
{
_myCar = car;
}
}
public class Car
{
public void Drive(People people)
{
Console.WriteLine($"{people.Name} Moving by car");
}
}
第二個版本已經將交通工具,改由主程式建立,然後傳遞給人,所以也新增加了一個 Drive() 方法,讓交通工具先保存在 People 內,而等到 Move() 的時候,就直接調用剛才注入的交通工具來執行,所以產生依賴物件的控制權不再由 Move() 方法內直接實作,而是相依於外部注入的實體,這樣的行為我們就稱呼它叫做依賴注入
當然還可以有更多的版本繼續走下去,還有很多要改善的地方,但是這個 part 我們只要先搞懂依賴注入是怎麼一回事就夠了
接著我們換到另外一個情境,看看透過剛才學到的技巧,應該怎麼實作
嗯,就繼續剛才的交通工具好了,現在的情況是這樣的,有一群小夥子在飆車,每一個飆車族手上都有個無線電,一開始飆車的時候,每個人都必須要先調整到同一個無線電的頻道,而飆車地點從無線電公布,這樣大家才聽得到。聽到了就會一窩蜂的往那邊飆過去,所以大概會有幾個類別:
無線電:需要廣播飆車的地點 ( Notice ) 飆車族:就是飆車的人,要能夠飆車 ( CrazyMove ),一開始需要先調整無線電的頻道 程式碼大概像是這樣
// Main
internal class Program
{
private static void Main(string[] args)
{
var radio = new Radio();
var man1 = new FastMan("張三", radio);
var man2 = new FastMan("李四", radio);
radio.Notice("陽明山");
Console.ReadKey();
}
}
// Lib
public class FastMan
{
private readonly string _name;
private readonly Radio _radio;
public FastMan(string name, Radio radio)
{
this._name = name;
this._radio = radio;
_radio.SetRoger(this);
}
public void CrazyMove(string place)
{
Console.WriteLine($"{_name} 接獲指示,飆車前往{place}...");
}
}
public class Radio
{
private readonly List<FastMan> _list = new List<FastMan>();
public void Notice(string place)
{
Console.WriteLine($"無線電傳來聲音:讓我們奮力奔向 {place} 吧!!");
foreach (var man in _list)
{
man.CrazyMove(place);
}
}
public void SetRoger(FastMan fastMan)
{
_list.Add(fastMan);
}
}
這邊用到的技巧就是幾個物件導向的原則,合起來就達到了這個效果,在程式碼中只要先透過 Radio 宣布飆車地點,所有人就會接收到資訊,並做出相應的行為。
把這個概念完善一點,調整頻道的動作,其實就是加入一個清單,離開頻道,就是從清單中移除;廣播通知的對象則依循清單中的名單處理;通知對象其實就是透過依賴注入的方式,將物件注入給 Radio;聽到的人具體要做甚麼行為,則是由聽到的自行決定;也因為不是所有人都喜歡飆車,說不定也有的人聽到之後的反應是繼續做自己的事情,所以程式碼為了要有彈性,應該要做一個介面,其他的人就實作這個介面,來實現具體的行為。
調整一下程式碼
// Main
private static void Main(string[] args)
{
var radio = new Radio();
var man1 = new FastMan("張三", radio);
var man2 = new IdleMan("李四", radio);
radio.Notice("陽明山");
Console.ReadKey();
}
// Lib
public interface IRadioKeeper
{
void CrazyMove(string place);
}
public class IdleMan : IRadioKeeper
{
private readonly string _name;
private readonly Radio _radio;
public IdleMan(string name, Radio radio)
{
this._name = name;
this._radio = radio;
_radio.SetRoger(this);
}
public void CrazyMove(string place)
{
Console.WriteLine($"{_name} 接獲指示,站在原地發呆看著其他人飆車前往{place}...");
}
}
public class Radio
{
private readonly List<IRadioKeeper> _list = new List<IRadioKeeper>();
public void Notice(string place)
{
Console.WriteLine($"無線電傳來聲音:讓我們奮力奔向 {place} 吧!!");
foreach (var man in _list)
{
man.CrazyMove(place);
}
}
public void SetRoger(IRadioKeeper fastMan)
{
_list.Add(fastMan);
}
}
那接著又如果張三想回家睡覺,不想要飆車了怎麼辦?所以我們要幫它做一個離開無線電頻道的方法,而因為這個方法不只張三用,李四可能也會用,所以我們應該將它放在介面,讓繼承的類別實作,實作細節就是呼叫 radio 的一個方法,讓廣播的對象清單移除掉。而 radio 類別,為了相依介面,我們也應該將它抽象成為介面、或是抽象類別
最終程式碼在整理一下,大概會是這樣子,因為先前例子沒有寫得很好,順便把一些名稱重新命名了
// Main
private static void Main(string[] args)
{
var radio = new Radio();
var man1 = new FastMan("張三", radio);
var man2 = new IdleMan("李四", radio);
radio.NoticeEverybody("陽明山");
man1.LeaveRadio();
radio.NoticeEverybody("北海岸");
Console.ReadKey();
}
// Lib
public interface IRadioKeeper
{
void UpdatePlace(string place);
void JoinRadio();
void LeaveRadio();
}
public class IdleMan : IRadioKeeper
{
private readonly string _name;
private readonly BaseRadio _baseRadio;
public IdleMan(string name, BaseRadio baseRadio)
{
this._name = name;
this._baseRadio = baseRadio;
JoinRadio();
}
public void UpdatePlace(string place)
{
Console.WriteLine($"{_name} 接獲指示,站在原地發呆看著其他人飆車前往{place}...");
}
public void JoinRadio()
{
_baseRadio.JoinChannel(this);
}
public void LeaveRadio()
{
_baseRadio.LeaveChannel(this);
}
}
public class FastMan : IRadioKeeper
{
private readonly string _name;
private readonly BaseRadio _baseRadio;
public FastMan(string name, BaseRadio baseRadio)
{
this._name = name;
this._baseRadio = baseRadio;
JoinRadio();
}
public void UpdatePlace(string place)
{
Console.WriteLine($"{_name} 接獲指示,飆車前往{place}...");
}
public void JoinRadio()
{
_baseRadio.JoinChannel(this);
}
public void LeaveRadio()
{
_baseRadio.LeaveChannel(this);
}
}
public class Radio : BaseRadio
{
public void ChangePlace(string place)
{
Console.WriteLine($"無線電傳來聲音:讓我們奮力奔向 {place} 吧!!");
NoticeEverybody(place);
}
}
public abstract class BaseRadio
{
private readonly List<IRadioKeeper> _list = new List<IRadioKeeper>();
public void NoticeEverybody(string place)
{
foreach (var man in _list)
{
man.UpdatePlace(place);
}
}
public void JoinChannel(IRadioKeeper man)
{
_list.Add(man);
}
public void LeaveChannel(IRadioKeeper man)
{
_list.Remove(man);
}
}
好囉,我們剛才已經把觀察者模式實作完畢了,讓我們來看一下觀察者模式的定義:一個目標物件管理所有相依於它的觀察者物件,並且在它本身的狀態改變時主動發出通知
所以我們除了radio.NoticeEverbody("somewhere");
的實作,沒有與 wiki 上的定義相符合,而是每一次都通知,但是實作模式切記不能生搬硬套,主要還是要看情境,你說不說的出來,為甚麼你要這樣寫,而不是怎樣怎樣。
我當然可以說因為我的情境是飆車族、通知的方法是無線電,就像聊天室一樣我說甚麼就通知甚麼,不管我這次說的話跟上次說的話一不一樣都要通知。
但如果我的情境不是這個,而是在一些伺服器的狀態更新之類的,那麼當然是我的狀態有更新,才需要發出 Request 給觀察者,所以就像 Bill 每次都會提到的獨孤九劍,沒有一定的形式,招要活學活使。
透過上面的步驟,大概也能夠理解到設計模式其實都是用物件導向原則組合出來的,怎麼組合的或許每個人的作法都有一些不同,但重要的是目的與情境有沒有滿足。希望大家學習設計模式的時候不要看著圖生搬硬套,應該從目的著手,
設計模式其實都是基於物件導向原則的一個實作出來的方式,只要達到目的,怎麼完成的方式其實並不重要