作者試了一年半的 Akka actor model,結果他的團隊寫了一整團亂七八糟的 code (尤其是新人寫的)。他講述 actor model 的缺點 - actor 在互動時,都是靠傳遞訊息 (loosely couple),如果這一連串的訊息有因果關系 (就是一串對話),在 Actor 內就得用個 Map 紀錄這段歷程,直到對話結束。
本來如果直接呼叫 function 的話,歷程就是在參數與回傳值間交換而已,用了 Actor Model,就得暫存在 Map,花精神額外管理。而他們的程式碼就大部份都在維護這類髒東西,測試也都花很多精力維持 Actor 對話的正確流程。
作者採用 functional programming 後,團隊的開發品質的確有助益,但是採用 Actor Model 後,卻沒有得到類似的結果,只留下一堆垃圾。
我自己的經驗是,能夠直接呼叫 function,同步的得到結果,就這樣寫程式吧。程式界有很多奇特的偏門,Actor Model 就是其中之一,這些偏門實在不是每天寫的程式該遇到的,尤其是一個十幾二十人的專案,還要跑個三、五年以上。Actor Model 的存在可以說是為了解決特殊的問題 -- 它適合 high concurrency、以及分散式的系統。這些系統上套用 Actor 的模型就很適合。如果是每天寫的 CRUD,或是一些演算,用 Actor 只是在綁自己而已。
不熟 Akka, 想問一下GO 是否也會有類似的問題? 不太能體會他說的 Map 指得是什麼?
另外他說的function, 其實是 Future, GO要做 Future, 其實也是得 goroutine + channel .
Akka 提供一個 Actor 模型,如果你的程式架構都依賴它,那麼整個軟體寫起來會整個偏向 Actor。但是 golang 的比較像 library,想用的地方就用,沒需要的地方還是可以照正常的寫法。
它說的 function 是 async 的,自然是回傳 Future。如果直接串的話,就是在 future callback 裡寫接下來的邏輯:
class FooActor {
void onMessage(msg) {
//foo 送訊息給 bar,而且要求回傳
Future future = barActor.ask("hello")
future.onSuccess(result-> {
// 對方回給你它的名字,處理一些邏輯
String name = result;
//現在這個 callback 裡的 thread 是誰??
});
}
}
上面的範例,兩個 actor 交換資料,fooActor 靠 future 得到了對方的名字,但其實這個已經違反 Actor model 的精神,因為兩個 actor tightly couple,而且 callback 的 thread 是誰的也是很困擾的。Actor 就是想要遠離 thread 的操作,才特別設計的模型。
一個正統的 Actor 會這樣改寫:
class FooActor {
void onMessage(msg) {
if (msg instanceOf StartHello) {
//tell(), 射後不理
barActor.tell("hello")
} else if (msg instanceOf HelloName) {
String name = ((HelloName)msg).name;
//do something about name
}
}
}
這樣設計的話,FooActor 就只會對 HelloName
這樣的訊息做反應,它不理會這個訊息是不是來自 barActor,而且 thread 這次也是對的,Actor 模型會保證 thread safety。
好啦,如果現在有另一個 bazActor ,它可以提供 email address,讓 fooActor 最終完成寄信的需求 (收集到 name 和 email 後,就寄信)
class FooActor {
Map storage = new HashMap();
void onMessage(msg) {
if (msg instanceOf StartHello) {
barActor.tell("hello")
} else if (msg instanceOf HelloName) {
String name = ((HelloName)msg).name;
storage.put("name", name);
} else if (msg instanceOf StartEmail) {
bazActor.tell("email")
} else if (msg instanceOf EmailAddress) {
String address = ((EmailAddress)msg).address;
String name = storage.get("name");
sendEmail(name, address, "subject...");
}
}
}
好啦,上面就是 FooActor 和 Bar/BazActor 的一連串對話,等三方對話完後,FooActor 才有辦法寄信,中間過程中只能用 Map 暫存 name
或 email
。看到這就知道這程式夠難維護吧!這可是照著 Actor 模型的精神去寫的,actor 間完全的 decouple。
如果用 Future 一直串就沒這問題了,但這就是一般的傳統的 async 操作。如果大部份的邏輯都能這樣做,那導入 Actor 架構的意義何在?這就是作者導入一年半後,對於 Actor 模型的存疑。
我沒有 golang 的經驗,不過如果有人硬把所有的 function 呼叫通通改用 channel 互丟,那其實也會造成類似的結果吧。
結論就是架構軟體前要思考一下,慎重考慮導入全新的模型是否值得 ,不要因為技術紅 (比方說 ReactJs),或者是想玩新玩意 (比方說 microservice) 就隨便引入。當然很多事做了才知道啦,在新舊技術間不斷地試誤,就是我們開發者的日常...