---
title: "Rails 中實現 Domain-Driven Design 的挑戰（ver. 2023）"
date: 2023-04-05T00:00:00+08:00
publishDate: 2023-04-05T00:00:00+08:00
lastmod: 2023-03-27T19:37:42+08:00
tags: ["經驗","架構","Ruby","Rails","Domain-Driven Design"]
toc: true
permalink: "https://blog.aotoki.me/posts/2023/04/05/the-challenge-to-add-ddd-in-rails/"
language: "zh-tw"
---


有段時間沒有寫 Domain-Driven Design（以下簡稱 DDD）的主題，最近剛好跟一些朋友討論以及做了不少實驗，覺得可以針對這些題目再一次討論在 Ruby on Rails 中導入會遇到的問題。

<!--more-->

## 套件概念{#concept-of-package}

這是前陣子跟在 Shopify 工作的[朋友](https://twitter.com/_st0012)聊到，在 Ruby 裏面是沒有 Namespace（命名空間）的概念，也因此許多語言都有的套件（Package）在 Ruby 之中需要用一些特殊的方式去實現。

舉個例子，從 JavaScript（CommonJS）、Golang 或者 Python 這幾個我近期有寫到的語言來看，都能夠具體的透過命名空間做出區分。

```js
import { User } from './entities'

export class DatabaseUsers implement UserRepository {
  public ListAll(): User[] {
    // ...
  }
}
```

```go
package repositories

import (
  "github.com/elct9620/example/internal/entities"
)

// ...
func (u *DatabaseUsers) ListAll() []entities.User {
  // ...
}
```

這樣子會有幾個好處，首先是我們可以區分出「內部」跟「外部」的功能，也就是可以限制外部能夠存取到的行為，進而讓封裝變的單純，另一方面透過「選擇性引用」的方式，可以減少命名上的衝突，像是 `Admin::User` 和 `System::User` 可以根據使用情境直接用 `User` 來使用。

正因如此 Shopify 加入了 [packwerk](https://github.com/Shopify/packwerk) 這個 gem 來輔助檢查這件事情，在比較複雜的系統中，能夠明確的聲明套件的依賴關係，對複雜度的問題是非常有幫助的，在 DDD 之中則會成為切分邊界的形式。

> Packwerk 是 Shopify 用來檢查套件的封裝是否合理的工具，可以用來驗證非 Public API 不會被外部存取到，來確保每個套件符合 Clean Architecture 的設計，避免不恰當的依賴造成耦合。

## 框架限制{#limitation-of-framework}

框架（Framework）之所以被稱之為框架，就是因為其設定好非常多「預設立場」也因此會讓我們在實現 DDD 的過程中遭遇到無法參考其他語言做法的狀況，同時 Rails 是為了快速實現想法而設計，DDD 中許多實踐在初期都是拖慢速度的原因。

從這點來看，也許不少公司在成長到一定規模後，會選擇把 Rails 為主的系統替換掉，可能是這個原因所造成的。而 Rails 官方也一直沒有給出一個指南說明，那麼在產品成長後繼續維持以 Rails 為主的架構可能就找不到夠好的理由。

就我個人而言，在 Rails 框架中實踐 DDD 最為困難的就是 Repository（倉儲）的概念應該如何定位的問題，從性質上來看 Rails 的 Model 兼具了 Repository 和 Entity（實體）的特性，即使將讀取的部分提取，也還會受到寫入類型（`#save`）方法的限制，不容易乾淨的分離。

舉例來說，在 Rails 中一個 Model 同時負責了資料存取跟狀態管理兩種職責。

```ruby
def update
  @user = User.find(params[:id]) # Repository 的職責
  # ...
  @user.activate # Entity 的職責
  @user.save # Repository 的職責
end
```

同時也因為這樣的狀況，整個 Model 大多會跟背後的資料庫綁定，在大型系統中要轉換為 RESTful API 或者其他資料來源，也就變得難以切割，讓問題變得複雜。

> 不同語言也都有著不一樣的問題需要面對，然而在 Rails 同時在語言特性跟框架限制的狀態下，就讓這個問題變得更難處理。

## 缺少選擇{#lack-options}

如果想要突破框架的限制，以最近我比較長接觸的 Golang、Python 兩個語言的角度來看不外乎就是不使用框架跟使用相性較好的框架，然而在 Ruby 的生態系中仍有不少限制。

Golang 本身就是設計成開發網路服務的語言，因此許多 Rails 協助處理的問題大多被內建在語言中，從零開始搭建適合的架構在熟悉後並不困難，另一方面 Python 就我個人的認知中 [Django](https://www.djangoproject.com/) 的設計本身也蠻適合實踐 DDD，因此就不會像 Rails 這樣有不少「被處理好」而失去彈性的地方。

回到 Ruby 的生態系，目前我認為最接近 DDD 的是以 Clean Architecture 為基礎設計的 [Hanami](https://hanamirb.org/) 框架，然而目前還在逐漸的完善整個生態系，跟 Rails 相比入門難度跟可以使用的選項也是非常不足的。

如果想要從零開始，也有像是 [Sinatra](https://sinatrarb.com/) 之類的工具可以做為基礎，然而要替換掉 ActiveRecord 的替代方案，像是 [Sequel](https://sequel.jeremyevans.net/) 或者以其為基礎的 [ROM](https://rom-rb.org/) 都有不少需要花時間處理的問題，就讓這件事情的實現變的相對困難很多。

> Hanami 是以 ROM 為基礎建構 Model、Entity 等物件，然而背後做了非常多修改，才讓框架本身可以善用 ROM 的特性來實現適合 DDD 開發的環境。

整體來說，我們可以理解為整個 Ruby 的社群正在思考關於 Domain-Driven Design 的問題，然而也還沒有找到一個非常具體的方向，除了等待社群的嘗試之外，也只能靠自己不斷實驗來找出適合的方向。

