淺談我對 Domain-Driven Design 的理解和 Rails 中的實作

Domain-Driven Design (DDD)是一門不明覺厲的軟體架構設計理論,本文整理了我的理解,以及在 Ruby on Rails 中怎麼派上用場。

我發現 DDD 每個人看到的重點都不一樣,我到現在還是覺得有種瞎子摸象的感覺。

我想這是因為 DDD 是個大雜燴,把軟體需求管理、大架構、小架構 在同一個理論下解釋。因此三個部分,看你愛講哪一塊,每個人看的重點不同。

1. 和領域專家的共通語言,重點放在與領域專家的溝通 。我認為這一塊就是需求管理、軟體規格、建模方法等。有很多其他書(例如 User Story Mapping)跟這部分重疊了,講的也比 DDD 詳細完整。

2. 大架構戰略層次: 拆解不同領域,例如用 microservice 來做,或是用 Modular 來做(下述) 。我認為這一塊是 DDD 比較精彩的部分,也是我認為最重要的部分。

3. 小架構戰術層次: 例如 Service Object。我認為這一塊偏向物件導向理論,我覺得也不是重點,有很多其他講物件導向的書在講了,例如 SOLID 和各種 Design Pattern 等等。用了這些招數,也不表示就是 DDD。這塊還有出一些很進階的大架構常跟DDD一起討論,例如 Hexagonal Architecture, Clean Architecture 等等,但我覺得都太難理解導入了,已經遠離DDD的本質。

基礎理論

DDD 的聖經本是 領域驅動設計,不過我真正有看完的是這兩本 領域驅動設計精粹领域驱动设计精简版,重點大約是

  • Bounded Context 跟 Ubiquitous Language
    • Subdomain, Core Domain, Supporting Subdomain, Generic Subdomain 等等概念
    • 大體上就是將軟體切開成不成領域,每個領域內有自己統一的領域語言,讓所有關係人(包括PM跟開發人員)都用一樣的語言溝通
  • Context Mapping
    • 描述各種 bundled context 之間的溝通方式和 API 方法
  • Entity, Aggregate, Value Object, Service
    • 定義模型物件的種類,算物件導向的常識了
    • 根據業務,用 transaction 為單位隔離出來
    • 不需要即時的,非 transaction 的,則拆開不同 aggregate 用非同步處理
  • Domain Event 領域事件
    • 不需要同一個 transaction 的其他操作,用非同步的 event 發布出去
    • 用於跨 bounded contexts 的非同步(de-couple)溝通
    • 這也可以存下來
  • 建模方法
    • 和領域專家做 “事件風暴 Event Storming”。但其他方法也大同小異的樣子,包括:
      • Specification by Example 但這個作者認為會額外費工15%~25%…. XD
      • Impact Mapping
      • User Story Mapping
    • Event Storming 在 DDD 聖經本並沒有,我其實不喜歡後來這被加進 DDD 裡面一起講。我之前也有花錢上過課程,也聽過現場演示,但上完的感覺就是看不出來跟 User Story 需求收集有什麼不一樣。DDD 我認為還是講技術就好,硬是把需求管理也納進來包山包海都講,我覺得太分散重點了。

以上就基礎理論來說,其實也不是這麼難。本質目標就是大型軟體需要做(概念上的)切分。剩下的問題是如何實作。

更多DDD介紹可以參考 DDD 學習路徑與資源分享

實作

我認為DDD其實是沒有告訴你一定要怎麼實作切分的,因此找東西看的時候就會墜入五里霧,例如就是要弄微服務才是 DDD?

聖經 Domain-Driven Design 這本書,這本書也沒講微服務。但是後人實作時,就把微服務扯到也是DDD。做 SOA? 或是 Clean Architecture (這更難懂)?或是 Hexagonal Architecture (也很難懂) 以及加上這本 Implement Domain-Driven Design 書也提到很多技巧,例如 Repository, Event Sourcing 等等,好像這些設計都派上用場,才是 DDD ?

眾所周知,Rails 是一個單體(Monolithic)架構,同一個應用盡量不要實際拆分不同專案。我反對在 Rails 上用非標準的 MVC 架構,因為這會讓後續維護非常困難,新人上手難度爆表。任何一個新手學 Rails 三個月之後,若還是完全看不懂你的 Rails 專案架構,我認為很可能就是不好的 Rails 架構(除非你的公司有大把預算跟時程可以培訓新人上手啦)。

因此在追求 Rails 如何實踐 DDD 的路上,我看到以下幾種方式可以繼續保持 Rails 單體 MVC 的架構。基本上我認為只要可以拆好模組邊界,就符合 DDD 最重要的 bounded context 目的。

Interface Object

對 Rails 來說,第一步不應該直接就跳去 SOA 或微服務架構,而是應該先在單體架構內做 Component 切分即可,明確定義 public interface,只透過 class method 呼叫即可。

也就是這篇文章 Ruby on Rails – Bounded contexts via interface objects 所用的方式。

Component-Based Rails Applications (CBRA)

Component-Based Rails Applications

這本書採用 Rails engine 來做 DDD,聽起來蠻不錯的,實作上會碰到的問題有

  1. require 問題
  2. Migration 問題
  3. Testing 問題
  4. Asset loading 問題

總之就是問題蠻多的,等於是 Rails Engine 有什麼坑,你就會踩到什麼。

我感覺 Rails Engine 在 Rails core 中並不是優先級很高的功能,社群也不是很多人在用這個功能,多半是一些簡單的通用後台在用這個功能而已。 因此在你的應用中依賴這個功能,我認為是蠻危險的,會浪費很多時間在踩坑。

剛點進去看作者官網,發現作者自己都放棄 CBRA 這方式了,改推基於 package 的 Gradual modularization (又一本新書作者還在寫)

Domain-Driven Rails

products.arkency.com/domain-driven-rails/

這本書的作者 Arkency 也是不愛 Rails engine,因為它們認為 engine 主要用在拆 UI (web level),例如拆 admin UI,但是拆 admin UI 跟 DDD 要拆的 domain 其實是兩回事。另外就是 Rails engine 方案會依賴 Rails,這也是 Arkency 不喜歡的。

幾個內容心得:

  • Bounded Contexts
    • 其實就是拆 namespace,只透過 explicit class method 介面去溝通
    • 跟上述的 Interface Object 方式非常類似
  • Aggregates, value object, services 是常識了
  • Command bus
  • Domain events 用在跨 bounded contexts 溝通 de-couple
  • CQRS 主要是 read performance,另一個好處則是簡化 query,比 cache 不易出錯
  • Event Sourcing 實作超高難度
    • 理論很美好,但實作感覺超難
    • 整個流程和思維要改成用 event 紀錄為主,而且還需要搭配 CQRS 才實用,簡直超大工程
    • 有看到一個 Gem 套件 www.sequent.io/

CQRS 跟 Event Sourcing 都是 DDD 聖經本沒有的東西,我認為也不是必要的架構,要看業務場景需求吧。我沒有實作過。

Shopify 的 Packwerk 方案 (Modular Monolith)

Shopify 是世界上採用 Ruby on Rails 最大的單體應用之一 (在採用 Ruby 的公司市值排名第二) 很具有代表性。他們採用的理論是 Modular Monolith,並且釋出自己的 Packwerk 套件。雖然文件上沒有特別強調 DDD,但我認為其實這就是在實踐DDD最重要的精神。

Modular Monolith 其實呢,就是仍然保持單體應用(一個codebase),但是內部嚴格限制不同領域的邊界(strictly enforced boundaries between different domains)。

Modular Monolith 的實作跟上述 Interface Object 或 Arkency 的方式差不多的,而這個 Packwerk 套件是個檢查器(validator),原理是靜態程式碼分析(static code analysis)。Packwerk 用來定義 package 邊界,以及檢查程式碼是否滿足 package 的條件。畢竟團隊長這麼大,又只有一個 codebase 的話,需要嚴格檢查 package 之間的依賴情況,同時 Packwerk 還會檢查這個依賴不能是個 cycle 環(指套件互相依賴),必須是 acyclic 無環。

這個 1-Minute Demo of How Packwerk Works | Shopify Engineering 可以看一下就知道它的用處了。

以上,反正我不會糾結到底是不是DDD了。就像 Shopify 那兩篇 Blog 文章,完全沒有提到 DDD 耶。

發佈留言

發表迴響