網友 itplayer 在上一篇文章的留言中提到了洋蔥架構,我後來爬了一些文,發現其理念蠻有意思,而且跟自己上一篇文章裡面提到的作法有那麼一絲絲雷同處--這顯然是往自己臉上貼金。於是,將爬文所見整理一下,包括 Domain-Drive Design 多層式架構、六角架構、洋蔥架構等,作為此入門系列的一個概念總整理。上一版的範例程式也稍有修改,使其層次與模組的命名比較接近洋蔥架構。謬誤之處,還請各方不吝指正。

傳統的多層架構

傳統的多層(三層)架構類似堆疊,最底層是資料層,其上為領域層(和服務層、應用程式層等),最頂端是展現層,然後還有各層都會用到的基礎建設(infrastructure),如下圖所示。


以變動幅度來看,這種架構有點像蓋房子--底層地基的變動通常比較少,然後是房子本身,然後頂部加蓋和和房子外觀裝飾的部分允許最多變動的彈性。

這樣的比喻有些不符實情。在真實世界中,有誰會在房子蓋好之後還改地基的呢?很少吧。可是在軟體開發的世界裡卻很常見。自從有這了這點覺悟,我已經不再拿蓋房子來比喻軟體開發。

離題了。回到主題,這種堆疊架構有什麼缺點呢?

The birthday greetings kata 文章中提到,傳統的三層(layers)架構有下列缺點:
  1. It assumes that an application communicates with only two external systems, the user (through the user interface), and the database. Real applications often have more external systems to deal with than that; for instance, input could come from a messaging queue; data could come from more than one database and the file system. Other systems could be involved, such as a credit card payment service.
    譯:傳統分層架構假設應用程式只會跟兩種外部系統溝通,即使用者(透過 UI)和資料庫。在真實世界裡,應用程式通常要應付更多外部系統,比如說,輸入可能來自訊息佇列、資料可能來自一個以上的資料庫和檔案系統。此外,還可能與其他系統銜接,例如信用卡付款服務。
  2. It links domain code to the persistence layer in a way that makes external APIs pollute domain logic. References to JDBC, SQL or object-relational mapping frameworks APIs creep into the domain logic.
    譯:其領域層連結至儲存層的方式會導致外部 API 汙染領域邏輯。對 JDBC、SQL、或 ORM 框架等 API 的參考會悄悄溜進領域邏輯。
  3. It makes it difficult to test domain logic without involving the database; that is, it makes it difficult to write unit tests for the domain logic, which is where unit tests should be more useful.
    譯:一旦少了資料庫,就很難測試領域邏輯。也就是說,領域邏輯的單元測試會很不好寫,然而這個部分卻是單元測試應該能發揮其功效的地方。

大致了解傳統三層式架構的缺點之後,接著來看幾種改良的架構。

Domain-Driven Design 多層架構

在 Domain-Driven Design N-Layerd .NET 4.0 Architecture Guide 裡面所描繪的 DDD 多層架構,應用程式服務層和資料存取層都會用到領域層(商業邏輯層),如下圖所示:

DDD N-layer 架構

相較於傳統三層式架構,在領域層這邊有個微妙而關鍵的變化:領域層已經不依賴資料存取層,而是反過來由資料存取層依賴領域層。這點其實跟六角架構和洋蔥架構的精神一致。

接著就來看一下什麼是六角架構和洋蔥架構。

六角架構

六角架構(Hexagonal Architecture)又叫做 Ports and Adapters 模式,是由 Alistair Cockburn 於 2005 年提出。Duncan Nisbet 用六角形的桌子來比喻六角架構:
  • 所有的 domain objects 都在桌上。
  • 環繞桌子周圍的椅子就是 adapters。
  • 站在椅背後面的人等同於外部系統或服務。

桌子的結構基本上不會變動(領域物件不變);會變動的部分是跟外界銜接的 adapters。

椅子的規格必須符合桌子的 port,才能卡進去--adapters 必須符合 domain objects 的 port,彼此才能銜接得上。下圖取自 Duncan Nisbet 部落格


內六角是領域邏輯,外六角是 adapters,作為內六角與外部系統銜接的橋樑。

六角架構的一個重點是,domain model 並不依賴任何 layer(例如資料存取層),而是所有的 layers 都依賴 domain model。像這樣(取自 http://matteo.vaccari.name/blog/archives/154):

+-----+-------------+----------+
| gui | file system | database |
|-----+-------------+----------+
|          domain              |
|------------------------------+

下圖是一個設計範例,取自 Kamil Dworakowski 部落格



洋蔥架構

洋蔥架構(Onion Architecture)是由 Jeffrey Palermo 於 2008 年提出,如下圖所示:

洋蔥架構圖(取自 Epic 網站:The bellis perennis

其主要精神為:
  1. 應用程式係圍繞著一個獨立的物件模型來建構。
  2. 內層定義介面,外層實作介面。
  3. 耦合的方向是朝向中央。
  4. 應用程式的所有核心程式碼可以在與基礎建設分離的情況下正常運行。(按:實務上有人真的試過把基礎建設抽掉,用其他 mock 物件或 null objects 取代嗎?有此經驗的朋友煩請舉個手,更希望能分享一些心得。善哉!)
摘錄自Palermo 的系列文章的幾個重點:
  • 此架構比較適用於大型的、壽命較長的複雜系統,不適合小型網站。
  • 強調針對介面來寫程式,以及把基礎建設(infrastructure)抽離至外部層次。
  • 傳統上,基礎建設往往橫跨各層,以至於增加了許多無謂的耦合。
  • 此架構的一個倍受爭議處,是 UI 和 商業邏輯層都會相依於資料存取層。對,UI 也會依賴資料存取層。遞移相依仍然是一種相依。
  • 此架構的概念並非全是創新,只是原創者賦予了一個更正式的名字,且更詳細的闡述其架構模式,方便開發人員溝通。
  • 資料庫不是核心,而是外部元件。
  • 領域模型才是核心。
  • 大量使用 Dependency Injection 技巧。

哪些要放在核心、哪些要放在基礎建設?

很明顯的,領域模型要放在核心。可是,有些 cross cutting 的東西怎麼辦呢?這問題肯定會在設計架構時碰到。

關於這個問題,Palermo 本人在回答網友留言時曾這麼說:
When deciding what to add to the core, you are making a judgement call about stability. By referencing System.dll, you are making a bet that this will be stable enough in the future to allow upgrading without too much hassle. It's a pretty safe bet as long as .Net is around. If you add, or reference logging frameworks, security frameworks, or utility functions that in turn reference frameworks, you much make a judgement call about the stability of these as well. On a few projects, I have accepted Log4Net into the core because of its history of stability and small surface area. Data access libraries are a terrible bet in the core because they have a long-standing track record of being very unstable over time with new approaches coming out every 18 months. In face, just as Entity Framework comes on the scene, the NoSQL movement is currently jumping the chasm.
喔,他曾在某些專案中把 Log4Net 放在 core 裡面耶!你看,在決定哪些元件該放到 infrastructure,哪些該放到 core 時,連 Palermo 都跟我一樣很隨便彈性啊。因為這主要是取決於你對各相依元件的「穩定性」的看法。

我想作者的意思大概是這樣:跟你的應用程式的壽命來比較,能活得比它還久的元件就視為相對穩定,可納入 core 或被 core 參考。比應用程式還短命的元件當然就不夠穩定,該放到 infrastructure 裡面。舉例來說,我們的應用程式可能要 run 個十年,可是我們目前使用的資料存取技術或網路服務 API 可能三年之後就變了,那麼這些相對短命的技術或 API 就該放到 infrastructure 裡面。
寫到這裡,我想起先前的<Dependency Injection 筆記 (6)>裡面也有討論到元件的穩定性,於是自己又再複習了一下。

實作範例

如果你對洋蔥架構有興趣,這裡有幾份實作範例可供參考學習:

這幾個範例應該都比我在這裡提供的好多啦!

不過,本系列的範例程式,由於進展得非常慢,各版本的變化不是太劇烈,再加上有討論到一些細節,我想對於初次嘗試撰寫這類架構的朋友應該有點參考價值。至少我是這麼希望啦!

修改上一個版本的範例程式

由於先前版本的 Domain Model 已經是由各組件共用,這個部分已經有點接近洋蔥架構了,所以這次修改的幅度不大(我也不想改太多),主要是:
  • 命名空間的改變:Northwind.Core.Domain 和 Northwind.Core.BusinessLayer。
  • Repository 介面(IOrderRepository)改放到 NorthwindApp.Core.Domain.Interfaces 命名空間,其實作類別則放在 NorthwindApp.DataAccess。

修改之後的組件相依圖:



現在 NorthwindApp.Service 組件也會參考 NorthwindApp.DataAccess(先前版本沒有),主要是因為我在 NorthwindApp.Service 裡面處理物件組合的工作時,會需要用到 Repository 類別。參考底下的程式片段:

public class OrderService
{
    private OrderManager orderManager;

    public OrderService()
    {
        IOrderRepository orderRepository = new OrderRepository();
        orderManager = new OrderManager(orderRepository);
    }
    //....(略)
}

這裡使用了 Constructor Injection 技巧,來降低 OrderManager 與 OrderRepository 的耦合。當然,我們也可以用 IoC container 來解決組件層級的相依問題,讓 NorthwindApp.Service 不要直接參考 NorthwindApp.DataAccess。未來若打算加入 IoC container,可以加入一個組件,用來專門負責處理型別解析,例如 NorthwindApp.DependencyResolution.dll。

下面這張圖是展開後的組件相依關係,從這裡可以大致看出幾個關鍵類別和介面被劃分到哪些命名空間。



在組件名稱上,我並沒有像其他洋蔥架構範例程式那樣使用 *.Infrastructure.* 來命名,主要是覺得名稱過於冗長,而且懶得改太多。

另外可以注意的地方是 NorthwindApp.Core.Domain.dll 組件中有兩個子命名空間:*.Model 和 *.Interfaces。這個 *.Interfaces 命名空間,將來若有需要,可以考慮獨立出去,自成一個組件,作為其他「外圍」組件與核心 Domain Model 之間溝通的橋樑。顯然,到時候免不了要使用 dependency injection 技巧或 IoC container 來處理執行時期的型別解析(小心濫用 IoC container 反而變成了 anti-pattern)。

小結

本文簡介的三種架構,儘管名稱和實作細節有些差異,但無論是堆疊狀的 DDD 多層架構還是六角形、洋蔥、甚至向日葵架構,其基本精神並無二致--它們骨子裡都是 domain-driven、domain-centric、domain-oriented....whatever you call it。

Happy coding :)

延伸閱讀