有時候對於對象來說。在一個軟體中,不直接通過互相引用而做到共用資訊是非常有用的。比如像帶有外掛程式的軟體。可以互相進行通訊。假設我們有了很多個物件。其中一些包含一些資料。而另一些對象需要消費這些資料 不同的子集,我們不通過對資料生產者和消費者的直接引用來實現,而是通過更低耦合的方式。叫做建立一個“BlackBoard”(黑板)對象。該對象允許其他對象自由對其進行讀取/寫入資料。這種解耦方式使得消費者不知道也不必知道資料來自哪裡。如果想要瞭解更多關於黑板模式的資訊。我們常說的。Google是你最好的朋友。
一個最簡單的黑板對象應該是 Dictionary一些簡單的命名值的字典。所有的對象共用同一個字典引用。使得他們可以交換這些命名資料。這種方法有兩個問題。一個是名字。一個是型別安全—資料生產者和消費者對每一個資料值都必須共用一個字串標識。消費者也沒有對字典中的值進行編譯時間的類型檢查,比如,可能期望一個小數,結果運行時讀到了字串。本文對這兩個問題示範了一種解決方案。
背景
最近我在開發一個通用任務的非同步執行的引擎。我的通用任務通常有Do/Undo方法。原則上是相互獨立的,但是有一些任務需要從已經執行的任務重請求資料。比如。一個任務可以
為一個硬體裝置建立一個API,隨後的任務就可以使用建立好的API來操作硬體裝置。但是。我不想我的執行引擎知道關於這個執行任務的任何資訊。而且。我也不想直接手工的就在一個任務裡引用另一個任務。
黑板類
黑板類本質上是一個Dictionary的封裝類,對外暴露Get和Set方法。黑板類允許其他Object Storage Service並且取回資料。但是要求這些資料使用一個
BlackboardProperty 類型的標識符來表示這些資料是可存取的。BlackboardProperty 對象應該在那些準備讀寫黑板類的對象之間共用,因此,他應該在那些類中作為一個靜態成員。(很像WPF的相依性屬性。是他們所屬控制項的靜態成員)
注意:命名安全應該可以通過同樣的方式實現。但是但是依然沒有解決型別安全的問題。那麼。到了主要的部分了。那就是黑板類的代碼了
public class Blackboard : INotifyPropertyChanged, INotifyPropertyChanging{ Dictionary<string, object> _dict = new Dictionary<string, object>(); public T Get<T>(BlackboardProperty<T> property) { if (!_dict.ContainsKey(property.Name)) _dict[property.Name] = property.GetDefault(); return (T)_dict[property.Name]; } public void Set<T>(BlackboardProperty<T> property, T value) { OnPropertyChanging(property.Name); _dict[property.Name] = value; OnPropertyChanged(property.Name); } #region property change notification public event PropertyChangingEventHandler PropertyChanging; public event PropertyChangedEventHandler PropertyChanged; protected virtual void OnPropertyChanging(string propertyName) { if (PropertyChanging != null) PropertyChanging(this, new PropertyChangingEventArgs(propertyName)); } protected virtual void OnPropertyChanged(string propertyName) { if (PropertyChanged != null) PropertyChanged(this, new PropertyChangedEventArgs(propertyName)); } #endregion}
黑板屬性(BlackBoardProperty)類
BlackBoardProperty 類 提供了一個標識符來存取黑板對象中的資料。定義了名稱和值的類型。也定義了一個預設的傳回值。以防黑板類中對應屬性沒有值。
/// <summary>/// 對應黑板類中的屬性的強型別標識符/// </summary>/// <typeparam name="T">該類能識別的屬性值的類型</typeparam>public class BlackboardProperty<T>{ /// <summary> /// 屬性的名稱 /// <remarks> /// 黑板類的屬性通過名稱來儲存。請注意不要讓相同的名字有不同的屬性值。因為如果被用在同樣的黑板類上。他們會互相覆蓋值 /// </remarks> /// </summary> public string Name { get; set; }//當黑板對象沒有包含對應屬性的時候。該Factory 方法被用來提供一個預設的值 // Func<T> _createDefaultValueFunc; public BlackboardProperty(string name) : this(name, default(T)) { } /// <summary> /// /// </summary> /// <param name="name"></param> /// <param name="defaultValue"> /// 當黑板類不包括該屬性的時候。該值會被返回。 /// <remarks> /// 如果預設的值是一個常量或是一個實值型別的時候,使用該構造方法。 /// </remarks> /// </param> public BlackboardProperty(string name, T defaultValue) { Name = name; _createDefaultValueFunc = () => defaultValue; } /// <summary> /// </summary> /// <remarks>/// 如果預設值是一個參考型別,並且,你不想要共用該執行個體給多個黑板對象的時候。請使用該/// 建構函式 /// </remarks> /// <param name="name"></param> /// <param name="createDefaultValueFunc"></param> public BlackboardProperty(string name, Func<T> createDefaultValueFunc) { Name = name; _createDefaultValueFunc = createDefaultValueFunc; } public BlackboardProperty() { Name = Guid.NewGuid().ToString(); } public T GetDefault() { return _createDefaultValueFunc(); }}
我承認不是非常有用的代碼。但是。能夠類比兩個類的使用。
下一個例子會更和現實情況接近。但是肯定是被簡化過了的。在下面的例子裡。我定義了集中不同的任務。我用這些任務來啟動對硬體裝置的串連。操作裝置。關閉串連。這些任務通過一個執行引擎依次執行,這些任務通過一個公用的黑板類來共用資料。至於這個任務類的和執行引擎(ExecutionEngine)類還是留到另一篇文章中把。
//一個裝置的介面例子interface IDevice{ void Connect(); void Reset(); decimal Read(string obis); void Close();}//任務執行個體化了裝置api,設定並且啟動串連class InitiateDeviceTask : Task{ //這個用來定義黑板類裡的DeviceAPI 變數 public static BlackboardProperty<IDevice> DeviceAPIProperty = new BlackboardProperty<IDevice>(); protected override void Execute(Blackboard context) { IDevice deviceAPI = null; //deviceAPI = ...建立並設定裝置 API, 串連到裝置 deviceAPI.Connect(); //這是最重要的部分 – 我把deviceAPI 放在了黑板類裡 //並使用 DeviceProperty作為該對象的標識符 context.Set(DeviceAPIProperty, deviceAPI); }}//t該任務重設裝置class ResetDeviceTask : Task{ protected override void Execute(Blackboard context) { //從黑板類裡取出 deviceAPI // InitiateDeviceTask這個任務必然要在本任務之前執行 IDevice deviceAPI = context.Get(InitiateDeviceTask.DeviceAPIProperty);//注意不需要進行轉換 deviceAPI.Reset(); }}//該任務讀取裝置的一個寄存器class ReadRegisterTask : Task{ public string RegisterName { get; set; } //Factory 方法為該屬性提供一個預設的值 public static BlackboardProperty<Dictionary<string, decimal>> ReadingsProperty = new BlackboardProperty<Dictionary<string, decimal>>("Readings", () => new Dictionary<string, decimal>()); protected override void DoExecute(Blackboard context) { IDevice deviceAPI = context.Get(InitiateDeviceTask.DeviceAPIProperty); decimal value = deviceAPI.Read(RegisterName);//我們可以把值放在一個倉庫裡 //可以把寄存器的名字和值儲存到字典裡。這樣其他的任務也可以使用該值了 //如果 readings屬性不在黑板類裡。那麼一個空的字典對象將會被 createDefaultValueFunc Factory 方法建立ReadingsProperty來完成。 context.Get(ReadingsProperty)[RegisterName] = value; }}//該任務關閉對裝置的串連class CloseConnectionTask : Task{ protected override void Execute(Blackboard context) { IDevice deviceAPI = context.Get(InitiateDeviceTask.DeviceAPIProperty); deviceAPI.Close(); }}class Demo{ public void StartDemo() { ExecutionEngine engine = new ExecutionEngine(); engine.Enque(new InitiateDeviceTask()); //建立 deviceAPI 對象, 串連, 放在黑板類裡 engine.Enque(new ReadRegisterTask() { RegisterName = "1.8.0" }); //使用裝置api來讀取一個值 engine.Enque(new ReadRegisterTask() { RegisterName = "3.8.0" }); //使用裝置api來讀取另一個值 engine.Enque(new ResetDeviceTask()); //使用裝置api來重設裝置 engine.Enque(new CloseConnectionTask()); //使用裝置api來關閉裝置串連 //引擎建立了一個黑板, /開啟一個新線程並且一個接一個將任務傳遞給黑板對象來執行 // engine.Start(); }}
黑板類另一種可能的使用方式就是一個支援外掛程式的軟體。如果需要的話允許外掛程式進行通訊,這種情況下屬性改變的時候能夠通知是很有用的。
還有一件重要的事情是注意 BlackboardProperty 執行個體一般應該作為邏輯上擁有該屬性的類的一個靜態成員。那麼既然那是靜態。同樣的BlackboardProperty 執行個體就可以出現在多個黑板對象裡。當某一個給定的屬性。黑板對象裡沒有值的時候。他會請求BlackboardProperty 執行個體提供一個預設的值。預設的值可能是一個參考型別,因此,如果你不想在多個黑板對象間共用同一個引用。在建立BlackboardProperty 對象的時候務必使用下面的建構函式。
public BlackboardProperty(string name, Func createDefaultValueFunc)
這就會使得預設的值不會在多個黑板對象間共用。
有意思的地方
我應該說過了。這個方案一部分是受微軟WPF中相依性屬性的影響。還參考了我前段時間讀到的一篇關於枚舉類的文章
許可
本文包括原始碼和檔案在CPOL下授權。
原文地址: Type-safe-blackboard-property-bag
著作權聲明:本文由http://leaver.me 翻譯,歡迎轉載分享。請尊重作者勞動,轉載時保留該聲明和作者部落格連結,謝謝!