---
title: "釐清脈絡 - 重新思考 Rails 架構"
date: 2024-09-13T00:00:00+08:00
publishDate: 2024-09-13T00: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/13/rethink-rails-architecture-make-context-clean/"
language: "zh-tw"
---


我們繼續用「物流系統」作為案例，來探討將軟體架構設計完善時所需的前置準備，也就是去了解整個系統的脈絡（Context）或者說學習該領域（Domain）的知識。

這個過程大多還未進入到開發階段，因此不論語言、框架都是通用的，甚至可以說是否要使用某個語言或者框架，可能要再確認後才決定更加適合。

<!--more-->

## 從案例學習{#learn-by-example}

不論是設計文件（Design Document）或者產品需求文件（Product Requirement Document）如果只描述功能、規格層面的問題，對所有閱讀文件的人來說都是非常大的心理負擔，對新成員、初次接觸的領域來說，都會變得容易出錯。

舉例來說，在物流系統中有對於時區的特殊需求，以規格來描述會像這樣

> 當從站點出發時時，在 ATD 欄位以 `MMdd` 格式紀錄出發時間。抵達目標站點時，在 ATA 欄位以 `MMdd` 格式紀錄抵達時間，所有時間皆以當地時間表示。

以一份規格來說，已經算是相當清楚的資訊，將要填寫的數值、格式、換算等資訊都有描述出來，然而對於開發團隊仍要花時間理解完整的運作。

如果以案例的方式描述，那麼會像這樣

> 假設有一筆運送紀錄單號 O-1234
> 當從桃園（TPE）出發，在 ATD 欄位以當地時間（UTC+8） `1600` 填入。
> 當抵達沖繩（OKA）時，在 ATA 欄位以當地時間（UTC+9） `1830` 填入。

上述的案例看起來就會比規格容易理解很多，同時也會發現「規格」跟「案例」一定程度是互補的，我們可以從多個案例中推導出一個可能的規格，而案例也可以幫助對於規格的理解更加清楚。

除此之外，這些案例的撰寫方式實際上也很將近使用者故事（User Story）的描述方式，通常也會是描述使用者需求的一種呈現。

> 客戶下了一筆運送訂單 O-1234 從桃園（TPE）出發
> 當機場人員確認出發後，在 ATD 欄位以當地時間（UTC+8）`1600` 填入後送處
> 那麼在報表上可以看到 O-1234 的 ATD 呈現 `16:00`

> 客戶下了一筆運送訂單 O-1234 從桃園（TPE）出發
> 當機場人員確認抵達後，在 ATA 欄位以當地時間（UTC+9）`1830` 填入後送出
> 那麼在報表上可以看到 O-1234 的 ATA 呈現 `18:30`

出發跟抵達可能是兩個不同的流程（不一樣的步驟）因此我們會將使用者故事拆成兩件事情獨立描述，而且正好涵蓋了我們對於規格的理解。

> 這類型的案例就很適合用 [Cucumber 撰寫文件進行測試](https://blog.aotoki.me/series/test-with-cucumber/)

## 分類脈絡{#group-context}

正常狀況下，我們會不斷蒐集到各種需求、規格上的描述，然而如果只作為「單一系統」來看待，就可能把各種行為混合在一起處理，因此需要將脈絡釐清。

舉例來說，空運、海運等等都會是集中運輸的方式進行，也就是說我們除了透過案例了解系統時，也會發現一些預期外的資訊。

以大多數人的生活經驗來看，寄送包裹會是一個訂單加上貨品的組合。

```ruby
class Order < ApplicationRecord
  # 一筆訂單可以同時運送多個貨品
  has_many :products
end

class Product < ApplicationRecord
  belongs_to :order
end
```

然而，在空運或者海運的狀況下，運送的規模不會是一台車子，而是一整個貨櫃，因此還需要知道這些被寄送的商品被怎樣組合到其他運送單位上。

```ruby
class Shippment < ApplicationRecord
  # 一個運輸單位有多個「貨櫃」
  has_many :containers
end

class Container  < ApplicationRecord
  # 每個貨櫃以「棧版」為單位集裝
  has_many :pallets
  belongs_to :container
end

class Pallet  < ApplicationRecord
  # 商品被打散到不同棧版上
  has_many :products
end
```

加入了運送資訊後，是否開始覺得整個系統變得複雜，如果要再加入不同類型的運輸單位（如：貨車可能是單一貨櫃或者棧版）我們可能還要做更多的處理。

同時，你可能還會發現 `Order` 和 `Pallet` 同時都管理 `Product` 那麼其中一邊的物件做了修改後，是否會影響到另一邊的資料正確性，都會需要考量進去。

根據需求不同，會有不一樣的分類方式。在這個情境來看，我會選則分成三個不同的情境。

* 訂單（Order）
* 運送（Shipment）
* 集裝（Container）

判斷的依據通常會根據使用者故事的描述可以找到線索，通常有對應的操作就表示我們會有類似的分組出現，至少在「運送」這件事情上我們可以從文章一開始的案例看出來。

那麼，訂單跟分配貨品，就有蠻高的機率是兩件不同的事情。

> 印象中當時「集裝」是被設計在運送的畫面上，不過那個畫面非常複雜，實際操作設定哪些貨櫃被送出去應該是可以在另外的畫面被處理的。

