# 2025/04/27(Draft)
戰術設計:
常用模式: Entity, Value Object, Aggregate, Aggregate Root, Domain Servive, Factory, Anti-Corruption Layer, Repository
戰略設計是必須的:全局視角。關注領域模型之間的整體協作
根據業務邊界提出"限界上下文",而限界上下文之間的協作,提出"上下文映射&子域"
=>核心是理解業務知識
=>戰略三大主題:上下文/精煉/大型結構
貧血 & 充血 模型:
貧血模型 => Service腫脹 => Rails
充血模型 => 區分業務職責
***DDD不是取代物件導向,而是基於物件導向更近一步將模型分配到對應的邊界中,並定義不同邊界的模型與協作方式,使其交互過程清晰,依賴關係耦合更少***
---
貧血模型:有屬性沒有行為的模型(業務封裝邏輯:只提供業務數據容器,並不發生業務行為=>都讓service做就好了...)
這會讓service散落在各個角落,而且某個業務邏輯會重複出現在不同的service object之中。
service業務處理外還要處理外部服務,交互... 太胖了!
優點:
1. 關注點分離:每一層注重自己的職責
2. 復用性強
3. 前期開發效率高(適合MVP)
缺點:
1. 底層缺乏抽象
2. 業務邏輯分散
3. 難以持續演化

---
充血模型:有屬性又有行為
service不需要了解業務,將業務邏輯與基礎設施操作分離。(Rails應該不容易達成,因為model包含find_by or create ... 這很難額外封裝)
1. User interface: 不包含任何業務邏輯處理,僅暴露應用層服務,這裡程式碼應該相對簡單
2. Application:不包含業務邏輯,接受用戶請求後,通過基礎設施層加載領域模型(聚合根),然後交由領域模型完成業務操作,最後由基礎設施層持久化領域模型
3. Domain: 對業務進行領域建模的結果,包含所有領域模型 ex:Entity, Value Object和領域服務(行為)
4. Infrastructure: 實踐領域層定義的基礎設施接口,不包含業務邏輯。輸出和輸入應該是領域模型或者基礎數據類型
---
Hexagonal Architecture


...
# 2025/04/28
Command Object的資料包裝物件來代表"意圖"而非"動作"(可以加以驗證,但不可以有商業邏輯)
Struct
# 2025/04/29
Factory
```ruby=
# app/domain/factories/article_factory.rb
module Domain
module Factories
class ArticleFactory
# 從 Command 或 DTO 建立 ArticleEntity
def self.build_from_command(cmd)
title = ArticleTitle.new(cmd.title) # Value Object
content = ArticleContent.new(cmd.content)
ArticleEntity.new(
id: ArticleId.new, # 產生新的 ID
title: title,
content: content,
metadata: Metadata.defaults # 一些預設資訊
)
end
end
end
end
```
但我感覺rails不需要經過這流程,因為這樣的話只是複雜化,而多一層封裝而已,而且物件創立其實不太會寫測試
---
Repository提供save方法,來達成數據持久化 => rails物件本身不用再次封裝,除非save流程牽涉到多種物件 => aggrate才會需要?
=> 想到的例子是event內的repeatable event=> 但專案內定義是form object? 某種程度上好像把商業邏輯也寫進了form object內=>難怪挺髒的?=>可以考慮用aggrate和Repository重構看看?
---
在 Rails 裡實踐 DDD 事件驅動(Domain-Driven Event-Driven)的大致流程,就是:
1. **在領域層(Entity/Aggregate)定義並產生事件**
2. **在應用層(Application Service/Repository)發佈事件**
3. **在基礎設施層註冊訂閱者(Subscriber)並處理事件**
以下分別以三種常見作法示範:
---
## 1. 用 ActiveSupport::Notifications
### 1.1 定義並發佈事件
```ruby
# app/services/article_application_service.rb
class ArticleApplicationService
def publish(id)
ActiveRecord::Base.transaction do
article = Article.find(id)
article.publish! # 領域行為,會更新狀態、時間
article.save!
# 事件發佈
ActiveSupport::Notifications.instrument(
'article.published', # 事件名稱
id: article.id,
title: article.title
)
end
end
end
```
### 1.2 在啟動時訂閱
```ruby
# config/initializers/article_events.rb
ActiveSupport::Notifications.subscribe('article.published') do |name, start, finish, id, payload|
# payload = { id: ..., title: ... }
SearchIndexingJob.perform_later(payload[:id])
NotificationMailer.article_published(payload[:id]).deliver_later
end
```
- **優點**:不需要額外 gem,Rails 原生支援;
- **注意**:需在 initializer 或 Rails.application.configure 裡訂閱一次。
---
## 2. 用 Wisper(輕量的 Pub/Sub Gem)
### 2.1 安裝並設定
```ruby
# Gemfile
gem 'wisper'
# app/models/article.rb
class Article < ApplicationRecord
include Wisper::Publisher
def publish!
raise 'Already published' if published_at
update!(published_at: Time.current)
broadcast(:article_published, self) # 發佈事件
end
end
```
### 2.2 實作訂閱者
```ruby
# app/subscribers/article_subscriber.rb
class ArticleSubscriber
def article_published(article)
SearchIndexingJob.perform_later(article.id)
NotificationMailer.article_published(article.id).deliver_later
end
end
```
### 2.3 在 ApplicationService 註冊並呼叫
```ruby
# app/services/article_application_service.rb
class ArticleApplicationService
def initialize
@repo = Article
# 將 subscriber 綁到 Article model
Wisper.connect(ArticleSubscriber.new, async: true)
end
def publish(id)
article = @repo.find(id)
article.publish! # 會自動呼叫 subscriber
end
end
```
- **優點**:乾淨的發/訂閱 API、內建非同步選項;
- **注意**:若重複 connect,要確保只註冊一次。
---
## 3. 用專門的事件存儲庫(RailsEventStore)
當你需要完整的事件溯源(Event Sourcing)或複雜的流程管理(Saga/ProcessManager)時,可以引入 [RailsEventStore](https://github.com/RailsEventStore/rails_event_store):
### 3.1 定義事件
```ruby
# app/events/article_published.rb
class ArticlePublished < RailsEventStore::Event
# attributes: metadata、data
end
```
### 3.2 在 Service 內發佈
```ruby
class ArticleApplicationService
def initialize(event_store: Rails.configuration.event_store)
@event_store = event_store
end
def publish(id)
ActiveRecord::Base.transaction do
article = Article.find(id)
article.publish!
article.save!
event = ArticlePublished.new(data: { id: id, title: article.title })
@event_store.publish(event, stream_name: "Article$#{id}")
end
end
end
```
### 3.3 註冊訂閱器
```ruby
# config/initializers/rails_event_store.rb
Rails.configuration.event_store.tap do |store|
store.subscribe(SearchIndexingHandler.new, to: [ArticlePublished])
store.subscribe(ArticleNotificationHandler.new, to: [ArticlePublished])
end
```
- **優點**:內建事件儲存、重放、安全的事務邊界;
- **適用**:需要完整事件歷史、回溯、複雜 Saga 流程。
---
### 小結
1. **產生事件(在 Entity 或 Service)** → 2. **發佈事件(Event Bus)** → 3. **訂閱事件(Subscriber / Handler)** → 4. **執行副作用(索引、通知、外部 API)**
2. Rails 原生可以用 **ActiveSupport::Notifications**;輕量級可用 **Wisper**;需要完整事件溯源時可用 **RailsEventStore**。
3. 關鍵在於:**把「狀態更新」與「副作用處理」分離**,讓你的領域模型只專注在業務規則上,副作用邏輯統一由訂閱者處理。
這樣就能在 Rails 裡,以 DDD 事件驅動的方式清晰地管理領域對象的狀態變化與後續動作!
想看看pub/sub怎應用
---
application layer會有command/query/veiw類型對象 => command object/ query object/ decorator object
---
# 2025/05/05
在 DDD 架構中,`Proc`/`Lambda` 最常被用來「封裝可替換的行為」,讓業務邏輯更易組合、測試與擴充。以下三個典型範例說明它們如何落地:
---
## 1. Specification(規格模式)
將複雜的條件判斷抽成可重用、可組合的規格(Specification),每個規格都用一支 `->`(Lambda)實作:
```ruby
# app/domain/specifications/specification.rb
class Specification
def initialize(&predicate)
@predicate = predicate
end
def satisfied_by?(entity)
@predicate.call(entity)
end
def and(other)
Specification.new { |e| satisfied_by?(e) && other.satisfied_by?(e) }
end
def or(other)
Specification.new { |e| satisfied_by?(e) || other.satisfied_by?(e) }
end
end
# 使用範例
active_spec = Specification.new(&->(user){ user.active? })
adult_spec = Specification.new(&->(user){ user.age >= 18 })
adult_active_spec = active_spec.and(adult_spec)
# 驗證
if adult_active_spec.satisfied_by?(current_user)
# …允許後續操作
end
```
* **好處**:把「活躍使用者」或「成人使用者」的規則獨立成小單元,可任意組合(`and`/`or`),也能針對某個規格寫專門的單元測試。
---
## 2. Strategy(策略模式)/Domain Service
當某段業務邏輯需要不同演算法時,可以把演算法注入成 `Proc`/`Lambda`:
```ruby
# app/domain/services/price_calculator.rb
class PriceCalculator
def initialize(strategy)
@strategy = strategy
end
# items: [{ price: 100, qty: 2 }, …]
def calculate(items)
@strategy.call(items)
end
end
# 不同策略
standard = ->(items){ items.sum { |i| i[:price] * i[:qty] } }
holiday = ->(items){ standard.call(items) * 0.9 } # 打 9 折
volume_off = ->(items){
total = standard.call(items)
total > 1000 ? total * 0.85 : total
}
# 在 ApplicationService 或 Handler 裡:
calc = PriceCalculator.new(holiday)
order_total = calc.calculate(cart.items)
```
* **好處**:把「計價邏輯」與「呼叫端」分離,隨時可增減/切換策略,而不用動到核心程式。
---
## 3. Domain Events(領域事件)處理
使用 `Proc`/`Lambda` 當成事件訂閱者(Subscriber),收到事件後一行程式就能執行:
```ruby
# app/domain/events/event_publisher.rb
module EventPublisher
@subscribers = Hash.new { |h,k| h[k] = [] }
def self.subscribe(event_name, &handler)
@subscribers[event_name] << handler
end
def self.publish(event_name, payload)
@subscribers[event_name].each { |h| h.call(payload) }
end
end
# 在初始化階段或某個配置檔:
EventPublisher.subscribe(:order_created) do |order|
SendOrderConfirmationEmail.call(order.id)
end
EventPublisher.subscribe(:order_created) do |order|
InventoryService.reserve_items(order.id)
end
# 在 CommandHandler 達成訂單建立後觸發:
EventPublisher.publish(:order_created, order)
```
* **好處**:對事件的反應邏輯可以「隨時新增/移除」而不侵入核心流程,也方便測試單一事件處理。
---
### Proc vs Lambda 小提醒
* **`lambda`**:參數檢查嚴格、`return` 只跳出 lambda 區塊。
* **`Proc.new`/`proc`**:參數可少傳、`return` 會試圖回到定義它的方法、比較危險。
在 DDD 中,推薦用 `->(...) { ... }` 或 `lambda`,明確把行為當「獨立函式」注入。
---
透過這些模式,`Proc`/`Lambda` 就成為 DDD 架構下,「把行為當作資料」流動/組合的利器,既保持程式碼乾淨,也增強了彈性和測試性。
# 2025/05/06
---
## 整体调用链示意
假设我们注册了两层 Middleware:
```ruby
config.middleware.use Logging
config.middleware.use Authentication
```
在 Rails 初始化后,底层会把它们串成一个链条,最终接到你应用的最“终点”——Rails 路由与 Controller。大致结构是:
```
[ Web Server ]
│
▼
Logging#call(env)
│ (在内呼叫 Authentication)
▼
Authentication#call(env)
│ (在内呼叫 Rails App 本身)
▼
Rails Router → Controller#action
```
当一个 HTTP 请求进来时,会依照上面的顺序,一层层地执行 `call(env)`,并把 `env` 继续往 “下一层” 传。
---
## 详细执行步骤
以一次 GET `/orders` 请求为例,伪代码跟踪如下:
1. **Web Server 接受请求**
* 比如 Puma 收到一个 GET `/orders`,把请求环境封装到一个 `env` Hash。
* 调用最外层 Middleware:
```ruby
response = Logging.new( next_app ).call(env)
```
* 这里 `next_app` 就是下一环,也就是 `Authentication` 的实例。
2. **第一层:Logging#call(env)**
```ruby
class Logging
def initialize(app)
@app = app
end
def call(env)
Rails.logger.info "Received #{env['REQUEST_METHOD']} #{env['PATH_INFO']}"
@app.call(env) # <──① 将请求「往下一层」传递
end
end
```
* **做了什么?**
* 先打日志:`"Received GET /orders"`
* 然后用 `@app.call(env)` 把请求交给它的下家(也就是 `Authentication`)
* **为什么能往下?**
* 在 `initialize` 的时候,Rails 框架已经把 `app` 设为下一个 middleware 的实例。
3. **第二层:Authentication#call(env)**
```ruby
class Authentication
def initialize(app)
@app = app
end
def call(env)
if env["HTTP_AUTH_TOKEN"] == "secret"
@app.call(env) # <──② 通过认证后,再次「往下一层」传给 Rails 核心
else
[401, {"Content-Type"=>"text/plain"}, ["Unauthorized"]]
end
end
end
```
* **做了什么?**
* 检查 header 里的 `AUTH_TOKEN`
* 如果合法,就继续往下,不合法就直接回 401,后面的 Controller 根本不会被触发
* **继续传递?**
* 同样是调用 `@app.call(env)`,`@app` 这次就是 Rails 应用自身,包括路由解析、Controller action。
4. **Rails 核心路由与 Controller**
* Rails 收到从 Authentication 传过来的 `env`,启动路由匹配,找到 `OrdersController#index`:
```ruby
class OrdersController < ApplicationController
def index
orders = Order.where(user_id: current_user.id)
render json: orders
end
end
```
* 最终返回一个 `[status, headers, body]` 三元组给上层。
5. **响应沿链条回传**
* Rails 返回的三元组直接交给 `Authentication#call` 调用处作为返回值,它再把这个返回值交给 `Logging`,最后到 Web Server,再发给客户端。
---
## 为什么「解耦」?
* **呼叫者(Web Server)**:完全不知道哪一层会处理认证、打日志,或最终调用哪个 Controller,只管把请求传给最外层 Middleware。
* **每一层 Middleware**:
* **只关注自己的事**(Logging:打日志;Authentication:验 token)
* **通过 `@app.call(env)` 决定是否往下传**(可以中途拦截、修改响应,或者直接 short-circuit)
* **Controller**:更不需要关心认证或日志,假设进来就是一个“已经验证过”的请求,只专注在业务逻辑上。
---
### 拓展思考
* 如果要在所有 Controller 前做 “限流”/“风控”,只要写一个新的 Middleware,插到链的合适位置即可,无需修改 Controller 任何代码。
* 这就是责任链(Chain of Responsibility)在 Rails 中最典型、最天然的落地:「每一层决定要不要把请求继续往下传」,并且相互独立,达到解耦的效果。
*
**解釋責任鏈在領域服務中的使用**
在 DDD 分層架構裡,責任鏈(Chain of Responsibility)本身是一種領域行為(Domain Behavior),不應該散落在應用層(Application Layer)或塞進實體/值對象裡──而是要集中到領域層的一個\*\*領域服務(Domain Service)\*\*中,負責『組裝』和『執行』這條鏈』。
---
## 為什麼這麼做?
| 層級 | 責任 | 不當放置的問題 |
| -------- | ---------------------------------------------------------- | ------------------------------------------------------- |
| **應用層** | 用例流程 orchestration:接收命令、呼叫 Domain Service、處理事務、回傳結果 | 如果在這裡維護責任鏈,就會把業務細節混到用例流程中,造成 ApplicationService 臃腫、難以重用 |
| **領域模型** | 實體(Entity)、值對象(Value Object)、聚合根(Aggregate)– 保持單一職責:狀態與不變式 | 如果把鏈式邏輯塞進實體或值對象,就會把多個獨立行為耦合在一起,違反「單一職責」且測試困難 |
| **領域服務** | 處理「一個以上實體/聚合」的跨聚合行為,或無法歸屬到某個實體的複雜運作 | **正確的落點**:在這裡把各個 Handler/Step 組合成一條鏈,並執行它。封裝所有業務邏輯流程。 |
---
## 範例:訂單處理的責任鏈放在領域服務中
```ruby
# app/domain/services/order_processing_service.rb
class OrderProcessingService
def initialize(chain = default_chain)
@chain = chain
end
# 用例層或 Controller 只需呼叫這個方法
def call(request)
@chain.call(request)
end
private
# 在這裡組裝整條責任鏈
def default_chain
ValidateStock.new(
ProcessPayment.new(
CreateRecord.new(
SendConfirmationEmail.new
)
)
)
end
end
```
* **ValidateStock**、**ProcessPayment**、**CreateRecord**、**SendConfirmationEmail** 都是各自只做一件事的 Handler。
* `OrderProcessingService` 負責「把它們串在一起」及「啟動執行」。
* **呼叫端**(ApplicationService、Controller、Job)只要:
```ruby
OrderProcessingService.new.call(order_request)
```
完全不用知道具體有哪些步驟,也不用管理 transaction 或錯誤處理細節。
---
## 結論
> **在 DDD 裡,領域服務是負責協調多個聚合/實體或實作跨聚合的行為的合適場所。**
>
> 責任鏈作為一連串「有序執行的領域步驟」,它的**組裝邏輯**與**執行邏輯**都屬於領域行為,理應放在領域層的領域服務中,而不是分散在應用層或混進某個實體裡。
這樣不但能保持各層的**單一職責**、讓程式碼更乾淨,也方便未來**增減步驟**、撰寫**聚焦測試**,以及在不同用例間重複利用同一條責任鏈。
---
# 2025/05/12
以下是一種「先抓商業邏輯、後做資料庫設計」的可行流程,結合 DDD/Hexagonal 架構與 TDD 思路,在 Rails 專案中也能落地:
## 1. 定義領域模型與行為(Domain-First)
1. **蒐集用例**
以「故事 (User Story)」或「領域事件」為單位,寫下清楚的場景敘述:
> 「當使用者送出訂單時,系統須計算總金額、檢查庫存、保留貨品,並最終標記訂單為 `confirmed`。」
2. **抽象出聚合與實體**
* 以故事中出現的名詞(訂單 order、項目 line\_item、庫存 stock)當「實體/值物件」。
* 以故事中出現的動詞(計算金額、檢查庫存、確認訂單)當「方法/行為」。
3. **撰寫純 Ruby 物件**
在 `app/domain/…` 下,先建立「純邏輯」的類別,不碰 ActiveRecord:
```ruby
# app/domain/order.rb
class Order
attr_reader :id, :items, :status
def initialize(id:, items:)
@id = id
@items = items # array of LineItem
@status = :pending
end
def total_amount
items.sum { |li| li.price * li.quantity }
end
def confirm!(stock_checker:)
raise "庫存不足" unless stock_checker.available?(items)
stock_checker.reserve!(items)
@status = :confirmed
end
end
```
4. **為領域物件寫測試**
完全針對「Order#total\_amount」、「Order#confirm!」寫單元測試。
```ruby
describe Order do
let(:items) { [LineItem.new(price:100,q:2), …] }
subject { Order.new(id:1, items: items) }
it "計算總價" do
expect(subject.total_amount).to eq 200
end
it "confirm! 時會呼叫 stock_checker" do
checker = double(available?: true, reserve!: true)
subject.confirm!(stock_checker: checker)
expect(subject.status).to eq :confirmed
end
end
```
---
## 2. 定義 Repository 介面
因為我們還沒做 DB,就用「介面」先寫好要的方法:
```ruby
# app/domain/order_repository.rb
class OrderRepository
def find(id); raise NotImplementedError; end
def save(order); raise NotImplementedError; end
end
```
> **註**:呼叫端(Application Service)只依賴這個抽象,不關心底層怎麼存。
---
## 3. 撰寫 Application Service(Use Case)
把故事流程承接起來、協調聚合與倉儲(Repository):
```ruby
# app/application/services/place_order.rb
class PlaceOrder
def initialize(order_repo:, stock_checker:)
@repo = order_repo
@stock_checker = stock_checker
end
def call(user_id:, items:)
order = Order.new(id: SecureRandom.uuid, items: items)
order.confirm!(stock_checker: @stock_checker)
@repo.save(order)
order
end
end
```
也為它加上對應的測試,保證「用例流程」符合商業需求。
---
## 4. 設計資料庫(Migrations)
當所有領域邏輯、介面與測試都跑綠了,就把重點放到 DB:
```bash
rails g migration CreateOrders id:uuid total_amount:decimal status:string
rails g migration CreateLineItems order_id:uuid price:decimal quantity:integer
```
> **註**:欄位可依照 `order.total_amount`、`order.status`、`LineItem` 屬性補齊。
---
## 5. 實作 Repository Adapter(Infra Layer)
將 ActiveRecord 與你的領域模型做映射:
```ruby
# app/models/order_record.rb # ActiveRecord 版
class OrderRecord < ApplicationRecord
has_many :line_item_records
end
# app/repositories/order_repository_impl.rb
class OrderRepositoryImpl < OrderRepository
def find(id)
rec = OrderRecord.find(id)
items = rec.line_item_records.map do |li|
LineItem.new(price: li.price, quantity: li.quantity)
end
Order.new(id: rec.id, items: items).tap { |o| o.status = rec.status.to_sym }
end
def save(order)
rec = OrderRecord.find_or_initialize_by(id: order.id)
rec.total_amount = order.total_amount
rec.status = order.status.to_s
rec.save!
# 同步 line items...
end
end
```
---
## 6. 整合並發佈
在初始化 Application Service 時,注入具體的 Repository 與 StockChecker:
```ruby
repo = OrderRepositoryImpl.new
checker = RealStockChecker.new
PlaceOrder.new(order_repo: repo, stock_checker: checker)
.call(user_id: ..., items: [...])
```
這樣,你就完成了:
1. **先確保商業邏輯乾淨、測試充足**
2. **再設計 DB 架構與欄位**
3. **最後把 persistence 納入架構**
---
### 這樣做的好處
* **Domain-First**:所有核心規則都經過專注測試,跟資料庫無關,乾淨易測。
* **高內聚低耦合**:領域物件、用例服務、Repository 介面、Infra 層各司其職。
* **彈性強**:以後要改成別的儲存(NoSQL、外部 API),只需實作另一個 RepositoryImpl,不改 Domain/Use Case。
透過以上流程,就能在 Rails 專案中,真正做到「從商業邏輯出發、最後再做資料庫設計」的 DDD 實踐。
# 2025/05/14
在策略模式裡,**Context** 就像是一個「中介者/管家」,它只暴露給外部一個統一的方法,背後根據注入的策略(Strategy)決定真正要做什麼演算法。Context 的意義在於:
1. **隱藏細節**:使用者(或 Controller)不需要知道有哪些策略,只呼叫 Context 的同一個方法即可。
2. **統一介面**:不管背後換了哪一個策略,呼叫方式都一樣。
3. **動態切換**:在執行時注入不同策略,就能改變行為,而不用改 Context 或呼叫端程式。
---
## 更真實的範例:運費計算
想像一個電子商務平台,需要根據使用者選擇的運送方式(標準、快遞、海外)來計算費用,但業務要求這些演算法要能獨立維護、測試,且未來再加新方式不用動到既有程式。
### 1. 定義策略介面與具體策略
```ruby
# app/domain/shipping_strategy.rb
class ShippingStrategy
# 統一介面:所有策略都要實作 calculate 方法
def calculate(weight:, distance:)
raise NotImplementedError
end
end
# app/domain/standard_shipping.rb
class StandardShipping < ShippingStrategy
def calculate(weight:, distance:)
# 標準運費:NT$50 + 每公斤 NT$10 + 每公里 NT$1
50 + weight * 10 + distance * 1
end
end
# app/domain/express_shipping.rb
class ExpressShipping < ShippingStrategy
def calculate(weight:, distance:)
# 快遞運費:NT$100 + 每公斤 NT$20 + 每公里 NT$2
100 + weight * 20 + distance * 2
end
end
# app/domain/international_shipping.rb
class InternationalShipping < ShippingStrategy
def calculate(weight:, distance:)
# 國際運費:NT$200 + 每公斤 NT$50 + 每公里 NT$5
200 + weight * 50 + distance * 5
end
end
```
### 2. 實作 Context:ShippingCostCalculator
```ruby
# app/services/shipping_cost_calculator.rb
class ShippingCostCalculator
def initialize(strategy)
@strategy = strategy
end
# 統一的計算方法,呼叫者只需知道「計算運費」,不必關心細節
def calculate(weight:, distance:)
@strategy.calculate(weight: weight, distance: distance)
end
end
```
### 3. 在 Controller 動態切換策略
```ruby
class OrdersController < ApplicationController
def create
# 從前端參數決定策略
strategy =
case params[:shipping_method]
when 'standard' then StandardShipping.new
when 'express' then ExpressShipping.new
when 'international' then InternationalShipping.new
else
raise "不支援的運送方式"
end
calculator = ShippingCostCalculator.new(strategy)
cost = calculator.calculate(
weight: params[:weight].to_f,
distance: params[:distance].to_i
)
render json: { shipping_cost: cost }
end
end
```
---
### 為何看得出 Context 的意義?
1. **呼叫端(Controller)只跟 Context 互動**
```ruby
calculator = ShippingCostCalculator.new(chosen_strategy)
cost = calculator.calculate(...)
```
不會出現 `if … elsif …` 把演算法寫在 Controller 裡,也不需要重複計算邏輯。
2. **要新增新運費策略,只要**
* 新增一個 `class NewShipping < ShippingStrategy` 並實作 `calculate`
* 在 Controller 的 `case` 或在更進階做法的「配置檔」裡注入即可
不用碰到 `ShippingCostCalculator` 或其他已存在的策略。
3. **統一測試 Context**
```ruby
it "用標準運費計算" do
calc = ShippingCostCalculator.new(StandardShipping.new)
expect(calc.calculate(weight: 2, distance: 10)).to eq(50 + 2*10 + 10*1)
end
```
你也可以獨立測試每個策略:
```ruby
it "ExpressShipping 打 100 + 2*20 + 10*2" do …
end
```
---
### 小結
* **Context(ShippingCostCalculator)** 的角色就是「給外部一個固定門面」,讓你隨時替換背後的策略(Strategy),卻不用改呼叫它的程式。
* 它隱藏了「到底是標準、快遞、還是國際運費算法」的細節,只留下「計算運費」這件事。
* 當你在專案裡,發現某段邏輯有多種演算法,且想把它們分開維護、動態切換、統一測試,就非常適合把它們抽成策略,再用 Context 來包一層統一介面。
---
在 DDD 的分層裡,通常把程式碼分成:
1. **Domain Layer(領域層)**
* 核心商業規則,包含 Entity、Value Object、Domain Service、Specification、Policy…
2. **Application Layer(應用/用例層)**
* 用例(Use Case)或 Application Service,負責協調領域物件去完成一次「業務流程」(transactional orchestration),但不含太多商業規則本身。
3. **Infrastructure Layer(基礎建設層)**
* 實作 Repository、第三方 API、Queue、ORM 等技術細節。
4. **UI Layer(界面層)**
* Controller、Presenter、View…
---
### 對照 Shipping 範例
```ruby
# 抽象策略──定義「計算運費」這件核心規則
class ShippingStrategy
def calculate(weight:, distance:); raise; end
end
# 各種演算法──純商業邏輯
class StandardShipping < ShippingStrategy; def calculate(...) … end; end
class ExpressShipping < ShippingStrategy; def calculate(...) … end; end
class InternationalShipping < ShippingStrategy; def calculate(...) … end; end
# Context/Calculator──也可以看作是 Domain Service
class ShippingCostCalculator
def initialize(strategy); @strategy = strategy; end
def calculate(weight:, distance:); @strategy.calculate(weight: weight, distance: distance); end
end
```
* **StandardShipping、ExpressShipping、InternationalShipping**
這些都屬於 **Domain Layer** 的「商業邏輯」,即 Domain Service(或 Policy Object)。它們實作了不同的「運費計算規則」。
* **ShippingCostCalculator**
因為它也是針對「運費計算」這個領域概念,完全不依賴 ORM、外部資源,純粹執行商業行為,你也可以把它當成一個 **Domain Service**。它負責「把策略(算法)串起來、執行計算」。
---
### 那麼 Use Case(應用層)長什麼樣?
Use Case 層的職責是「接收外部請求、協調一或多個 Domain Service/Entity、處理事務邊界、回傳結果」。舉例,若我們要設計一個「查詢某訂單的運費」的用例,就會長成下面這樣:
```ruby
# app/application/use_cases/calculate_order_shipping_cost.rb
class CalculateOrderShippingCost
def initialize(order_repo:, shipping_strategy_selector:)
@repo = order_repo
@selector = shipping_strategy_selector
end
def call(order_id:)
order = @repo.find(order_id)
# 根據訂單屬性(國內/國際;快速/普通)選策略
strategy = @selector.select_for(order)
calculator = ShippingCostCalculator.new(strategy)
cost = calculator.calculate(weight: order.weight, distance: order.distance)
Result.new(cost: cost, order_id: order.id)
end
end
```
* `.find`、`.select_for`、`ShippingCostCalculator` 這些都在 **Domain Layer**,屬於商業邏輯/Domain Service。
* 但把它們「串在一起」的 `CalculateOrderShippingCost` 就是 **Application Layer**(一個用例)。它負責:
1. 載入資料(Repository)
2. 挑策略(可能是另一個 Selector Service)
3. 執行計算(呼叫 Domain Service)
4. 處理交易邊界(如果需要)
5. 回傳結果(DTO or Value Object)
---
### 總結
* **策略(Strategy)與 Calculator**:都是 **Domain Service**,它們承載真正的「商業規則/算法」。
* **用例(Use Case)**:在 **Application Layer**,它只負責「把各種 Domain Service、Repository、Entity 組合在一起」,形成一次完整的業務流程(例:計算運費、下訂單、付款流程…)。
所以,如果你問「ShippingStrategy 那些 class,是屬於商業邏輯還是 Use Case?」──
* **它們主要是商業邏輯(Domain Service)**。
* **真正的 Use Case**,是那個在 `app/application/use_cases` 下、把策略注入、呼叫、並回傳結果的 Service。
---
在 Clean Architecture(或一般 DDD 分層)裡:
1. **Entities(Enterprise Business Rules/Domain Layer)**
* 封裝「核心不變式」與「單一行為」──也就是純粹的領域商業規則(enterprise business rules)。
* 例如:`Order#total_amount`、`Address#valid?`、`Money#add`…
* 這裡的「商業邏輯」是你說的「每一個行為」。
2. **Use Cases(Application Business Rules/Application Layer)**
* 負責「把多個行為串起來」,實現具體的用例流程(application business rules)。
* 這邊會協調 Entities、Domain Services、Repository、外部介面,並處理事務邊界、權限檢查等。
* 例如:`PlaceOrder` 用例會呼叫 `Order.new`、`order.confirm!`、`OrderRepository.save`,最後回傳結果。
換句話說:
* **Domain(Entities+Domain Services)**:真正在定義「這個系統裡,什麼才叫做合法的『訂單』、什麼叫做合法的『金額計算』」──屬於整個企業/產品線通用的規則。
* **Application(Use Cases)**:則是「在這個應用場景下,我們要如何把那些規則組合起來,完成一次『下訂單流程』或『計算運費流程』」,包含呼叫時機、交易(transaction)範圍、例外處理和回傳格式。
---
### 以下單為例
```ruby
# 【Entities / Domain Service】
class Order
def total_amount; …; end # 單一行為
def confirm!(stock_checker); …; end # 單一行為 + 不變式
# 【Use Case / Application Service】
class PlaceOrder
def call(user_id:, items:)
order = Order.new(user_id:, items:)
order.confirm!(stock_checker)
repository.save(order)
send_confirmation_email(order)
order
end
end
```
* `Order#confirm!` 是「單一行為」→ **Enterprise rule**
* `PlaceOrder#call` 是「把多個行為按順序執行」→ **Application rule**
---
**結論**:
> – 你說的「商業邏輯是每一個行為」——對應到 Entities/Domain Service(enterprise business rules)。
> – 「Use Case 是將每個行為組合而成不同的結果」——對應到 Use Cases(application business rules)。
---
# 2025/05/18
下面以「後台訂單篩選」為例,示範如何在 Rails 專案中,把規約模式(Specification Pattern)落地成一個可複用、可組合的過濾機制。
## 1. 場景描述
* 管理員想在後台訂單列表頁,依照「訂單狀態」與「訂單金額範圍」做篩選:
1. 只看已**確認**(`status: 'confirmed'`)的訂單
2. 只看「總金額 ≥ NT\$1,000」的訂單
* 篩選條件可以任意組合:
* 只篩確認狀態
* 只篩高金額
* 同時篩「確認且高金額」
* 全部訂單(沒套任何規約)
---
## 2. 定義規約介面與具體規約
```ruby
# app/domain/specifications/specification.rb
class Specification
# 抽象方法:檢驗單一物件,或轉成 AR scope
def satisfied_by?(candidate)
raise NotImplementedError
end
def to_scope(relation)
raise NotImplementedError
end
# 組合:回傳新的 AndSpecification
def and(other)
AndSpecification.new(self, other)
end
end
# app/domain/specifications/and_specification.rb
class AndSpecification < Specification
def initialize(left, right)
@left, @right = left, right
end
def satisfied_by?(candidate)
@left.satisfied_by?(candidate) && @right.satisfied_by?(candidate)
end
def to_scope(relation)
# 先套左邊,再套右邊
@right.to_scope(@left.to_scope(relation))
end
end
```
```ruby
# app/domain/specifications/confirmed_order_specification.rb
class ConfirmedOrderSpecification < Specification
def satisfied_by?(order)
order.status == 'confirmed'
end
def to_scope(relation)
relation.where(status: 'confirmed')
end
end
# app/domain/specifications/high_value_order_specification.rb
class HighValueOrderSpecification < Specification
def initialize(min_amount = 1_000)
@min = min_amount
end
def satisfied_by?(order)
order.total_amount >= @min
end
def to_scope(relation)
relation.where('total_amount >= ?', @min)
end
end
```
---
## 3. 在 Controller 中使用
```ruby
class Admin::OrdersController < Admin::BaseController
def index
spec = build_specification
# 用規約來產生最終的 ActiveRecord::Relation
@orders = spec.to_scope(Order.all).order(created_at: :desc).page(params[:page])
end
private
def build_specification
specs = []
specs << ConfirmedOrderSpecification.new if params[:status] == 'confirmed'
specs << HighValueOrderSpecification.new(params[:min_amount].to_i) if params[:min_amount].present?
# 如果沒條件,就回傳「全域匹配」物件
return AllSpecification.new if specs.empty?
# 將多個規約合併成一條責任鏈
specs.reduce { |chain, spec| chain.and(spec) }
end
end
```
```ruby
# app/domain/specifications/all_specification.rb
class AllSpecification < Specification
def satisfied_by?(_); true; end
def to_scope(relation); relation; end
end
```
---
## 4. 測試範例
```ruby
describe ConfirmedOrderSpecification do
let!(:confirmed) { create(:order, status: 'confirmed') }
let!(:pending) { create(:order, status: 'pending') }
it "only matches confirmed" do
spec = ConfirmedOrderSpecification.new
expect(spec.satisfied_by?(confirmed)).to be true
expect(spec.satisfied_by?(pending)).to be false
expect(spec.to_scope(Order.all)).to include(confirmed)
expect(spec.to_scope(Order.all)).not_to include(pending)
end
end
describe "複合規約" do
let!(:o1) { create(:order, status: 'confirmed', total_amount: 500) }
let!(:o2) { create(:order, status: 'pending', total_amount: 2000) }
let!(:o3) { create(:order, status: 'confirmed', total_amount: 2000) }
it "confirmed AND high value" do
spec = ConfirmedOrderSpecification.new
.and(HighValueOrderSpecification.new(1000))
expect(spec.satisfied_by?(o1)).to be false
expect(spec.satisfied_by?(o2)).to be false
expect(spec.satisfied_by?(o3)).to be true
expect(spec.to_scope(Order.all)).to contain_exactly(o3)
end
end
```
---
## 5. 優點回顧
* **可組合**:隨時用 `and`、日後也能實作 `.or`、`.not`
* **復用性**:同樣的 `ConfirmedOrderSpecification` 不只用在後台,也能在其他地方重複使用
* **測試容易**:單獨驗證各個規約,也能驗證複合邏輯
* **清晰分層**:Domain Layer 專注定義「什麼叫做已確認」、「什麼叫做高價值」,Controller 只負責組裝規約並套用