DDD系統的傳統分層架構:
分層架構的一個重要原則是:每層只能與位於其下方的層發生耦合。分層架構也分為幾種:在嚴格分層架構中,某層只能與直接位於其下方的層發生耦合;而松散分層架構則允許任意上方層與任意下方層發生耦合。由於用戶界面層和應用服務通常需要與基礎設施打交道,許多系統都是基於松散分層架構的。
事實上,較低層也是可以和較高層發生耦合的,但這只局限於采用觀察者模式或者調停者模式的情況。較低層是絕對不能直接訪問較高層的。例如,在使用調停者模式時,較高層可能實現了較低層定義的接口,然后將實現對象作為參數傳遞到較低層。當較低層調用該實現時,它並不知道實現出自何處。
用戶界面只用於處理用戶顯示和用戶請求,它不應該包含領域或業務邏輯。有人(我就屬於這種人)可能會認為,既然用戶界面需要對用戶輸入進行驗證,那么它就應該包含業務邏輯。事實上,用戶界面所進行的驗證和對領域模型的驗證是不同的。在介紹實體的章節中將會就講到,對於那些粗制的,並且只面向領域模型的驗證行為,我們依然應該予以限制。
如果用戶界面使用了領域模型中的對象,那么此時的領域對象僅限於數據的渲染展現。在采用這種方式時,可以使用展現模型(是不是視圖模型呢?)對用戶界面與領域對象進行解耦。
由於用戶可能是人,也可能是其他的系統,有時用戶界面層將采用開放主機服務(對接微信接口時是不是就可以用這種方式?)的方式向外提供API。
用戶界面層是應用層的直接客戶。
應用服務位於應用層中。應用服務和領域服務是不同的,因此領域邏輯也不應該出現在應用服務中。應用服務可以用於控制持久化事務和安全認證,或者向其他系統發送基於事件的消息通知,另外還可以用於創建郵件以發送給用戶。應用服務本身並不處理業務邏輯,但它卻是領域模型的直接客戶。應用服務是很輕量的,它主要用於協調對領域對象的操作,比如聚合。同時,應用服務是表達用例和用戶故事的主要手段。因此,應用服務的通常用途是:接收來自用戶界面的輸入參數,再通過資源庫獲取到聚合實例,然后執行相應的命令操作,比如:
public void CommitBacklogItemToSprint(string aTenantId, string aBacklogItemId, string aSprintId)
{
using(TransactionScope scope = new TransactioScope()) { TenantId tenantId = new TenantId(aTenantId); BacklogItem backlogItem = backlogItemRepository.backlogItemOfId(tenantId, new BacklogItemId(aBacklogItemId)); Sprint sprint = sprintRepository.sprintOfId(tenantId, new SprintId(aSprintId)); backlogItem.CommitTo(sprint); scope.Complete(); } }
如果應用服務比上述功能復雜許多,這通常意味着領域邏輯已經滲透到應用服務中了,此時的領域模型將變成貧血模型。因此,最佳實踐是將應用層做成很薄的一層。當需要創建新的聚合時,應用服務應該使用工廠或聚合的構造函數來實例化對象,然后采用資源庫對其進行持久化。應用服務還可以調用領域服務來完成和領域相關的任務操作,但此時的操作應該是無狀態的。
當領域模型用於發布領域事件時,應用層可以將訂閱方注冊到任意數量的事件上,這樣的好處是可以對事件進行存儲和轉發。同時,領域模型只需要關注自己的核心邏輯;領域事件發布器也可以保持輕量化,而不用依賴於消息機制的基礎設施。
我們將在另外的章節中就講到領域模型對業務邏輯的處理。然而,在傳統的分層架構中,卻存在着一些與領域相關的挑戰。在分層架構中,領域層或多或少地需要使用基礎設施層。這里我並不是說核心的領域對象會直接參與其中,而是說領域層中的有些接口實現依賴於基礎設施層。
比如,資源庫接口的實現需要基礎設施層提供的持久化機制。那么,如果我們將資源庫接口直接實現在基礎設施層會怎樣呢?由於基礎設施層位於領域層之下,從基礎設施層向上引用領域層則違反了分層架構的原則。遵從分層架構原則並不意味着領域對象需要與基礎設施層發生直接耦合,此時,采用依賴倒置原則(推薦)可以為它們解耦。
在傳統的分層架構中,基礎設施層位於底層,持久化和消息機制便位於該層中。這里的消息包含了消息中間件所發的消息、基本的電子郵件或文本消息。可以將基礎設施層中所有的組件和框架看作是應用程序的低層服務,較高層依賴於該層發生耦合以重用技術上的基礎設施。即便如此,我們依然應該避免核心的領域模型對象與基礎設施層發生直接耦合。