上一篇閒談軟體設計:友善的距離意外在 Facebook 上引起不少人的回應 (是的,我不但偷偷加了副標,還擴充了些內容),不過首先要澄清一下,接下來一系列文章 (如果能堅持住繼續寫的話),我不會討論哪個架構比較好,情境不同,本來對架構的選擇就不同,架構師本身也有所謂的偏好,所以討論哪種架構比較好,這種吃力不討好的筆戰,只是自找麻煩。因此,我只是把在一個抽象的架構中實作上可能會遇到的技術選擇或是設計選擇的種種因素放在文章中讓大家思考,嗯,通常我也不會寫答案,頂多是某些過去從經實驗的經驗分享。
在落落長的引言結束,還是回到本文來吧!如果覺得這篇文章的副標題有點エロ的人,應該都是被標題騙進來的,如果是被騙進來的,可以按上一頁離開,或繼續看完假裝自己不是XD,好啦,玩笑開完了。不管用哪種語言開發軟體,除非是那種一個 function 寫個幾萬行的人 (來人啊,把這種人拖去砍了),不然,一般都會根據某些因素,切割成模組或是特定功能的區塊 (一個 class 或是一個 function),但要完成一個特定功能,這些模組或區塊勢必要一起合作,因此這些模組與區塊就發生了關係。
以目前主流的語言來說,大多可以用物件的方式描述可被使用的區塊,UML 的 class diagram 將物件之間的關係分成兩大類:實體階層的關係與類別階層的關係,實體階層有 dependency、association、aggregation 和 composition 四種,類別階層有 generalization 和 realization (implementation) 兩種,共計六種,雖說是六種,但在不同語言哩,實現的方式也不逕相同,所以我也不打算談怎樣的實作才算是哪一種關係。
我們就單純討論該如何讓一個物件知道另一個物件?你在開玩笑嗎?這不是很簡單嗎?別看這這樣,Martin Fowler 可是寫了篇 《Inversion of Control Containers and the Dependency Injection pattern》文章描述幾種 dependency injection 的方法: constructor injection、setter injection 和 interface injection,以及用來查找物件的 service locator。不過既然 Martin Fowler 寫得這麼詳細了,我又有什麼好寫的呢?就三個:自己的習慣、 container-based annotated dependency injection 和 singleton,後面兩種是我覺得有意思的東西。
先看我自己的習慣吧!我通常將必要的物件關係使用 constructor injection,例如 A 物件需要 B 和 C 物件才能運作,便會在 constructor 宣告 B 和 C 物件的參數,在建立 A 物件時,就要傳入 B 與 C 物件,但 constructor injection 會有缺點,首先,當需要的物件關係越多,常會造成 constructor 出現 long parameter list 的 bad smell,不過這也好,提醒自己,代表著 A 物件似乎做太多事了,可能也出現 low cohesion 的問題;再者,是建立物件的先後順序會受限,甚至會出現建立 A 物件時需要物件 C,建立 C 物件可能會需要 D 物件,但建立 D 物件時需要 A 物件,此時就出現雞生蛋、蛋生雞的問題了,這時,就只能用 setter injection 解開這個迴圈了。
Setter injection 則用在預計執行期間會換掉的關係上,例如根據外部輸入的條件,更換不同的演算法,像是根據購買物品的種類或是數量,使用不同折扣計算的演算法。至於,interface injection 則只用過二次,一次是使用 plug-in 架構時,為了讓 plug-in 能取得可能需要的 host resource 物件,只要載入的 plug-in 有實作特定 interface,就替該 plug-in 注入所需的物件 (參閱閒談軟體設計:Plug-in)。至於另一次經驗,等以後有機會再提。
接著看 Container-based annotated dependency injection,有在用 Spring framework 或是使用 J2EE container 的開發者對於 @Autowired
或是 @Inject
等 annotation 應該很眼熟,自己在用 Spring framework 開發 Web service 時也用很多,像下面的程式,只要一個單純的 annotation,Spring framework 就會幫開發者注入合適的物件:
但要能讓 Spring framework 注入物件, UserManager
本身必須是個 Spring bean 物件,不論是用 @Bean
或是用 xml 讓 Spring framework 將 UserManager
建立為 bean 物件,這些關係的注入才會生效。就如同《Spring Boot in Action》書中所說的:
Like any framework, Spring does a lot for you, but it demands that you do a lot for it in return.
不過,就如前篇閒談軟體設計:友善的距離,我對框架都會保持友善的距離,因此,我很少會向上例那樣,直接將 @Autowired
這類框架專屬的 annotation 加到 domain 物件中,那該怎麼辦呢?一般來說 domain 物件我習慣放在有 -core
後綴詞修飾的 projet 中,真正提供服務或是與 Spring framework 整合的物件則放在 -ws
或 -spring
修飾的對應 project 中,例如下圖:
UserManager
在 acl-core
的 project 中,使用 Spring framework 提供 Web Service 服務的 UserManagerBean
則是在 acl-spring
的 project 中。
雖然,UserManagerBean
沒有任何特殊的 method,只是一個單純的繼承,有點多餘的感覺,卻也多了點距離。另外,也讓測試變得簡單一點,因為測試時,不需要 Spring framework 的介入,在以前還沒有 SSD 的時候,光是等待 Spring framework 啟動然後注入物件就要十幾秒,但一個單元測試 method 可能才執行 0.x 秒,即便有 SSD 將啟動時間縮短到數秒,整體來說,這個代價實在太高了,而且當測試越多,代價就跟著提高。所以測試 domain 物件的邏輯時,雖然 annotation 很方便,我還是喜歡回歸到單純的 constructor injection 或 setter injection。
最後,就是 singleton 了,我很刻意不使用的 design pattern,或是說,我通常只有在真的只允許一個物件實體的情況下才會使用,但若看網路上 iOS 或 Android 大量的範例程式碼,singleton 被大量被當成 service locator 使用,特別是 Activity
是一個生命週期完全被 Android 掌控的物件,開發者既無法自己建立 Activity
物件,也不知道該在什麼時候使用 setter injection 注入所需要的 domain object,或是想在不同的 Activity
間傳遞複雜的物件,又或是不想一路傳遞物件到較深的物件中,於是能以 static 方式或是從全域取得物件的 singleton 就被大量使用了,但我覺得這完全不是 singleton 原本的意圖。
在最近的專案中,我試著用 interface injection 的方式,搭配 Android 對 Activity
的 lifecycle callback,在有實作特定 interface 的 Activity
注入需要的物件,同樣地,Fragment
也能用這種方式注入物件,因此不需要依賴 singleton 作為 service locator。
以上,就是和大家分享的三個有趣的設計決策思考。雖然,目前規劃中的主題,大概還有四篇,如果平時還有想到有趣的東西會再加入,但如果有希望我分享和討論的主題,可以留言給我,若能幫上忙,有空就分享出來。