順暢的使用C# Actor:另一個解決方案
在前兩篇文章中,我們了解到Erlang中靈活的模式匹配,以及在C#甚至F#中會都遭遇的尷尬局面。那么現(xiàn)在就應(yīng)該來設(shè)計一個解決方案了,我們?nèi)绾尾拍茉贑#這樣的語言里順暢地使用Actor模型呢?不僅如此,***我們還能獲得其它一些優(yōu)勢。
C# Actor:“消息”、“協(xié)議”和“接口”
Actor模型中的對象如果要進行交互,唯一的手段便是發(fā)送消息。不同語言/平臺上的消息有不同的表現(xiàn)形式,但是它們所傳遞的信息是一致的:
◆做什么事情
◆做這件事情需要的數(shù)據(jù)
例如,Erlang中往往會使用Tag Message的格式作為消息:
- {doSomething, Arg1, Arg2, Arg3, ...}
其中,原子doSomthing表示“做什么”,而后面的ArgN便是一個個的參數(shù),使用Erlang中的模式匹配可以很方便地捕獲消息中的數(shù)據(jù)。在C#等語言中,由于并非專為了Actor模型設(shè)計,因此一個Message往往只能是一個對象。但是這個對象的職責(zé)并沒有減輕,因此我們需要自己處理的事情就多了。我們可能會這樣做:
學(xué)Erlang的Tag Message,但是這樣會產(chǎn)生大量丑陋的類型轉(zhuǎn)換操作,并且喪失了靜態(tài)檢查功能。
為每種消息創(chuàng)建不同的Message類型,但是這樣會產(chǎn)生大量類類型,每個類型又有各種屬性,非常麻煩。
這兩種做法在上一篇文章里都有過討論,感興趣的朋友可以再去“回味”一番。那么,究竟什么是消息呢?根據(jù)我的理解,“消息”其實是這么一種東西:
◆“消息”表示“發(fā)送方”和“接受方”之間的“通信協(xié)議”(例如Erlang中的“模式”)。
◆“消息”表示“發(fā)送方”要“接受方”所做的事情,但是并沒有要求“接受方”需要怎么做。
◆一個Actor可能會會作為“接受方”遵守多種“通信協(xié)議”。
經(jīng)過這樣的描述,您是否覺得.NET中有一種東西和“消息”非常接近?沒錯,那就是“接口”,因為:
◆“接口”從概念上講便是一種“協(xié)議”。
◆“接口”表示“能做什么”,但沒有限制“怎么做”。
◆一個Actor可以實現(xiàn)多個接口,即遵守多種協(xié)議。
看上去還真是一一對應(yīng)啊!那么我們再來深入一步進行對比,“接口”能否傳遞消息所要表現(xiàn)的信息?答案也是肯定的:
◆做什么事情:接口中的一個方法。
◆需要的數(shù)據(jù):接口的參數(shù)。
也就是說,如之前的那條Erlang消息,在C#中便可以表示為:
- x.DoSomething(arg1, arg2, arg3, ...)
基于這樣的類比,我們發(fā)現(xiàn)使用“接口”還可以帶來一個額外的東西,那就是“消息組”。如Erlang這樣語言,消息與消息之間是完全獨立的。.NET中的接口可以包含多個方法,這就是一種“分組”,我們可以利用這種方式來更好地管理有關(guān)聯(lián)的消息。此外,利用.NET中的訪問限制符(public,internal等)還可以實現(xiàn)消息的公開和隱藏。而且因為接口的參數(shù)是強類型的,所以可以得到編譯期的檢查,也可以享受編輯工具的代碼提示及重構(gòu)……C#編程里的種種優(yōu)勢似乎我們一個都沒有拉下。
C# Actor看似美好的實現(xiàn)
等一下,接口只是一種“協(xié)議”,但是“消息”還必須是一個實體,一個對象,并且“攜帶”了這個協(xié)議才能在Actor之間傳遞埃這個對象除了攜帶協(xié)議所需要的數(shù)據(jù)以外,還要能夠告訴接受方究竟該“操作什么”?!安僮鳌睅稀皵?shù)據(jù)”,于是我就想到了“委托”。例如,如果我們想要發(fā)送一個“協(xié)議”,叫做IDoHandler,那么我們便可以構(gòu)造一個Action﹤IDoHandler>對象——這正是Lambda表達式的用武之地:
- Action﹤IDoHandler> m = x => x.Do(0, 1, 2, ...);
好,那么我們還是用乒乓測試來嘗試一番。我們知道,乒乓測試會讓Ping對象和Pong對象相互發(fā)送消息,我們各使用一個“消息組”,也就是“接口”來定義消息:
- public interface IPongMessageHandler { }
- public interface IPingMessageHandler { }
那么,Ping和Pong兩個Actor類型又該如何定義呢?我們知道,Ping需要處理Pong發(fā)來的消息,因此它需要實現(xiàn)IPongMessageHandler接口,并且需要接受類型為Action﹤IPongMessageHandler>的消息。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執(zhí)行即可。嚴(yán)格說來,“接口”只是一個“消息組”,具體的“消息”還是要落實到接口中的方法。定義了Ping和Pong之后,我們便可以明確接口中的方法了(確切地說,是明確了方法的參數(shù)):
- public interface IPongMessageHandler
- {
- void Pong(Pong pong);
- }
- public interface IPingMessageHandler
- {
- void Ping(Ping ping);
- void Finish();
- }
使用了接口,自然就要提供方法的實現(xiàn)了。我們先從典型而簡單的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
- }
原本需要在得到消息之后,根據(jù)消息的內(nèi)容作出不同的響應(yīng)。而現(xiàn)在,消息會被自動轉(zhuǎn)發(fā)為接口中的方法調(diào)用,我們只需要實現(xiàn)特定的方法即可。在Ping方法中,我們會得到一個Ping類型的對象——于是我們再向它回復(fù)一個消息。消息的類型是Action﹤IPongMessageHandler>,可以看出,使用Lambda表達式構(gòu)造這樣一個消息特別方便。
Ping類也只需要實現(xiàn)IPongMessageHandler即可,只是這段邏輯“略顯復(fù)雜”:
- 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,則回復(fù)一個Ping消息,否則就回復(fù)一個Finish并退出。***啟動乒乓測試:
new Ping(5).Start(new Pong());由于使用了接口作為消息的協(xié)議,因此無論是編輯器還是編譯器都可以給我們足夠的支持。同時,對于消息的處理也無須如上一篇文章那樣不斷進行判斷和類型轉(zhuǎn)換,代碼可謂流暢不少。
C# Actor致命的缺陷
雖說沒有***的東西,但目前的缺陷卻是致命的。
在實際使用過程中,消息的“發(fā)送方”和消息的“接收方”應(yīng)該完全無關(guān),它們互不知道對方具體是誰,只應(yīng)該基于“協(xié)議”,也就是“接口”來實現(xiàn)??上г谏厦孢@段代碼中,很多東西都被“強橫”地限制住了。例如,Ping消息會附帶一個ping對象作為參數(shù),ping對象會等待一個Pong消息。但是,發(fā)送Ping消息(并等待Pong消息)的一方很可能是各種類型的Actor,不一定是Ping類型。有朋友可能會說,那么我們把IPingMessageHandler的Ping方法的簽名改成這樣,不就可以了嗎?
- void Ping(Actor﹤Action﹤IPongMessageHandler>> ping)
是的,此時的ping,的確是在“等待Pong消息的Actor對象”。但是,這意味著ping對象它也只能是這個指明的Actor類型了。在實際使用過程中,這幾乎是不可能的事情。因為一個Actor很可能會接受各種消息,它很難做到“一心一意”。因此這篇文章所提出的做法,幾乎只能滿足如乒乓測試這樣簡單的Actor模型使用場景。我們必須改變。
改變的方式有不少,從“向弱類型妥協(xié)”到“利用.NET 4.0中的協(xié)變/逆變”,都可以滿足不同的場景——不過我們還是下次再說吧。
F#的實現(xiàn)
本文描述的方式也可以運用在在F#中。首先自然還是接口的定義:
- type IPingMessageHandler =
- abstract Ping : Ping -> unit
- abstract Finish : unit -> unit
- and IPongMessageHandler =
- abstract Pong : Pong -> unit
以上便是F#中定義接口的方式,與C#相比更為簡潔。接著便是Ping類型的實現(xiàn):
- 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 ﹤- count - 1
- if (count > 0) then
- pong ﹤﹤ fun h -> self |> h.Ping
- else
- pong ﹤﹤ fun h -> h.Finish()
- self.Exit()Pong類型的實現(xiàn)則更為簡單:
- 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;
【編輯推薦】