適合C# Actor的訊息執行方式(3):中看不中用的解決方案

iDotNetSpace發表於2009-07-20

 在前兩篇文章中,我們瞭解到Erlang中靈活的模式匹配,以及在C#甚至F#中會都遭遇的尷尬局面。那麼現在就應該來設計一個解決方案了,我們如何才能在C#這樣的語言裡順暢地使用Actor模型呢?不僅如此,最好我們還能獲得其它一些優勢。

“訊息”、“協議”和“介面”

  Actor模型中的物件如果要進行互動,唯一的手段便是傳送訊息。不同語言/平臺上的訊息有不同的表現形式,但是它們所傳遞的資訊是一致的:

  1. 做什麼事情
  2. 做這件事情需要的資料

  例如,Erlang中往往會使用Tag Message的格式作為訊息:

{doSomething, Arg1, Arg2, Arg3, ...}

  其中,原子doSomthing表示“做什麼”,而後面的ArgN便是一個個的引數,使用Erlang中的模式匹配可以很方便地捕獲訊息中的資料。在C#等語言中,由於並非專為了Actor模型設計,因此一個Message往往只能是一個物件。但是這個物件的職責並沒有減輕,因此我們需要自己處理的事情就多了。我們可能會這樣做:

  • 學Erlang的Tag Message,但是這樣會產生大量醜陋的型別轉換操作,並且喪失了靜態檢查功能。
  • 為每種訊息建立不同的Message型別,但是這樣會產生大量類型別,每個型別又有各種屬性,非常麻煩。

  這兩種做法在上一篇文章裡都有過討論,感興趣的朋友可以再去“回味”一番。那麼,究竟什麼是訊息呢?根據我的理解,“訊息”其實是這麼一種東西:

  1. “訊息”表示“傳送方”和“接受方”之間的“通訊協議”(例如Erlang中的“模式”)。
  2. “訊息”表示“傳送方”要“接受方”所做的事情,但是並沒有要求“接受方”需要怎麼做。
  3. 一個Actor可能會會作為“接受方”遵守多種“通訊協議”。

  經過這樣的描述,您是否覺得.NET中有一種東西和“訊息”非常接近?沒錯,那就是“介面”,因為:

  1. “介面”從概念上講便是一種“協議”。
  2. “介面”表示“能做什麼”,但沒有限制“怎麼做”。
  3. 一個Actor可以實現多個介面,即遵守多種協議。

  看上去還真是一一對應啊!那麼我們再來深入一步進行對比,“介面”能否傳遞訊息所要表現的資訊?答案也是肯定的:

  1. 做什麼事情:介面中的一個方法。
  2. 需要的資料:介面的引數。

  也就是說,如之前的那條Erlang訊息,在C#中便可以表示為:

x.DoSomething(arg1, arg2, arg3, ...)

  基於這樣的類比,我們發現使用“介面”還可以帶來一個額外的東西,那就是“訊息組”。如Erlang這樣語言,訊息與訊息之間是完全獨立的。.NET中的介面可以包含多個方法,這就是一種“分組”,我們可以利用這種方式來更好地管理有關聯的訊息。此外,利用.NET中的訪問限制符(public,internal等)還可以實現訊息的公開和隱藏。而且因為介面的引數是強型別的,所以可以得到編譯期的檢查,也可以享受編輯工具的程式碼提示及重構……C#程式設計裡的種種優勢似乎我們一個都沒有拉下。

看似美好的實現

  等一下,介面只是一種“協議”,但是“訊息”還必須是一個實體,一個物件,並且“攜帶”了這個協議才能在Actor之間傳遞啊。這個物件除了攜帶協議所需要的資料以外,還要能夠告訴接受方究竟該“操作什麼”。“操作”帶上“資料”,於是我就想到了“委託”。例如,如果我們想要傳送一個“協議”,叫做IDoHandler,那麼我們便可以構造一個Action物件——這正是Lambda表示式的用武之地:

Action<IDoHandler> m = x => x.Do(0, 1, 2, ...);

  好,那麼我們還是用乒乓測試來嘗試一番。我們知道,乒乓測試會讓Ping物件和Pong物件相互傳送訊息,我們各使用一個“訊息組”,也就是“介面”來定義訊息:

public interface IPongMessageHandler { }

public interface IPingMessageHandler { }

  那麼,Ping和Pong兩個Actor型別又該如何定義呢?我們知道,Ping需要處理Pong發來的訊息,因此它需要實現IPongMessageHandler介面,並且需要接受型別為Action的訊息。Pong與Ping類似,因此它們的定義為:

public class Ping : Actor<Action<IPongMessageHandler>>, IPongMessageHandler
{
    private int m_count;

    public Ping(int count)
    {
        this.m_count = count;
    }

    protected override void Receive(Action<IPongMessageHandler> message)
    {
        message(this);
    }

    ...
}

public class Pong : Actor<Action<IPingMessageHandler>>, IPingMessageHandler
{
    protected override void Receive(Action<IPingMessageHandler> message)
    {
        message(this);
    }

    ...
}

  從程式碼上看,實際操作中我們並不需要讓Ping或Pong直接繼承Handler介面,只要最終提供一個物件給message執行即可。嚴格說來,“介面”只是一個“訊息組”,具體的“訊息”還是要落實到介面中的方法。定義了Ping和Pong之後,我們便可以明確介面中的方法了(確切地說,是明確了方法的引數):

public interface IPongMessageHandler
{
    void Pong(Pong pong);
}

public interface IPingMessageHandler
{
    void Ping(Ping ping);
    void Finish();
}

  使用了介面,自然就要提供方法的實現了。我們先從典型而簡單的Pong物件看起:

public class Pong : Actor<Action<IPingMessageHandler>>, IPingMessageHandler
{
    ...

    #region IPingMessageHandler Members

    void IPingMessageHandler.Ping(Ping ping)
    {
        Console.WriteLine("Pong received ping");
        ping.Post(h => h.Pong(this));
    }

    void IPingMessageHandler.Finish()
    {
        Console.WriteLine("Finished");
        this.Exit();
    }

    #endregion
}

  原本需要在得到訊息之後,根據訊息的內容作出不同的響應。而現在,訊息會被自動轉發為介面中的方法呼叫,我們只需要實現特定的方法即可。在Ping方法中,我們會得到一個Ping型別的物件——於是我們再向它回覆一個訊息。訊息的型別是Action,可以看出,使用Lambda表示式構造這樣一個訊息特別方便。

  Ping類也只需要實現IPongMessageHandler即可,只是這段邏輯“略顯複雜”:

public class Ping : Actor<Action<IPongMessageHandler>>, IPongMessageHandler
{
    ...

    public void Start(Pong pong)
    {
        pong.Post(h => h.Ping(this));
    }

    #region IPongMessageHandler Members

    void IPongMessageHandler.Pong(Pong pong)
    {
        Console.WriteLine("Ping received pong");

        if (--this.m_count > 0)
        {
            pong.Post(h => h.Ping(this));
        }
        else
        {
            pong.Post(h => h.Finish());
            this.Exit();
        }
    }

    #endregion
}

  收到Pong訊息之後,將count減1,如果還大於0,則回覆一個Ping訊息,否則就回復一個Finish並退出。最後啟動乒乓測試:

new Ping(5).Start(new Pong());

  由於使用了介面作為訊息的協議,因此無論是編輯器還是編譯器都可以給我們足夠的支援。同時,對於訊息的處理也無須如上一篇文章那樣不斷進行判斷和型別轉換,程式碼可謂流暢不少。

致命的缺陷

  雖說沒有完美的東西,但目前的缺陷卻是致命的。

  在實際使用過程中,訊息的“傳送方”和訊息的“接收方”應該完全無關,它們互不知道對方具體是誰,只應該基於“協議”,也就是“介面”來實現。可惜在上面這段程式碼中,很多東西都被“強橫”地限制住了。例如,Ping訊息會附帶一個ping物件作為引數,ping物件會等待一個Pong訊息。但是,傳送Ping訊息(並等待Pong訊息)的一方很可能是各種型別的Actor,不一定是Ping型別。有朋友可能會說,那麼我們把IPingMessageHandler的Ping方法的簽名改成這樣,不就可以了嗎?

void Ping(Actor<Action<IPongMessageHandler>> ping)

  是的,此時的ping,的確是在“等待Pong訊息的Actor物件”。但是,這意味著ping物件它也只能是這個指明的Actor型別了。在實際使用過程中,這幾乎是不可能的事情。因為一個Actor很可能會接受各種訊息,它很難做到“一心一意”。因此這篇文章所提出的做法,幾乎只能滿足如乒乓測試這樣簡單的Actor模型使用場景。我們必須改變。

  改變的方式有不少,從“向弱型別妥協”到“利用.NET 4.0中的協變/逆變”,都可以滿足不同的場景——不過我們還是下次再說吧。

F#的實現

  本文描述的方式也可以運用在在F#中。首先自然還是介面的定義:

type IPingMessageHandler =
    abstract Ping : Ping -> unit
    abstract Finish : unit -> unit

and IPongMessageHandler = 
    abstract Pong : Pong -> unit

  以上便是F#中定義介面的方式,與C#相比更為簡潔。接著便是Ping型別的實現:

and Ping() =
    inherit (IPongMessageHandler -> unit) Actor()
    let mutable count = 5
    override self.Receive(message) = message self

    member self.Start(pong : Pong) = 
        pong << fun h -> self |> h.Ping
    
    interface IPongMessageHandler with 
        member self.Pong(pong) =
            printfn "Ping received pong"
            count if 
(count > 0) then pong << fun h -> self |> h.Ping else pong << fun h -> h.Finish() self.Exit()

  Pong型別的實現則更為簡單:

and Pong() =
    inherit (IPingMessageHandler -> unit) Actor()
    override self.Receive(message) = message self
    
    interface IPingMessageHandler with
        member self.Ping(ping) =
            printfn "Pong received ping"
            ping << fun h -> self |> h.Pong
        
        member self.Finish() =
            printfn "Finished"
            self.Exit()

  啟動乒乓測試:

(new Pong()) |> (new Ping()).Start;

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/12639172/viewspace-609712/,如需轉載,請註明出處,否則將追究法律責任。

相關文章