Rails 中實現 Domain-Driven Design 的挑戰(ver. 2023)
有段時間沒有寫 Domain-Driven Design(以下簡稱 DDD)的主題,最近剛好跟一些朋友討論以及做了不少實驗,覺得可以針對這些題目再一次討論在 Ruby on Rails 中導入會遇到的問題。
套件概念
這是前陣子跟在 Shopify 工作的朋友聊到,在 Ruby 裏面是沒有 Namespace(命名空間)的概念,也因此許多語言都有的套件(Package)在 Ruby 之中需要用一些特殊的方式去實現。
舉個例子,從 JavaScript(CommonJS)、Golang 或者 Python 這幾個我近期有寫到的語言來看,都能夠具體的透過命名空間做出區分。
1import { User } from './entities'
2
3export class DatabaseUsers implement UserRepository {
4 public ListAll(): User[] {
5 // ...
6 }
7}
1package repositories
2
3import (
4 "github.com/elct9620/example/internal/entities"
5)
6
7// ...
8func (u *DatabaseUsers) ListAll() []entities.User {
9 // ...
10}
這樣子會有幾個好處,首先是我們可以區分出「內部」跟「外部」的功能,也就是可以限制外部能夠存取到的行為,進而讓封裝變的單純,另一方面透過「選擇性引用」的方式,可以減少命名上的衝突,像是 Admin::User
和 System::User
可以根據使用情境直接用 User
來使用。
正因如此 Shopify 加入了 packwerk 這個 gem 來輔助檢查這件事情,在比較複雜的系統中,能夠明確的聲明套件的依賴關係,對複雜度的問題是非常有幫助的,在 DDD 之中則會成為切分邊界的形式。
Packwerk 是 Shopify 用來檢查套件的封裝是否合理的工具,可以用來驗證非 Public API 不會被外部存取到,來確保每個套件符合 Clean Architecture 的設計,避免不恰當的依賴造成耦合。
框架限制
框架(Framework)之所以被稱之為框架,就是因為其設定好非常多「預設立場」也因此會讓我們在實現 DDD 的過程中遭遇到無法參考其他語言做法的狀況,同時 Rails 是為了快速實現想法而設計,DDD 中許多實踐在初期都是拖慢速度的原因。
從這點來看,也許不少公司在成長到一定規模後,會選擇把 Rails 為主的系統替換掉,可能是這個原因所造成的。而 Rails 官方也一直沒有給出一個指南說明,那麼在產品成長後繼續維持以 Rails 為主的架構可能就找不到夠好的理由。
就我個人而言,在 Rails 框架中實踐 DDD 最為困難的就是 Repository(倉儲)的概念應該如何定位的問題,從性質上來看 Rails 的 Model 兼具了 Repository 和 Entity(實體)的特性,即使將讀取的部分提取,也還會受到寫入類型(#save
)方法的限制,不容易乾淨的分離。
舉例來說,在 Rails 中一個 Model 同時負責了資料存取跟狀態管理兩種職責。
1def update
2 @user = User.find(params[:id]) # Repository 的職責
3 # ...
4 @user.activate # Entity 的職責
5 @user.save # Repository 的職責
6end
同時也因為這樣的狀況,整個 Model 大多會跟背後的資料庫綁定,在大型系統中要轉換為 RESTful API 或者其他資料來源,也就變得難以切割,讓問題變得複雜。
不同語言也都有著不一樣的問題需要面對,然而在 Rails 同時在語言特性跟框架限制的狀態下,就讓這個問題變得更難處理。
缺少選擇
如果想要突破框架的限制,以最近我比較長接觸的 Golang、Python 兩個語言的角度來看不外乎就是不使用框架跟使用相性較好的框架,然而在 Ruby 的生態系中仍有不少限制。
Golang 本身就是設計成開發網路服務的語言,因此許多 Rails 協助處理的問題大多被內建在語言中,從零開始搭建適合的架構在熟悉後並不困難,另一方面 Python 就我個人的認知中 Django 的設計本身也蠻適合實踐 DDD,因此就不會像 Rails 這樣有不少「被處理好」而失去彈性的地方。
回到 Ruby 的生態系,目前我認為最接近 DDD 的是以 Clean Architecture 為基礎設計的 Hanami 框架,然而目前還在逐漸的完善整個生態系,跟 Rails 相比入門難度跟可以使用的選項也是非常不足的。
如果想要從零開始,也有像是 Sinatra 之類的工具可以做為基礎,然而要替換掉 ActiveRecord 的替代方案,像是 Sequel 或者以其為基礎的 ROM 都有不少需要花時間處理的問題,就讓這件事情的實現變的相對困難很多。
Hanami 是以 ROM 為基礎建構 Model、Entity 等物件,然而背後做了非常多修改,才讓框架本身可以善用 ROM 的特性來實現適合 DDD 開發的環境。
整體來說,我們可以理解為整個 Ruby 的社群正在思考關於 Domain-Driven Design 的問題,然而也還沒有找到一個非常具體的方向,除了等待社群的嘗試之外,也只能靠自己不斷實驗來找出適合的方向。