---
title: "職責劃分 - 重新思考 Rails 架構"
date: 2024-09-27T00:00:00+08:00
publishDate: 2024-09-27T00: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/09/27/rethink-rails-architecture-decide-responsibility/"
language: "zh-tw"
---


透過釐清脈絡、邊界後，我們大致上就能對整個系統的全貌有一定程度的理解。接下來會需要根據當下系統的狀況，釐清我們需要有哪些類型的物件，以及擔任怎樣的職責，來確保[單一職責（Single-responsibility principle）](https://en.wikipedia.org/wiki/Single_responsibility_principle)

<!--more-->

## 職責{#responsibility}

Rails 作為一個 MVC 框架，對於物件的職責還是相當容易區分。然而這只限於比較簡單的情境，當我們的系統變複雜後，通常會需要繼續細分任務才足以使用，比較主流的就是 [Clean Architecture](https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html) 的方式。

單純從 MVC 的角度看，我們會有這樣的責任分配

* Model - 保存狀態、商業邏輯
* View - 呈現資訊、處理算繪（Render）
* Controller - 接收操作並且協調 Model & View

這種情境通常表示輸入的資料跟狀態通常非常接近或者一致，並且也沒有太複雜的處理邏輯。當使用者填寫的表單跟資料庫有差異時，Controller 就需要做大量的處理，並且 Model 上的驗證機制很可能無法發揮作用。

此時 Form Object 的概念就會被加入進來，把 Controller 和 Model 的職責抽離一部分出來，變成新的類型。

* Model - 保存狀態、商業邏輯
* View - 呈現資訊、處理算繪（Render）
* Controller - 協調 Form 和 Model 並且回傳 View 產生的內容
* Form - 根據輸入轉換成 Model 可以使用格式

然而，當我們希望 Model 的狀態不該被輕易改變，就會需要依靠實作一些方法來協助處理。此時就會從 Model 直接以 `#assign_attributes` 方法讀取 Form 的內容，轉變為依靠 Service Object 來操作的形式。

* Model - 保存狀態、商業邏輯
* View - 呈現資訊、處理算繪（Render）
* Controller - 協調 Form 和 Service 協作回傳 View 產生的內容
* Form - 根據輸入轉換成 Service 的參數
* Service - 根據參數操作 Model 進行處理

上述這種擴充的過程是比較接近 Clean Architecture 的方式，然而在實作上如果沒有恰當的設計，就會很難擴充成這樣的架構，因此在開發初期仍需要耗費一定成本釐清某些介面（Interface）的存在。

## 案例{#example}

上述的說明仍是相對抽象，我們繼續以物流系統的案例進行說明。在開發初期，我們通常會直接採取 Rails 常見的的方式實作，大致上如下。

```ruby
class ShipmentController < ApplicationController
  def update
    @shipment.update(shipment_params)
    # ...
  end

  private

  def shipment_params
    params.require(:shipment).permit(
      items_attributes: %i[shippable_type shippable_id]
    )
  end
end
```

當我們需要針對不同角色給予權限，或者一性次修改所有運送的品項時，在 `Shipment` 上實作驗證就會相當複雜，此時拆分為 Form Object 就會是個好方法。

```ruby
class ShipmentController < ApplicationController
  def update
    @form = Customer::ShipmentForm.new(shipment_params)
    @form.valid!

    @shipment.update(@form.attributes)
    # ...
  end

  # ...
end
```

```ruby
class Customer::ShipmentForm < BaseForm
  # ...

  def attributes
    {
      items_attributes: formated_items
    }
  end
end
```

像這樣子，我們可以在 Form 裡面針對 `items_attributes` 額外做一些處理，雖然也可以用 `items` 直接對應物件，不過會造成有額外的職責，因此需要避免。

然而，這樣依舊很難針對運送操作進行細部的處理，因此會繼續擴充出 Service Object 來協助我們處理。

```ruby
class ShipmentController < ApplicationController
  def update
    @form = Customer::ShipmentForm.new(shipment_params)
    @form.valid!

	@service = Customer::UpdateShipmentService.new
	@service.call(
	  @shipment,
	  items: @form.items
	)

    # ...
  end

  # ...
end
```

```ruby
class Customer::UpdateShipmentService < BaseService
  # ...

  def call(shipment, items:)
    items.each do |item|
        state = item.delete(:state)
		case state
		when :remove then shipment.remove_item(item[:id])
		when :update then shipment.update_item(item[:id], item)
		when :add then shipment.add_item(item)
		end
	end
  end
end
```

如上述的例子，我們被允許在使用者輸入的資料加入一些額外的屬性，這些屬性不用對應 `Shipment` 和 `ShipmentItem` 的資料表欄位，我們可以透過 Form Object 先檢查過這些輸入的內容是否正確，並且轉交給 Service Object 根據提供的內容進行對應的動作。

> 上述仍是單純的 CRUD 情境，如果是有搭配一些 API 呼叫的情境，使用 Service Object 來進行協調這些處理等等，就更能夠將不同類型的職責區分出來。

