關於《.NET 框架設計》書中 Demo 的更正 (二)

Jeffrey Y.發表於2015-04-07

  首先,感謝作者在本書中 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 "";
    }
}

相關文章