關於《.NET 框架設計》書中 Demo 的更正 (二)
首先,感謝作者在本書中 3.9 節模式的分享。在此繼續分享我對於本書中的一些 demo 的看法和建議。歡迎批評指正!謝謝。
當我讀完第三章 非同步訊息事件驅動模式 之後,對於本書,我忽然有了這個一個印象,那就是:
理想很豐滿,現實很骨感
作者在書的首頁裡面曾經提到,他這些東西,最開始是在部落格上寫的,不過我沒有翻閱他的部落格,接下來的我會按照書中 demo 的內容來敘述。
下面,我會就第三章 3.9 節 非同步訊息事件驅動模式,來說下作者示例程式碼中的一些問題,以及我對於這篇例項的修改。就像作者在書的開頭所說的那樣,我也和他一樣,我說的全都是乾貨!
首先,當我們讀完作者對於第 3.9 節的模式原理性方面的描述之後,我們會發現,其實非同步訊息事件驅動模式的機制的核心,實際上就是三個字“無等待”。無等待的提交事務、無等待的獲取處理結果、無等待的批量處理。當然,做的好一點,實際上還是應該可以讓這個模式身上看到鈍化模式的身影,鈍化模式的本質就是“物件方法和資料的持久化”。
無等待,加上持久化,可以“讓擎天柱插上翅膀”。
但是作者…………在本書的 demo 中並沒有用到。
非同步訊息事件驅動模式,其實很簡單,即無等待地將訊息全部推送到伺服器,並非同步接收反饋結果。這裡,為了實現這個模式,很重要的一個特點就是“非同步”。而且,很明顯(也許是為了簡化~~),作者在這個 demo 中沒有用到非同步!不過說句實話,這個 demo 應該是使用非同步的,實際上,沒有非同步,這個 demo 根本沒法做到無等待提交,不是嗎?
接下來,我們看一下這個 demo 的程式碼。
(1) lock 語句
在本書第 150 頁,作者使用了下面這樣的程式碼,我覺得不是很合適,可能是由於 demo 緣故,作者敲了這麼段“隨手程式碼”。雖然對於理解設計思想來說並無大礙,這個問題可以忽略不計。
lock (this)
{
if (this.Count > 0)
......
}
lock 語句中被 lock 的物件也是有講究的,最安全合適的做法是私有變數的 lock 操作,這裡我就不做具體討論了,修改的程式碼如下:
private static object LockObject = new object();
lock (LockObject)
{
......
}
(2) 自定義事件
同頁,定義了一個 MessageNotifyEvent 事件,在 remove 裡面的程式碼寫錯了。應該是 -=,事實上,對於像書上 demo 的寫法,你可以完全單單定義一個 event 變數就可以了,.NET 事件內部邏輯會幫你處理這些 add 和 remove 的操作,除非你有特別的需求,需要在 add 和 remove 的時候做其他的操作,否則我認為,就 demo 來說,從簡即可,無需包裝成屬性進行操作。
public event MessageEventNotifyHandler MessageNotifyEvent
{
add {......}
remove
{
if (......)
{
this.messageNotifyEvent += value;
}
}
}
(3) 關於列舉和常量的用法
在本書第 15 頁,作者的原話是這樣的:“…………總能看到常量和列舉這兩個元素出現在對方的位置上,這是不是很奇怪?應該使用列舉的地方結果卻使用了常量,而應該使用常量的地方又使用了列舉。…………”,然後,當我讀到第 150~151 頁的時候,我覺得這裡有那麼一點“自相矛盾”的味道。
[Serializable]
public struct OperationState
{
public const string Finished = "Ok";
public const string Exception = "Error";
}
其實,這裡對於操作訂單的狀態,列舉 再適合不過了,但他卻用了常量。
(4) 用同步的 demo 來解釋非同步的模式
這個 demo 應該是使用 BeginInvoke 的非同步操作,而作者只是簡單的使用了後期繫結、但實為同步的 DynamicInvoke 來處理請求操作,乍一看,還是挺高大上。所以這個 demo 無法實現註釋所描述的那樣,即無等待傳送訂單資料。
(5) 用單一的控制檯 demo 展現 server-client 模式原理
這個就不詳細描述了,這點會根據後面的程式碼說明來
修改後的 DEMO 程式碼
修改後的 demo 程式碼有點長,邏輯設計有點複雜,不過還是基於作者的這個 demo 的。
改動後的 demo 分為三塊:客戶端、伺服器端、公共類庫,因為我覺得就算 demo 再簡單,也得符合和體現設計原理和原則。Demo 裡面,server 和 client 之間的互動,他們的核心是資料,資料是公共的,所以這些應該被抽取並定義在共同引用的類庫專案裡面,這裡定義在 Common 這個類庫裡。
/// <summary>
/// 訂單狀態
/// </summary>
[Serializable]
public enum OrderState
{
/// <summary>
/// 訂單未處理
/// </summary>
Unprocessed,
/// <summary>
/// 正在處理中
/// </summary>
InProcessing,
/// <summary>
/// 成功提交訂單
/// </summary>
Submitted,
/// <summary>
/// 提交失敗,訂單不合法
/// </summary>
Invalid,
/// <summary>
/// 由於伺服器連線問題,需要重新提交
/// </summary>
NeedTryAgain
}
原先的訂單操作狀態是被抽取出來的,和訂單是分離的,而且狀態只有兩個,OK 和 Error,實際上,根據操作,我設計了五個訂單狀態,根據註釋和列舉名稱,我覺得,應該很好理解。
[Serializable]
public class OrderItem
{
public string Id { get; private set; }
public string Name { get; private set; }
public double Price { get; set; }
public int Count { get; set; }
public OrderItem(string id, string name, double price)
{
Id = id;
Name = name;
Price = price;
}
}
[Serializable]
public class Order
{
public string Id { get; set; }
public OrderState State { get; set; }
public List<OrderItem> Items { get; set; }
public Order()
{
State = OrderState.Unprocessed;
}
}
這是訂單類,處理狀態被設計成訂單的一部分,其實這才是合理的,訂單的狀態應該屬於訂單本身。
接下來,我們先看看 server 端。為了模擬 server,最簡單的方式就是 webservice,這裡我使用 WCF 新建一個 project 來模擬 server 端。server 端的協議介面看上去像這個樣子。
[ServiceContract]
public interface IOrderSubmitService
{
/// <summary>
/// 提交訂單
/// </summary>
/// <param name="order"></param>
[OperationContract]
OrderProcessMessage SubmitOrder(Order order);
}
就一個簡單的 SubmitOrder,沒什麼花頭,來看看該方法返回的 OrderProcessMessage 類的定義。
[Serializable]
[DataContract]
public partial class OrderProcessMessage
{
[DataMember]
public string MessageId { get; private set; }
[DataMember]
public OrderState MessageState { get; private set; }
[DataMember]
public string MessageException { get; set; }
public OrderProcessMessage(string id)
{
MessageId = id;
MessageState = OrderState.Submitted;
MessageException = null;
}
public OrderProcessMessage(string id, string exceptionMessage)
{
MessageId = id;
MessageState = OrderState.Invalid;
MessageException = exceptionMessage;
}
}
伺服器處理訂單之後會返回這個類的物件,這個物件指示是否處理成功,如果失敗,那麼失敗的原因是什麼。接下來看 WCF 服務類。
[ServiceBehavior]
public class OrderSubmitService : IOrderSubmitService
{
[WebMethod]
public OrderProcessMessage SubmitOrder(Order order)
{
var message = String.Format("訂單 {0} 格式不正確", order.Id);
return IsOrderValid(order) ? new OrderProcessMessage(order.Id) :
new OrderProcessMessage(order.Id, message);
}
private bool IsOrderValid(Order order)
{
return order.Id.StartsWith("P");
}
}
有關於 WCF 方面的東西我就不解釋了。這個 DEMO 中的 WCF Server 也是灰常簡單的~~,高手估計也就掃一眼。
接下來,主要的邏輯就在客戶端,在客戶端我們就一個類:OrderQueue。這個類負責和 Order 有關的操作。首先,我們定義一個委託,這個委託用來定義事件,事件用來觸發提交操作,所以委託和伺服器 SubmitOrder 具有相同的簽名。
/// <summary>
/// 處理訊息事件
/// </summary>
/// <param name="order"></param>
public delegate OrderSubmitSvc.OrderProcessMessage OrderQueueEventNotifyHandler(Order order);
OrderQueue 類的設計如下:
public class OrderQueue
{
private static object LockObject = new object();
public static OrderQueue GlobalQueue = new OrderQueue();
public List<Order> Orders = new List<Order>();
private System.Timers.Timer Timer = new System.Timers.Timer();
public List<OrderProcessMessage> ProcessMessages { get; private set; }
public event OrderQueueEventNotifyHandler OrderProcessNotifyEvent;
private OrderQueue();
private Order GetFirstUnprocessedOrder();
private void PushProcessedMessage(OrderProcessMessage message);
private void SetOrdersRetry(Order order);
public bool HasUnprocessedOrders();
public bool HasInProcessingOrders();
public bool HasRetryOrders();
public void PrepareNeedRetryOrders();
public void BeignDetectOrders();
public void StopDetectOrders();
}
書中原 demo 將此類繼承 Queue,實際上對於非同步操作,Queue 是不夠的,後面會說到。原 demo 對於該類使用了單例模式,在這裡,我不作設計改動。在類的內部定義 Orders 集合,用於存放動態新增的 Order 項。Timer 用來定時的掃描 Orders 集合,如果有未處理的訂單,那麼,訂單處理邏輯將被觸發。而另外一個 ProcessMessages 集合用於存放每一個訂單的處理結果。
下面,來說一下 OrderProcessNotifyEvent 事件,這個事件用於操作訂單處理,當 Orders 中存在未處理的訂單時,OrderProcessNotifyEvent 所繫結的方法將被觸發,在這裡,我們會將其繫結到 WCF 的 SubmitOrder 方法。
重點是私有構造器裡面用 lambda 方式定義的 Timer 事件方法:
private OrderQueue()
{
ProcessMessages = new List<OrderProcessMessage>();
Timer.Interval = 1000;
Timer.Elapsed += (sender, e) =>
{
if (HasUnprocessedOrders())
{
if (OrderProcessNotifyEvent != null)
{
// 讀取第一個未處理的訂單
Order order = GetFirstUnprocessedOrder();
if (order == null) return;
// 設為正在處理中
order.State = OrderState.InProcessing;
// 新增已處理資訊
OrderProcessNotifyEvent.BeginInvoke(order, ar =>
{
var processedOrder = ar.AsyncState as Order;
try
{
OrderProcessMessage messageResult = OrderProcessNotifyEvent.EndInvoke(ar);
if (processedOrder != null)
{
processedOrder.State = messageResult.MessageState;
}
// 新增處理結果
PushProcessedMessage(messageResult);
// 將處理完的訂單移走
Orders.Remove(processedOrder);
}
catch (EndpointNotFoundException ex)
{
SetOrdersRetry(processedOrder);
}
}, order);
}
}
};
BeignDetectOrders();
}
根據程式碼邏輯,我們可以看到,每隔一定的時間,它會掃這個 Orders 集合,如果存在未處理的,那麼首先將第一個出現的未處理的訂單取出,接著馬上將其狀態置為 InProcessing,此處這一步很關鍵,因為是多執行緒操作,所以,這裡如果不設定狀態,那麼後續的執行緒取資料會出現混亂,接著傳入當前訂單,非同步呼叫提交訂單,在提交的時候,會發生兩種可能性:成功連線伺服器、伺服器連線失敗。如果連線成功,那麼他會在回撥函式的 EndInvoke 被呼叫的時候觸發 EndpointNotFoundException 異常,這時,我們就捕獲該異常,將該訂單狀態設定為 NeedTryAgain。如果處理成功,訂單狀態將被改為 Submitted / Invalid 其中之一。將處理結果記錄下來,然後將其從原先的訂單集合中刪除。
這裡就要談一談為什麼使用 List 而不是 Queue,Queue 是先進先出,它的進出永遠是頭尾的操作,沒有辦法從中間去抽調資料;為什麼說 Queue 不滿足當前的操作邏輯呢?因為,提交資料是非同步的,也就是說,當我第一個執行緒 BeginInvoke 之後,第二個執行緒在下一個 token 到來之後,立馬進入而這個時候,假設第一個訂單還未處理完,第二個訂單被設定成 InProcessing。接著,如果第二個訂單在第一個訂單之前處理完成,那麼這個時候需要從原集合中刪除,如何刪?這個時候如果僅僅是純粹的 Dequeue,那麼就錯了,所以 Queue 不適合,而萬能的 List 才是合適的選擇。
該類中其它一些相關的方法實現如下:
private Order GetFirstUnprocessedOrder()
{
for (int i = 0; i < Orders.Count; i++)
{
var order = Orders[i];
if (order.State == OrderState.Unprocessed)
{
return order;
}
}
return null;
}
private void PushProcessedMessage(OrderProcessMessage message)
{
lock (LockObject)
{
foreach (var existedMessage in ProcessMessages)
{
if (existedMessage.MessageId == message.MessageId)
{
existedMessage.MessageState = message.MessageState;
existedMessage.MessageException = message.MessageException;
return;
}
}
ProcessMessages.Add(message);
}
}
private void SetOrdersRetry(Order order)
{
// 新增未處理訊息
var message = new OrderProcessMessage { MessageId = order.Id,
MessageState = OrderState.NeedTryAgain, MessageException = "無法連線伺服器" };
PushProcessedMessage(message);
order.State = OrderState.NeedTryAgain;
}
還有幾個簡單的輔助方法,瞅一眼就行:
public bool HasUnprocessedOrders()
{
return Orders.Count(o => o.State == OrderState.Unprocessed) > 0;
}
public bool HasInProcessingOrders()
{
return Orders.Count(o => o.State == OrderState.InProcessing) > 0;
}
public bool HasRetryOrders()
{
return Orders.Count(o => o.State == OrderState.NeedTryAgain) > 0;
}
public void PrepareNeedRetryOrders()
{
foreach (Order order in Orders)
{
if (order.State == OrderState.NeedTryAgain)
{
order.State = OrderState.Unprocessed;
}
}
}
public void BeignDetectOrders()
{
Timer.Start();
}
public void StopDetectOrders()
{
Timer.Stop();
}
最後,是客戶端 DEMO 的業務處理邏輯:
static void Run()
{
// 初始化遠端訂單服務
OrderSubmitServiceClient orderService = new OrderSubmitServiceClient();
// 繫結服務處理方法到訂單佇列通知當中
OrderQueue.GlobalQueue.OrderProcessNotifyEvent += orderService.SubmitOrder;
// 初始化訂單
var orders = CreateOrders();
// 無等待傳送訂單資料
orders.ForEach(order => OrderQueue.GlobalQueue.Orders.Add(order));
// 等待訂單處理完成
WaitOrderQueueProcess();
for (OutputOrdersProcessResult(OrderQueue.GlobalQueue);
OrderQueue.GlobalQueue.HasRetryOrders();
OutputOrdersProcessResult(OrderQueue.GlobalQueue))
{
Console.Write("\r\n還有未處理完的訂單,是否繼續提交處理?(Y / N): ");
string presskey = null;
for (presskey = Console.ReadLine().ToUpper();
presskey != "Y" && presskey != "N"; presskey = Console.ReadLine().ToUpper())
{
Console.Write("\r\n請正確輸入(Y / N): ");
}
if (presskey == "Y")
{
Console.WriteLine("\r\n請等待,正在嘗試再次提交訂單......");
OrderQueue.GlobalQueue.PrepareNeedRetryOrders();
Wait();
}
else break;
}
OrderQueue.GlobalQueue.StopDetectOrders();
Console.WriteLine("全部處理結束,按任意鍵退出...");
}
當我們無等待提交訂單之後,我們就可以在主執行緒中用 WaitOrderQueueProcess 來等待非同步提交的返回結果。
static void WaitOrderQueueProcess()
{
const int DOT_COUNT = 6;
for (int i = 0;
(OrderQueue.GlobalQueue.HasUnprocessedOrders() ||
OrderQueue.GlobalQueue.HasInProcessingOrders()) && i < int.MaxValue; i++)
{
if (i % DOT_COUNT == 0)
{
Console.Clear();
Console.Write("正在處理訂單 ");
}
else
{
Console.Write(".");
}
Thread.Sleep(500);
}
}
剩餘程式碼如下:
static List<Order> CreateOrders()
{
return new List<Order>
{
new Order { Id = "P001", Items = new List<OrderItem>
{
new OrderItem("I001", "液晶顯示器", 2000),
new OrderItem("I002", "Core I7 CPU", 3000),
new OrderItem("I003", "金士頓 DDR2 記憶體", 500)
}},
new Order { Id = "P002", Items = new List<OrderItem>
{
new OrderItem("I004", "土豪顯示器", 5000),
new OrderItem("I005", "土豪 CPU", 6000),
new OrderItem("I006", "火星人記憶體", 1000)
}},
new Order { Id = "X003", Items = new List<OrderItem>
{
new OrderItem("I007", "CRT 顯示器", 300),
new OrderItem("I008", "奔三 CPU", 1000),
new OrderItem("I009", "雜牌記憶體", 200)
}},
new Order { Id = "P004", Items = new List<OrderItem>
{
new OrderItem("I001", "液晶顯示器", 2000),
new OrderItem("I002", "Core I7 CPU", 3000),
new OrderItem("I003", "金士頓 DDR2 記憶體", 500)
}},
new Order { Id = "V005", Items = new List<OrderItem>
{
new OrderItem("I004", "土豪顯示器", 5000),
new OrderItem("I005", "土豪 CPU", 6000),
new OrderItem("I006", "火星人記憶體", 1000)
}},
};
}
static void OutputOrdersProcessResult(OrderQueue orderQueue)
{
Console.WriteLine();
foreach (var message in orderQueue.ProcessMessages)
{
Console.WriteLine(String.Format("訂單 {0} 單次處理結束,{1}{2}.",
message.MessageId, GetOrderStateString(message.MessageState),
message.MessageException == null ? "" : ("," + message.MessageException)));
}
}
static string GetOrderStateString(OrderState state)
{
switch (state)
{
case OrderState.Unprocessed: return "未處理";
case OrderState.InProcessing: return "正在處理";
case OrderState.Submitted: return "入庫成功";
case OrderState.Invalid: return "入庫失敗";
case OrderState.NeedTryAgain: return "需要再次嘗試";
default: return "";
}
}
相關文章
- 關於SSM框架的一個簡單DemoSSM框架
- 關於dva框架的簡單操作以及demo框架
- Spring框架中的設計模式(二)Spring框架設計模式
- 關於.Net中屬性的使用探討(二) (轉)
- .NET框架設計(常被忽視的框架設計技巧)框架
- 關於網路框架設計封裝的扯淡框架封裝
- .NET框架設計(1)框架
- 一個程式設計師的讀書筆記——關於程式設計的反思程式設計師筆記
- 關於 Angular 程式設計中的 shim 概念Angular程式設計
- 關於考勤模組中設計的問題
- 關於Petstore中WAF框架框架
- 介面設計中,關於字型設計的10 個錦囊
- 關於VC中的DLL的程式設計 (轉)程式設計
- 關於Sqlite的一個demoSQLite
- 新書出版《.NET框架設計—模式、配置、工具》感恩回饋社群!新書框架模式
- 《.NET 安全程式設計》 讀書筆記(一、二、三)程式設計筆記
- 程式設計師必看的書(二)程式設計師
- 設計、故事、運營、機制,關於遊戲的13本書遊戲
- 關於怎樣寫程式設計入門書的問答程式設計
- 程式設計中的那些套路——關於策略模式程式設計模式
- 關於將Jdon框架提升為DCI框架的設想框架
- Attribute 在.NET程式設計中的應用(二) (轉)程式設計
- 關於設計模式的設計模式
- 大家看看我設計的這個關於圖書館借書還書的模型屬於貧血還是充血?模型
- .NET框架設計(常被忽視的C#設計技巧)框架C#
- 一個基於Android的MVP框架DemoAndroidMVP框架
- 併發程式設計:DEMO:比較Stream和forkjoin框架的效率程式設計框架
- 關於.NET中的Server push技術Server
- 個人成長中,關於規劃設計的思考
- 關於 rxjs 程式設計中的 take(1) 操作JS程式設計
- 關於介面測試——自動化框架的設計與實現框架
- 關於JF框架中的空值處理框架
- 關於Spark中RDD的設計的一些分析Spark
- 讀書Oracle9i&10g程式設計藝術-Oracle中關於鎖的理解(1)Oracle程式設計
- 求助!!關於學習JAVA網路程式設計的方法和書籍!!Java程式設計
- 關於設計模式設計模式
- 物件導向設計的設計模式(二):結構型模式(附 Demo & UML類圖)物件設計模式
- 關於自動化平臺的動態選單設計(二)