---
title: "領域驅動設計 - 重新思考 Rails 架構"
date: 2024-08-23T00:00:00+08:00
publishDate: 2024-08-23T00:00:00+08:00
lastmod: 2024-06-02T17:03:47+08:00
tags: ["Rails","Domain-Driven Design","設計","Clean Architecture"]
series: "rethink-rails-architecture"
toc: true
permalink: "https://blog.aotoki.me/posts/2024/08/23/rethink-rails-architecture-domain-driven-design/"
language: "zh-tw"
---


在 Rails 中除了以模型（Model）為出發點思考如何設計，我們也可以從領域（Domain）的角度進行思考，雖然 Rails 的架構對於[領域驅動設計（Domain-Driven Design）](https://en.wikipedia.org/wiki/Domain-driven_design)的應用一直都是社群想挑戰的主題，仍沒有主流的方式，然而從不同的角度思考仍是有助於設計更好的系統。

<!--more-->

## 職責{#responsibility}

在學習物件導向時，很常會聽到單一職責（Single Responsibility）這樣的思考方式，我認為領域也是類似的思考方式，都是用於釐清某個物件或者功能應該涵蓋的範圍。

延續「分庫出貨」的實作，我們在思考系統的設計時，應該要先討論訂單（Order）和運送（Shipping）是否為同一件事情，這並沒有一個絕對的答案，基本上會取決於當下的情境和脈絡。

這裡我們先假設訂單跟運送是兩個不同領域的任務，因此我們要讓兩者可以專注在各自的責任上。那麼，原本 Rails 經常會像這樣設計關聯，可能不太適合。

```ruby
class Order < ApplicaitonRecord
  has_many :shippings

  # ...
end
```

因為在我們的概念中訂單和運送是兩個獨立的模組，因此在操作訂單時不應該去觸碰到運送相關的任務，那麼在 Model 的使用上，不要去定義關聯會是更好的。

## 流程 {#workflow}

如果要實現當所有品項送達時，更新訂單狀態的設計，就不會在 Model 上面進行處理，因為這是橫跨兩個不同領域的任務。

這個情境就很適合在 Controller 上進行，因此在實作 Controller 的時候會去描述兩個模型之間的互動是怎樣的。

```ruby
class ShippingState < ApplicationController
  # ...
  def update
    # Step 1
    @shipping.shipped!

	# Step 2
	completed = Shipping.for(@shipping.order_id).all?(&:shipped?)

    # Step 3
    if completed
	  order = Order.find(@shipping.order_id)
	  order.shipped!(at: @shipping.shipped_at)
	end
  end
end
```

在這裡我們的處理一共有三個步驟，第一個步驟是將運送標記成「送達」狀態，接下來我們會確認同一筆訂單的運送狀態是否都是送達的，假設滿足條件就會進到最後的階段將訂單也更新成送達狀態。

這樣的設計還有另一個好處，就能有一定程度的[冪等性](https://en.wikipedia.org/wiki/Idempotence)（Idempotency）及[最終一致性](https://zh.wikipedia.org/zh-tw/%E6%9C%80%E7%BB%88%E4%B8%80%E8%87%B4%E6%80%A7)（Eventual consistency）

從冪等性的角度來看，即使中間處理失敗，只要狀態沒問題，再次執行就能夠繼續下去。最終一致性的角度來看，不論哪一筆運送紀錄先完成，最後都能夠讓訂單狀態進入到「已送達」的狀態，即使發生問題，只要再次執行任一筆運送紀錄的處理，也還是會變成「已送達」

這樣也更能解釋清楚在模型驅動設計時，我們認為訂單會需要狀態跟送達時間，卻又無法解釋在 `has_many :shippings` 的前提下，我們為什麼會有這樣欄位的狀況，如果是從職責、流程的角度來分析，因為是不同領域的問題看似「重複」但是「意義不同」是經常發生的。

> 上述的 Controller 設計會讓 Controller 變得複雜，因此在 Rails 社群經常會使用 Service Object 的方式處理，然而我認為他更適合用 [Command Pattern](https://refactoring.guru/design-patterns/command) 來處理，也能作為銜接 [CQRS 架構](https://martinfowler.com/bliki/CQRS.html)做準備。

