蒼時弦也
蒼時弦也
資深軟體工程師
發表於

在 Rails 導入 Domain-Driven Design 是解藥還是毒藥

在八月份的開發者對話主題是「Domain-Driven Design(領域驅動開發,以下簡稱 DDD)」算是最近很常被提到的關鍵字,雖然還在學習相關的知識,不過我們還是藉由活動一貫的問答方式跟大家一步步深入討論不少相關的問題。

前提

因為我認為「戰略(設計)」是 DDD 最為重要的部分,大部分的文章也都會提醒這點,雖然很快的都會接入到「戰術(實作)」的部分,也因此我們大多數人都是從戰術來理解 DDD 的,我也不例外,這篇文章的討論也會以此為前提。

我認為很多討論都要建立在有「領域專家」的協助才能夠成立,像是我們該如何判斷當下的邊界(Bounded Context)來劃分出領域(Domain)和子領域(Sub-Domain)等等,因此在討論戰術時我們都會先給一個假設

規模

因為是從「公司想導入 DDD 在 Rails 使用但覺得奇怪」作為起始,我先拋出「DDD 是為了解決怎樣的問題」來進行分析。

以我的了解來看 DDD 的很多案例都是 Java 或 .NET 為前提,這些都是企業級的系統在使用的,然而在 Rails 中更多被新創公司所採用,即使有像是 GitHub、Shopify 這樣的成功案例,也不代表他們使用 DDD 來解決問題。

假設一個系統非常簡單,我們真的需要用 DDD 去處理嗎?

特性

接下來我們針對「奇怪」這件事情分析,當我們將以其他語言作為範例的「戰術」應用到 Rails 裡面時,無視語言特性的話,會出現格格不入的狀況。

舉例來說,當我們沒有 Interface(介面)的概念時,如何處理像是 Repository 實作的問題。同時 ActiveRecord 提供了 ORM 機制,也跟 Domain Layer 中的 Entity、Repository 設計互相衝突的。

剛好我前陣子在一篇討論 DDD 的文章看到了 DHH 的在 Twitter 上的討論,相比在 Rails 使用 DDD 而是更偏向 DCI(Data, Context and Interation) 的方式,然而這並不代表 DDD 無法被應用在 Rails 之中,而是我們需要理解 Rails 框架跟 Ruby 語言兩者的特性。

在維基百科的 DCI 條目 也提到屬於一種 MVC 的補充,以 Rails 來說確實更加適合。然而 .NET MVC 也能夠使用 DDD 這也表示在符合某些條件下,我們還是能夠在 Rails 中使用 DDD 的設計。

借鑑

在了解 DDD 的過程中,我認為對 Rails 工程師來說需要了解的是 DDD 想傳達的精神,以及在戰術應用的方式。

最廣為人知的就是分層架構,在學習過程中我也發現跟 Clean Architecture 有相似之處,而且分層架構我們也能很順利的在 Rails 找到對應的角色,雖然界線不一定那麼明顯,但還是有跡可循。

層級分析
Presentation Layer(表現層)View 類型,Rails 原生就支援輸出不同格式
Application Layer(應用層)在 Clean Architecture 也叫 Use Cases 基本上就是 Controller 的 Action(動作)
Domain Layer(領域層)基本上就是 Model 的部分,因為直接涵蓋所有 Domain Object 類型,也因此過去有 Fat Model 的說法,實際上很多衍伸的物件也是由此細分
Infrastructure Layer(基礎層)基本上就是 Rails 框架本身提供的,像是 ActiveRecord 提供了跟資料庫的連線等等

上面是很粗略的區分,然而我們還是能觀察到 DDD 的分層架構基本上就是對一個應用的分析,我們在實作上不一定要完全侷限教科書、網路文章的方式,而是根據語言、框架順勢實作。

解析

基於上面的「借鑑」我們再稍微深入的分析幾個觀念,來幫助大家在使用上可以不要被範例侷限住,而是去思考背後的意義。

流程

因為 Application Layer 的實作基本上上就是 User Flow(使用者流程)或者稱為 Use Case(使用案例)因此我們要注意的是不要把「邏輯」放在裡面。

就我認為,所謂的流程就是「完成一件事情」的步驟,以「結帳」來說我們會到櫃檯檢查商品、統計金額、從錢包拿出對應的金錢。

除非發生例外,不然是不會中斷的,因此我在 Controller 裡面開始避免使使用 if 來做檢查,因為預期外的事情是例外(Exception)是會中斷流程的。

Rails 有提供 rescue_from 來協助我們在 Controller 處理例外,因此使用例外來中斷「流程」是沒問題的。

邏輯

我們也可以用「商業邏輯」來表示,裡面就會包含了「判斷」這件事情,延續流程的結帳流程,我們在過程中就會有需要「檢查錢包金額足夠」的動作。

既然 Domain Layer 被用來集中整個商業邏輯,在原本的 Rails 中我們統一由 Model 來掌管這些事情,最後變得過大而難以維護,也因此發展出了 Concern 這類特性。

除此之外,經常被 Rails 教學提到的 Service Object 其實很明顯的就應該是 Domain Service 裡面封裝了數個處理特定流程的「邏輯」來對應這些情況,很多時候我們會發現每個專案的用法都不太一樣,主要就是因為我們沒有意識到這件事情。

我們可以延伸思考,在 Rails 中 Repository、Entity 跑去哪裡,要怎樣才能夠在不影響整個框架設計的前提下,引入這些概念。

順勢

最近在思考該怎麼把程式寫好時,發現了一個很重要的觀念就是「順勢」每個語言、框架都有他的特性、擅長的部分,除非我們在設計新的特性,不然應該要思考作者在設計時的想法,順著這個方式去實踐。

雖然前面我們提到 DHH 對 Rails 框架的認知是偏向 DCI 的,然而 DHH 所在的 37signals 這間公司,在他們的 E-Mail 產品 Hey 中還是有使用到 Domain Driven 的概念,但從文章的例子來說對 Rails 確實是非常自然的。

範例

這篇文章是我在 Facebook 的貼文的延伸,範例程式碼可以參考一下前面我所瞭解到的戰術應用,在 Rails 中應該是怎樣的。

透過提煉 DDD 的概念,我希望在我的 Rails 專案中能達到這幾件事情。

可讀的

一段好的程式應該要讓所有人都能輕鬆理解,因此基於 User Flow 的區隔可以清楚描述「行為意圖」當我們需要知道更多細節時,就可以往下了解商業邏輯。這也跟我們在學習一個新的領域的知識接近,先看整理後再去了解每個環節的細節。

內聚的

因為 DDD 提倡的是充血模型,而這個想法跟原本 Rails 提倡的 Fat Model 基本上都是類似的,如果是一個 Entity 那麼所有改變自身狀態的行為都應該被聚合在這個 Entity 之中,Rails 的 Model 其實也有這樣的性質在。

簡單來說,在設計物件的時候從商業邏輯的角度思考,將相關的邏輯整合在一起,這樣也能夠讓我們從流程深入探索到邏輯時,能夠更快速的理解,而不需要花時間在不同物件之間來回尋找。

可測試的

因為今年(2022)上了 Odd-e 的 LeSS 敏捷課程,讓我重新了解到敏捷開發的好處,其中一個環節就是 E2E Testing 的優點,這點在 DDD 的可測試性上也有很大的好處。

我們使用 E2E Testing 就是為了確認使用者可以正常使用,幾乎就是 Happy Path 的情況,其實就是對 Application Layer 的流程進行測試。

當我們確認使用者在正常情況下可以使用後,就可以縮小範圍到單元測試(Unit Test)針對 Domain Layer 的物件小範圍的檢查不同的情境,進而打造一個更容易擴充的系統。

探索

因為 DDD 是「設計」層級的問題,其實是很難透過一篇文章就解釋清楚,除此之外也需要花時間不斷的實踐跟驗證,才能找到適合 Rails 的應用方式。

所以,該停下來把 Java / .NET 的 DDD 戰術應用在你的 Rails DDD 戰術上,從「了解 Rails」開始,只要我們朝著正確的測試、重構前進,最終還是會得到跟 DDD 類似的架構。

最後,我跟朋友正在開發一款遊戲,也被我用來驗證敏捷開發、DDD 的可行性,有興趣的話可以觀看我每週一的直播 - Build Game in Rails 也歡迎提出問題,一起討論在 Rails 實踐上的問題。