ユビキタス言語 — チームの共通語を作る
「在庫引き当てのロジックを変えたい」
カズキの要望を聞いたリナは、コードを検索した。「在庫引き当て」という言葉は、コードの中にどこにも存在しなかった。Stock.decrement!、inventory_reserve、item_hold、decrease_stock...さまざまな表現が散在していた。
グローバルな grep をかけてみる。
# FreshCartのコードベースで "reserve" を検索
$ grep -r "reserve" app/ --include="*.rb" -l
app/models/stock.rb
app/services/cart_service.rb
app/workers/order_processing_worker.rb
app/models/inventory.rb
app/controllers/orders_controller.rb5ファイルに「在庫引き当て」に関係する処理が存在しているが、それぞれ異なる実装だ。どれが正規の実装なのか、それぞれどういう違いがあるのか、コードを追わなければわからない。
リナはため息をついた。「カズキに『カート追加時に仮押さえしたい』と言われても、今の仕様がどのコードなのかも特定できない」
ユビキタス言語とは
ユビキタス言語(Ubiquitous Language)とは、開発チームとドメインエキスパート(ビジネス側)が共有する共通の語彙のことだ。
「ユビキタス」は「どこにでも存在する」という意味だ。チームの会話、ドキュメント、コード、テスト、データベーススキーマ——あらゆる場所で同じ言葉を使う。
INFO
ユビキタス言語は「ビジネス用語をそのまま使う」という単純なルールだが、これがコードの品質を劇的に改善する。コードを読めば仕様がわかり、仕様を聞けばコードの場所が予測できる。「在庫引き当て」という言葉を変更したいと言われたら、StockReservation クラスを見ればいい。
言語の乖離がもたらすコスト
リナはFreshCartのコードベースで「同じ概念に使われている異なる言葉」を調査した。
| ビジネス概念 | コード内の表現(バラバラ) |
|---|---|
| 在庫引き当て | stock_reserve, item_hold, inventory_lock, decrement_stock, hold_quantity |
| 注文確定 | process(id, 1), confirm_order, finalize, complete_purchase, seal_order |
| 商品 | item, product, goods, merchandise, sku |
| 顧客 | user, customer, buyer, purchaser, member |
| 配送先住所 | address, shipping_address, delivery_addr, dest, to_addr |
| 注文合計 | total_price, amount, total, grand_total, order_amount |
この混乱が認知負荷を増大させ、バグを生み出す温床になっていた。「在庫引き当てのバグを直して」と言われた時、どのファイルのどのコードを直すべきか、経験豊富なエンジニアでも判断できない。
この「翻訳コスト」を数値で考えてみよう。
1つの概念について5つの異なる名前がある場合:
- コードを書く時: どの名前を使うべきか判断する (5分)
- コードを読む時: 「これはどの概念を指しているか」理解する (3分)
- 新機能を追加する時: 全パターンを探して影響確認 (30分)
- バグを修正する時: 全パターンを修正 (1時間)
- 新メンバーへの説明: 「このプロジェクトでの用語説明」 (2時間)
10の概念 × このコスト = 毎週数時間の無駄
ユビキタス言語の価値は「1週間で数時間の節約」という形で積み重なっていく。
用語集を作る実践
リナはカズキとともに「用語統一ワークショップ」を開催した。1時間のセッションで、FreshCartの主要な概念とその正式名称を決めていく。
ルールはシンプルだ:
- 概念を書き出す(ホワイトボードに付箋を貼る)
- 各概念について合意した英語名を決める
- 決まったら変えない(変える場合はチーム全員の合意が必要)
- コードに反映する
FreshCart ユビキタス言語辞典 v1.0
注文(Order)
- 顧客がカートを確定したもの
- ライフサイクル:
pending→confirmed→shipped→delivered|cancelled - 注: カート(Cart)は注文になる前の段階。別の概念として扱う
在庫引き当て(StockReservation)
- 注文確定時に商品の在庫を一時的に確保する行為
- 引き当て数量が在庫数量を超える場合は注文不可
- キャンセル時は在庫引き当てを「解放(release)」する
商品(Product)
- FreshCartが販売する食料品の単位
ProductVariant(サイズ・容量違い)を持つ場合がある- 注: 内部でバラバラに使われていた
item,goods,skuは廃止
顧客(Customer)
- FreshCartのサービスを利用する人
- 注: 内部実装の
User(認証・ログイン管理)とは概念を区別する - ビジネス文脈では常に
Customer、認証文脈ではUser
配送先(DeliveryAddress)
- 顧客が注文時に指定する届け先
- 顧客が保存している住所帳(
AddressBook)から選択するか、新規入力 - 注:
shipping_address,delivery_addrなどは廃止
在庫(Stock)
- 倉庫内にある商品の実数量
available_quantity(引き当て可能数量)=total_quantity-reserved_quantity
WARNING
用語集は「決めたら終わり」ではない。ビジネスが進化するにつれて言語も進化する。定期的に見直し、コードと同期させることが重要だ。用語の変更は「コードのリネーム」とセットで行う。
コードへの反映
ユビキタス言語を決めたら、コードに反映する。これは単なるリネームではなく、設計の改善だ。名前が変わることで、コードの構造自体も改善される。
Before: 曖昧なコード
# 何をする処理か、名前からわからない
def process(id, type, flag = false)
obj = Obj.find(id)
if type == 1
obj.update(status: flag ? 2 : 1)
Stock.where(item_id: obj.item_id).first.decrement!(:count)
Mailer.send_mail(obj.user_id, type)
elsif type == 2
obj.update(status: 3)
Stock.where(item_id: obj.item_id).first.increment!(:count)
Mailer.send_mail(obj.user_id, type)
end
end
# 呼び出し元 — 何をしているのかわからない
process(order_id, 1) # ??
process(order_id, 2, true) # ???After: ユビキタス言語を反映したコード
class Order < ApplicationRecord
# ビジネスの「注文ライフサイクル」がコードに現れている
enum :status, {
pending: 'pending',
confirmed: 'confirmed',
shipped: 'shipped',
delivered: 'delivered',
cancelled: 'cancelled'
}
def confirm!
raise Order::InvalidStateTransition,
"#{status}状態の注文は確定できません" unless pending?
transaction do
reserve_stock! # 在庫引き当て(ビジネス用語のまま)
update!(status: :confirmed, confirmed_at: Time.current)
OrderConfirmationMailer.send_confirmation(self).deliver_later
end
end
def cancel!(reason:)
raise Order::InvalidStateTransition,
"配送済みの注文はキャンセルできません" if shipped? || delivered?
transaction do
release_stock_reservations! # 在庫引き当て解放(ビジネス用語のまま)
update!(status: :cancelled, cancellation_reason: reason)
OrderCancellationMailer.send_cancellation(self).deliver_later
end
end
private
def reserve_stock!
order_items.each do |order_item|
StockReservation.reserve!(
product: order_item.product,
quantity: order_item.quantity,
order: self
)
end
end
def release_stock_reservations!
StockReservation.where(order: self).each(&:release!)
end
endconfirm! は注文確定の行為を、cancel! はキャンセルを、reserve_stock! は在庫引き当てを表している。コードを読めば、ビジネスで何が起きているかが理解できる。
StockReservation クラスの実装
class StockReservation < ApplicationRecord
belongs_to :product
belongs_to :order
class InsufficientStockError < StandardError
attr_reader :product_name, :requested_quantity, :available_quantity
def initialize(product_name:, requested_quantity:, available_quantity:)
@product_name = product_name
@requested_quantity = requested_quantity
@available_quantity = available_quantity
super("#{product_name}の在庫が不足しています。" \
"必要数: #{requested_quantity}, 在庫数: #{available_quantity}")
end
end
# 在庫引き当て — ビジネス用語がそのままメソッド名に
def self.reserve!(product:, quantity:, order:)
product.with_lock do
available = product.available_quantity
if available < quantity
raise InsufficientStockError.new(
product_name: product.name,
requested_quantity: quantity,
available_quantity: available
)
end
reservation = create!(
product: product,
order: order,
reserved_quantity: quantity,
reserved_at: Time.current,
status: :active
)
product.decrement!(:available_quantity, quantity)
reservation
end
end
# 在庫引き当て解放 — 「解放」というビジネス用語をメソッドに
def release!
transaction do
product.increment!(:available_quantity, reserved_quantity)
update!(status: :released, released_at: Time.current)
end
end
end「在庫引き当て」という概念が StockReservation クラスとして存在し、reserve! メソッドが「引き当て」という動詞を、release! が「解放」という動詞を体現している。
テストにもユビキタス言語を
テストコードにも同じ言語を使う。テストはドキュメントとしても機能する。ビジネス担当者が読んで理解できるテストが理想だ。
RSpec.describe Order, '注文確定' do
let(:customer) { create(:customer, membership_rank: :gold) }
let(:product) { create(:product, name: 'りんご 1kg', available_quantity: 10) }
let(:order) { create(:order, customer: customer, status: :pending) }
before do
create(:order_item, order: order, product: product, quantity: 3)
end
context '在庫が十分な場合' do
it '注文が確定される' do
order.confirm!
expect(order.reload.status).to eq('confirmed')
end
it '在庫が引き当てられる' do
order.confirm!
reservation = StockReservation.find_by(order: order, product: product)
expect(reservation).to be_present
expect(reservation.reserved_quantity).to eq(3)
end
it '引き当て後の利用可能在庫が減少する' do
order.confirm!
expect(product.reload.available_quantity).to eq(7) # 10 - 3
end
it '注文確定メールが送信される' do
expect(OrderConfirmationMailer)
.to receive(:send_confirmation).with(order)
.and_call_original
order.confirm!
end
end
context '在庫が不足している場合' do
before { product.update!(available_quantity: 2) }
it '在庫不足エラーが発生する' do
expect { order.confirm! }
.to raise_error(StockReservation::InsufficientStockError)
.with_message(/りんご 1kg/)
end
it '注文状態がpendingのまま変わらない(ロールバック)' do
order.confirm! rescue nil
expect(order.reload.status).to eq('pending')
end
it '在庫が変化しない(ロールバック)' do
order.confirm! rescue nil
expect(product.reload.available_quantity).to eq(2)
end
end
context '既に確定済みの注文の場合' do
before { order.update!(status: :confirmed) }
it '二重確定エラーが発生する' do
expect { order.confirm! }
.to raise_error(Order::InvalidStateTransition)
.with_message(/confirmed/)
end
end
endテストを読むだけで「在庫が十分な場合、注文が確定され、在庫が引き当てられる」というビジネスルールが理解できる。テストがビジネス仕様書の役割を果たしている。
データベーススキーマにも反映
# db/migrate/20240101000001_create_stock_reservations.rb
class CreateStockReservations < ActiveRecord::Migration[7.1]
def change
create_table :stock_reservations do |t|
t.references :product, null: false, foreign_key: true
t.references :order, null: false, foreign_key: true
t.integer :reserved_quantity, null: false
t.datetime :reserved_at, null: false
t.datetime :released_at # キャンセル時に在庫引き当てを解放した日時
t.string :status, null: false, default: 'active' # active / released
t.timestamps
end
# 同一注文で同一商品の重複引き当てを防ぐ
add_index :stock_reservations, [:order_id, :product_id],
unique: true,
where: "status = 'active'",
name: 'idx_active_reservations_uniqueness'
add_index :stock_reservations, :status
end
endテーブル名 stock_reservations(在庫引き当て)、カラム名 reserved_quantity(引き当て数量)、reserved_at(引き当て日時)、released_at(解放日時)——全てユビキタス言語に沿っている。
DBAとエンジニアが同じ用語でデータベースを議論できる。「stock_reservations テーブルの released_at がNULLのレコードが引き当て中の在庫です」という説明は、ビジネス担当者にも理解しやすい。
INFO
データベーススキーマにユビキタス言語を使うメリット: SQLを書く時もビジネス概念で考えられる。「アクティブな在庫引き当てを一覧する」という要件が、SELECT * FROM stock_reservations WHERE status = 'active' というSQLに自然に対応する。
APIレスポンスにも反映
フロントエンドや外部APIとのコントラクトにも同じ言語を使う。
// NG: 内部実装の言葉が漏れているAPIレスポンス
{
"ord_id": 12345,
"stat": 1,
"items": [
{ "itm_id": 999, "qty": 3, "prc": 200 }
]
}
// OK: ユビキタス言語を使ったAPIレスポンス
{
"order_id": "ord-12345",
"status": "confirmed",
"order_items": [
{
"product_id": "prod-999",
"product_name": "りんご 1kg",
"quantity": 3,
"unit_price": { "amount": 200, "currency": "JPY" }
}
],
"total_amount": { "amount": 600, "currency": "JPY" },
"confirmed_at": "2024-01-15T10:30:00+09:00"
}フロントエンドエンジニアとの会話でも「status が confirmed になったら確定メールを表示してください」と話せる。内部コードの話ではなく、ビジネスの話として。
ユビキタス言語の進化管理
言語は進化する。管理の方法を決めておく。
# FreshCart ユビキタス言語辞典
バージョン: 2.1
最終更新: 2024-03-01
オーナー: プロダクトチーム
レビュアー: エンジニアチームリード、ビジネスアナリスト
## 変更履歴
### v2.1 (2024-03-01)
- `StockAllocation` を `StockReservation` に改名
- 理由: 「割り当て」より「引き当て」がビジネス側の一般的な言い方
- 関連コード変更: PR #234
### v2.0 (2024-02-01)
- `item` → `product` に統一
- 理由: ドメインエキスパートが「商品」と言う時の英語は `product`
- `item` は「注文明細の一行」(`OrderItem`) の意味に限定
### v1.0 (2024-01-15)
- 初版作成バージョン管理されたドキュメントとして保持し、コードのPRと一緒にレビューする。用語が変わる際は、コード変更とドキュメント変更を同じPRに含める。
実践でよく起きる抵抗
リナがチームに「ユビキタス言語の統一」を提案した時、いくつかの抵抗があった。
「今のコードを全部書き直すの?」
→ 違う。新しくコードを書く時から使い始め、変更する機会があれば徐々に統一する。
「英語の命名は難しい」
→ まず日本語で合意してから英語化する。「在庫引き当て」→ StockReservation。ビジネス担当者の言葉を尊重する。
「チームによって言い方が違う」
→ それがユビキタス言語が必要な理由だ。全員が合意する一つの名前を選ぶ。
WARNING
ユビキタス言語の失敗パターン: 「エンジニアだけで決める」こと。ビジネス担当者が「その言葉は聞いたことない」という名前を使っても意味がない。必ずビジネス側と共同で作業する。
ユビキタス言語とAPIデザイン
ユビキタス言語はコードの内部だけでなく、外部に公開するAPIにも反映すべきだ。
# Bad: 内部実装の言葉がAPI仕様に漏れている
# POST /api/v1/orders
{
"status": 1, # 1が何か知らないとわからない
"item_refs": [...], # refsって何?
"usr_id": 123 # 省略系が意味不明
}
# Good: ユビキタス言語に基づいたAPI
# POST /api/v1/orders
{
"customer_id": 123,
"delivery_address": {
"postal_code": "150-0001",
"prefecture": "東京都",
"city": "渋谷区"
},
"order_items": [
{
"product_id": "PROD-001",
"quantity": 2
}
]
}APIレスポンスも同様だ。
# Bad: DB構造がそのまま出てしまっている
{
"id": 1,
"status_cd": 2, # status_cdって何?
"ttl_amt": 5400, # ttl_amtって?
"created_at": "..."
}
# Good: ビジネスの言葉で表現
{
"order_id": "ORD-20240315-001",
"order_status": "confirmed", # ステータスが明確
"total_amount": {
"amount_jpy": 5400, # 単位が明確
"formatted": "¥5,400"
},
"ordered_at": "2024-03-15T10:30:00+09:00" # ordered_at は注文日時
}INFO
外部APIはコードよりも変更が難しい。一度公開したAPIフィールド名を変えるとクライアントが壊れる。ユビキタス言語でAPI設計すれば、ビジネス用語との対応が取れたフィールド名になり、長期的に意味が変わりにくい。
DBスキーマへの反映
ユビキタス言語はDBのカラム名にも反映する。
# Bad: 短すぎる・意味不明なカラム名
create_table :orders do |t|
t.integer :uid # user_idのこと?unit_id?
t.integer :st # status? stock?
t.integer :ttl # total? title?
t.integer :typ # type? typography?
t.datetime :upd # updated_at?
end
# Good: ユビキタス言語に基づいたカラム名
create_table :orders do |t|
t.references :customer, null: false, foreign_key: true
t.string :status, null: false, default: 'pending'
t.integer :total_amount_cents, null: false, default: 0
t.string :currency, null: false, default: 'JPY'
t.datetime :confirmed_at
t.datetime :cancelled_at
t.timestamps
endスキーマを見ればビジネスの構造がわかる状態を目指す。
テストの名前もユビキタス言語で
テストの説明文も共通言語で書く。これにより、テストが「仕様書」として機能する。
# Bad: 技術的な記述のテスト
RSpec.describe Order do
describe '#confirm!' do
it 'updates status field to confirmed' do
order = create(:order, status: 'pending')
order.confirm!
expect(order.status).to eq('confirmed')
end
it 'calls Stock.decrement with correct args' do
# ...
end
end
end
# Good: ビジネスルールを記述したテスト
RSpec.describe OrderContext::Order do
describe '#confirm!' do
context '注文を確定する' do
it 'pending状態の注文が確定済みになる' do
order = build_pending_order_with_items
order.confirm!
expect(order.status).to eq(OrderContext::OrderStatus::CONFIRMED)
end
it '在庫がない場合は確定できない' do
order = build_pending_order_with_out_of_stock_items
expect { order.confirm! }.to raise_error(OrderContext::InsufficientStock)
end
it '既に確定済みの注文は再確定できない' do
order = build_confirmed_order
expect { order.confirm! }.to raise_error(OrderContext::InvalidTransition,
/confirmed状態の注文は確定できません/)
end
end
end
endテストの説明を読めば、そのコードが何のビジネスルールを実装しているかが一目でわかる。
リナの気づき
2週間後、リナとカズキは改善されたコードをレビューした。
「在庫引き当てのロジックを変えたい」とカズキが言った時、リナは即座に「 StockReservation クラスの reserve! メソッドと、注文確定時に呼ばれる reserve_stock! を変更することになります」と答えられた。
コードとビジネスが同じ言語で繋がった瞬間だった。
「こんなに変わるんだね」とカズキが言った。「コードを見せてもらう気になってきた」
それがユビキタス言語の本当の効果だ。エンジニアだけのものだったコードが、ビジネス担当者も関与できるものになる。
まとめ
- ユビキタス言語 = チーム全員が使う共通の語彙
- 適用範囲 = 会話・コード・テスト・DBスキーマ・APIレスポンス・ドキュメントのすべて
- 作り方 = エンジニアとビジネス担当者が共同でワークショップを開き合意する
- 維持方法 = バージョン管理された用語集、コード変更とセットで更新
- 効果 = 認知負荷の低下、バグの減少、チームコミュニケーションの改善
次の章では、ドメインを適切な単位に分割する「境界づけられたコンテキスト」を学ぶ。FreshCartの Product クラスが2000行になった謎が解けるはずだ。