Martin Fowler · 2002
Patterns of Enterprise Application Architecture 入門
業務アプリの設計で繰り返し現れる問題と、その定番の解決策(パターン)に名前をつけて整理したカタログ。初学者向けに「全体像・本質・カタログ・具体例」の順でまとめました。
原典カタログ: martinfowler.com/eaaCatalog / ここで命名されたパターン(Active Record, Repository, DTO, MVC, 楽観ロック…)は、今のフレームワークの設計語彙そのものです。
1
この本は何の本か(全体像)
「エンタープライズアプリケーション」とは、業務データを大量に扱い、長期間運用され、複数人で開発される業務システム(ECサイト、基幹システム、SaaS など)を指します。
この種のアプリには、技術が変わっても繰り返し現れる共通の設計問題があります。
業務ロジック(料金計算・在庫判定など)をどこに書くか
オブジェクトとリレーショナルDBの「形の違い」をどう埋めるか
Web画面の入力処理と表示をどう分けるか
ネットワーク越しの通信、同時編集の競合、セッション状態をどう扱うか
本書は、これらの問題に対する定番の解決策に名前をつけて整理したカタログ です。レシピ集のように「この問題にはこのパターン」と引けます。重要なのは、パターンは「正解」ではなく「選択肢とトレードオフ」 だという点。文脈に応じて選ぶための判断材料を与えてくれます。
本質:レイヤ化アーキテクチャ
本書を貫く最も重要な考え方がレイヤ化(Layering) です。アプリを責務ごとの「層」に分け、各層は自分の下の層だけに依存させます。
3
層
アーキテクチャ
関心ごとを層ごとに
分離することで、保守性・
拡張性・再利用性を高める
設計アプローチ。
THREE-TIER ARCHITECTURE
LAYER
01
01
Presentation(表示層)
→ Web Presentation Patterns
例: Controller, View
画面・API。ユーザーや外部システムとの入出力
LAYER
02
02
Domain(ドメイン層 / ビジネスロジック)
→ Domain Logic Patterns
例: Domain Model, Service Layer
業務ルールの本体。アプリの価値の中心
LAYER
03
03
Data Source(データソース層)
→ Data Source / O-R Mapping
例: Active Record, Data Mapper, Repository
DBや外部システムとのやり取り
層を分ける理由は、変更の影響を局所化する ため。画面のデザインを変えても業務ロジックは壊れず、DBのテーブルを変えても画面コードに波及しません。本書のパターンの大半は、この3層のどこかに属するか、層と層の境界(分散・並行性・セッション)を扱うものとして位置づけられます。
本書のパターンの地図
大分類 カテゴリ 解決する問題
ドメイン層 Domain Logic 業務ロジックをどう構造化するか
データソース層 Data Source Architectural オブジェクトとDBをどう橋渡しするか
データソース層 O-R Behavioral DBアクセスの効率と一貫性をどう保つか
データソース層 O-R Structural 関連・継承・値をテーブルにどうマッピングするか
データソース層 O-R Metadata Mapping マッピングをどう汎用化・自動化するか
表示層 Web Presentation 入力処理と画面表示をどう分けるか
層の境界 Distribution ネットワーク越しの呼び出しをどう設計するか
層の境界 Offline Concurrency トランザクションをまたぐ競合をどう防ぐか
層の境界 Session State ユーザーの一連の状態をどこに置くか
全層共通 Base 他パターンの土台となる小さな部品群
以降、カテゴリごとに各パターンを「一言で / 使いどころ / 具体例 / トレードオフ」で解説します。最初は「一言で」だけ拾い読み し、気になったものを深掘りするのがおすすめです。
2
Domain Logic Patterns(ドメインロジック)
ビジネスロジック(料金計算・在庫判定など)をコードのどこにどう配置するか を扱う。置き場所を誤ると、画面表示やDBアクセスに業務ルールが混ざり、変更に弱く重複だらけになる。
選び方の軸はシンプル。Transaction Script と Domain Model は「複雑さのスペクトラム」の両端 にあります。ロジックが単純なうちは手続き型の Transaction Script が速くて分かりやすく、業務ルールが複雑に絡み合うほどオブジェクト指向の Domain Model が保守性で勝ります。Table Module はその中間 。そして Service Layer はこれら3つのいずれとも組み合わせて使える別レイヤー で、アプリへの入口(操作の一覧)を定義し、トランザクション管理や複数オブジェクトの調整役を担います。
Transaction Script
一言で 1つの要求に対する処理を、1本の手続き(メソッド)としてまとめて書く。
使いどころ 「注文を確定する」「送料を計算する」といった操作ごとに処理を上から下へ順番に書くだけなので直感的で学習コストが低い。ロジックが単純な小規模アプリ、業務ルールがほとんど絡み合わないケースに向く。
具体例 【場面】ECサイトで「注文確定」ボタンが押されたとき、在庫を減らし合計金額を計算して注文を保存する一連の処理を、1つの手続きにまとめて書く。
def place_order(user_id, items)
total = items.sum { |i| i[:price] * i[:quantity] }
items.each do |i|
stock = Stock.find_by!(product_id: i[:product_id])
stock.decrement!(:quantity, i[:quantity])
end
Order.create!(user_id: user_id, total: total)
end
注意点 ルールが増えると似た手続きが量産され、共通ロジックの重複が起きやすい。複雑化したら Domain Model への移行を検討する。
Domain Model
一言で 業務の概念(注文・商品・顧客など)をオブジェクトとして表現し、データと振る舞い(ロジック)を一体で持たせる。
使いどころ 業務ルールが複雑で相互に絡み合う場合、ロジックを関連オブジェクトに分散させて重複を抑え、現実の業務概念とコードの構造を一致させられる。ルールが頻繁に変わる中〜大規模システムに向く。
具体例 【場面】同じECの注文だが、「合計金額の計算」や「割引の適用」といったルールを、手続きではなく Order / OrderLine といったオブジェクト自身に持たせる。データとふるまいが同じ場所にある。
class Order
def initialize(lines, customer)
@lines = lines
@customer = customer
end
def total
subtotal - discount
end
def subtotal
@lines.sum(&:amount)
end
def discount
@customer.vip? ? subtotal * 0.1 : 0
end
end
class OrderLine
def amount = @price * @quantity
end
注意点 オブジェクト設計やO/Rマッピングの学習コスト・初期コストが高い。単純なアプリにはオーバースペックになりやすい。
Table Module
一言で 1つのDBテーブルに対応するクラスを1つ作り、そのテーブルの全行分のビジネスロジックをそこに集約する。
使いどころ Domain Model が「1注文=1オブジェクト」なのに対し、Table Module は「注文テーブル全体=1オブジェクト」。表形式データを前面に出す環境(.NETのDataSetなど)と相性がよく、Transaction Script より構造化しつつ Domain Model ほど作り込まないバランス点。
具体例 【場面】.NETで注文テーブル全体を1つのDataSetとして読み込み、「注文という"テーブル"全体」を扱うクラスを1つ用意する。重要なのは 1行ごとにオブジェクトを作らない(行のidentityを持たない) こと。行(1件)ではなくテーブル単位でふるまいを持ち、引数で対象行のIDを渡す。
public class OrderModule {
private DataTable table; // 注文テーブル全体を保持
public decimal CalculateDiscount(long orderId) {
DataRow row = table.Rows.Find(orderId);
decimal total = (decimal)row["total"];
return total > 10000 ? total * 0.1m : 0;
}
}
注意点 ロジックが「行」ではなくテーブル単位のクラスに属するため、複雑な業務ルールの表現力は Domain Model に劣る。
Service Layer
一言で アプリが外部に提供する操作の一覧を「サービス」の層として定義し、アプリ境界(入口)を明確にする。
使いどころ 画面・API・バッチなど複数の呼び出し元から同じ業務操作を使いたいとき、ロジックの入口を1か所に集約できる。トランザクションの開始・終了や、複数オブジェクト/外部システムの呼び出し順序の調整(オーケストレーション)を担う。上記3パターンのいずれの上にも乗せられる。
具体例 【場面】Web画面・スマホアプリ・バッチの3経路から「注文キャンセル」を呼びたい。ドメインモデル(在庫・注文)の操作とトランザクション・通知の制御を OrderService にまとめ、各UIはこれを呼ぶだけにする。
class OrderService
def cancel_order(order_id)
ActiveRecord::Base.transaction do
order = Order.find(order_id)
order.cancel # ドメインモデルのふるまい
order.restore_stock # 在庫を戻す
Mailer.notify_cancel(order).deliver_later
end
end
end
注意点 設計を誤るとサービス層に業務ルールまで吸い込まれ、ドメイン層が貧弱な「手続きの寄せ集め」になりがち。ロジック本体はドメイン側に置き、サービス層は調整役に徹するのが基本。
3
Data Source Architectural Patterns
ドメインのオブジェクトとDBの間で、データの読み書きをどう橋渡しするか 。オブジェクトとテーブル/行の「形の違い」(インピーダンス・ミスマッチ)を埋める。最重要は Active Record (オブジェクト自身が永続化を持つ=Rails)と Data Mapper (専任のマッパーが変換=Hibernate/Doctrine)の違い。
Table Data Gateway
一言で 1つのテーブル全体への入り口となるオブジェクト。1インスタンスがそのテーブルの全行を担当する。
使いどころ SQLをドメインコードに散らばらせたくないとき、テーブルごとに「データアクセス窓口」を1つ用意してSQLを集約する。行をオブジェクトに変換せず、生のレコードセットのまま扱いたい場面に向く。
具体例 【場面】ユーザー一覧画面で、SQLをあちこちに散らさず「usersテーブル専用の窓口」を1つ作りたい。1インスタンスがテーブル全体を担当し、行はデータ(配列やハッシュ)として返す。
class UserGateway
def find(id)
DB.query("SELECT * FROM users WHERE id = ?", id) # 1行ぶんのHashを返す
end
def find_by_department(dept)
DB.query("SELECT * FROM users WHERE department = ?", dept) # 複数行
end
end
注意点 戻り値が単なるレコードの集合になりがちで、その行に対する振る舞い(ドメインロジック)を持たせる場所がない。
Row Data Gateway
一言で データソースの1レコードへの入り口となるオブジェクト。1行につき1インスタンスを作る。
使いどころ テーブル単位ではなく「1行=1オブジェクト」として扱いたいとき、各行のカラムをそのままフィールドに持つオブジェクトを用意する。
具体例 【場面】Table Data Gatewayと違い「1行=1オブジェクト」にしたい。各カラムをフィールドに持ち、DBアクセス専用。業務ロジックは持たず、保存・更新だけを担う。
class UserRow
attr_accessor :id, :name, :email
def insert
DB.execute("INSERT INTO users(name, email) VALUES(?, ?)", name, email)
self.id = DB.last_insert_id
end
def update
DB.execute("UPDATE users SET name=?, email=? WHERE id=?", name, email, id)
end
end
注意点 見た目はActive Recordに似るが、あくまでDBアクセス担当に徹し、ドメインロジックを持たないのが分かれ目。ロジックを足したくなった瞬間にActive Recordへ寄っていく。
Active Record
一言で DBの1行をラップし、DBアクセスのコードに加えてその行に対するドメインロジックも持つオブジェクト。
使いどころ ドメインロジックがそれほど複雑でなく、おおむねテーブル構造とオブジェクト構造が一致しているとき、「データ+振る舞い+永続化」を1クラスにまとめると一気に開発が速くなる。Railsアプリの大半がこのスタイル。
具体例 【場面】Railsそのもの。1行を表すオブジェクトが、DBアクセス(保存・検索)も業務ロジックも両方持つ。データと振る舞いが同じクラスに同居するのが特徴。
class User < ApplicationRecord
# 検索・保存はクラス/インスタンスが自分で行う
def self.active = where(status: "active")
# 業務ロジックも同じクラスに同居する
def deactivate!
update!(status: "inactive")
end
end
user = User.find(1)
user.deactivate!
注意点 手軽な反面、オブジェクトの構造がテーブル構造に強く縛られる。ドメインモデルが複雑になりDBと形がずれると、無理が出てData Mapperが欲しくなる。
Data Mapper
一言で オブジェクトとDBの間でデータを移し替える専任の「マッパー」層。両者を互いに無知なまま独立させる。
使いどころ ドメインモデルが複雑で、オブジェクトの設計をDBスキーマから完全に切り離したいときに使う。ドメインオブジェクトはDBの存在すら知らず、保存・読み込みはすべてマッパー担当。純粋なオブジェクト指向設計を保てる。HibernateやDoctrineが代表例。
具体例 【場面】ドメインオブジェクト(User)をDBの存在から完全に切り離したい。UserはSQLを一切知らず、別のMapperクラスがオブジェクトとテーブルの橋渡しをする。永続化の都合がドメインに漏れない。
// ドメインオブジェクトはDBを知らない(ただのデータと振る舞い)
class User {
private int id;
private String name;
}
// マッパーが変換と永続化を担う
class UserMapper {
User findById(int id) { /* SELECT して User に詰め替える */ }
void insert(User user) { /* User から INSERT 文を組み立てる */ }
}
注意点 ドメインとDBを疎結合にできる反面、マッパー層の実装コストが高く、小規模アプリではオーバースペックになりがち。
4
Object-Relational Behavioral Patterns
メモリ上のオブジェクトとDBの間で、DBアクセスの効率と一貫性をどう保つか 。3つのパターンは「書き込みの取りまとめ」「重複読み込みの防止」「読み込みの先送り」で対処する。いずれもORMの中核機能として裏で働いている。
Unit of Work
一言で 1つの業務トランザクションで変更されたオブジェクトを記録し、まとめてDBへ書き出し、並行性の問題も調整する仕組み。
使いどころ 変更のたびに毎回DBへ書くとSQL回数が増え、トランザクション境界もぼやける。「どれが新規・変更・削除か」を覚えておき、最後の commit で一括反映して効率と一貫性を両立する。
具体例 【場面】1リクエスト中に複数オブジェクトを変更したが、毎回バラバラにUPDATEを投げたくない。変更を「やることリスト」に溜め込み、最後にまとめてトランザクションでコミットする。
uow = UnitOfWork.new
uow.register_new(order) # 追加予定として記録するだけ
uow.register_dirty(customer) # 更新予定として記録するだけ
uow.register_removed(coupon) # 削除予定として記録するだけ
# ここまでDBアクセスは発生しない
uow.commit # 溜めた変更を1トランザクションでまとめて書き込む
RailsやHibernateでは、トランザクション内の変更追跡として裏で同様に働く。
注意点 コミットまで書き込みが遅延されるため、書き出しの順序(外部キー制約など)を正しく解決する仕組みが必要。
Identity Map
一言で 読み込んだオブジェクトをマップに保持し、各オブジェクトが一度だけ読み込まれることを保証する。
使いどころ 同じレコードを別々の場所で読み込むと、メモリ上に別オブジェクトが2つでき、片方の変更が反映されず不整合が起きる。IDをキーにマップへ覚えておき、次回からは同じインスタンスを返して防ぐ。原著は一貫性(正しさ)と性能の両面 で重視しており、同じデータを2度DBから読まずに済むためクエリやリモート呼び出しの削減にもなる。
具体例 【場面】同じリクエスト内で User.find(1) を2回呼んだら、DBに2回問い合わせず、1回目に読んだオブジェクトを使い回したい。「読み込み済みオブジェクトの台帳」を持ち、同じ行は常に同一インスタンスを返す。
def find(id)
return identity_map.get(id) if identity_map.get(id) # 台帳にあれば再クエリせず同じものを返す
obj = load_from_db(id)
identity_map.put(id, obj) # 初回はDBから読み、台帳に登録
obj
end
a = repo.find(1)
b = repo.find(1)
a.equal?(b) # => true(同一インスタンス。DBアクセスは1回だけ)
注意点 通常は1リクエスト単位で持つ。長く生かしすぎると、他プロセスがDBを更新した変更を見逃した古いデータを返す。
Lazy Load
一言で 必要なデータをまだ持っていないが、聞かれたら取りに行く方法を知っているオブジェクト。
使いどころ 読み込むたびに関連オブジェクトまで芋づる式に全部読むと無駄なDBアクセスが膨らむ。実際にアクセスされた瞬間に初めて関連データを読みに行く。
具体例 【場面】ブログ記事10件と各著者名を表示する画面。著者を「使うときに初めて読む」素朴な実装だと、記事ごとに著者クエリが飛び、1+10回のN+1問題になる。必要な関連を先読み(eager load)してまとめて取得する。
# before: 各 post.author でその都度クエリ → 1 + 10 回
posts = Post.limit(10)
posts.each { |post| puts post.author.name }
# after: 著者をまとめて先読み → 2 回(posts と authors)
posts = Post.includes(:author).limit(10)
posts.each { |post| puts post.author.name }
RailsのアソシエーションやHibernateのlazy fetchがこの仕組み。
注意点 ループの中で関連を遅延読み込みすると「1件ずつ追加クエリが飛ぶ」N+1問題 を招きやすく、まとめて先読み(eager load)すべき場面の見極めが要る。
5
Object-Relational Structural Patterns
オブジェクトの構造(関連・継承・値 )を、テーブル・行・列・外部キーしか持たないリレーショナルDBにどう対応づけるか。「関連を外部キー/中間テーブルに」「継承をテーブルに」「値オブジェクトを列に」という定石。
Identity Field
一言で オブジェクトに、対応するDB行の主キーを保持させて、メモリ上のオブジェクトとDBの行を結びつける。
使いどころ オブジェクトはメモリアドレスで、DBの行は主キーで識別される。両者を往復させるには、オブジェクト側に主キー(id)を持たせて橋渡しする。ほぼすべてのO/Rマッピングの土台。
具体例 【場面】メモリ上の User オブジェクトと、DBの行を結びつけたい。テーブルの主キー値(id)をオブジェクトのフィールドとして持たせる。Railsの id カラムや User.find(42) がこれ。
usersテーブル User オブジェクト
id | name user.id == 1 ← 主キー値を保持(Identity Field)
---+------ user.name == "田中"
1 | 田中
注意点 自然キー(メールアドレス等)より、意味を持たない代理キー(連番やUUID)の方が変更に強い。
Foreign Key Mapping
一言で オブジェクト間の参照(1対1・1対多)を、テーブルの外部キー列で表現する。
使いどころ Order が Customer を参照するような単純な参照関係。参照先の主キーを参照元の列に持たせる。
具体例 【場面】「1人の社員は1つの部署に所属する」関係を、employees テーブルの department_id(外部キー)で表現し、オブジェクト側では employee.department という参照に変換する。
employeesテーブル departmentsテーブル
id | name | department_id id | name
---+------+-------------- ---+--------
1 | 田中 | 10 ---> 10 | 営業部
employee.department #=> Department(id: 10)
注意点 多対多の関係はこの方法では表現できない(Association Table Mapping が必要)。
Association Table Mapping
一言で 多対多の関連を、両者の主キーを持つ専用の中間テーブルで表現する。
使いどころ 「社員は複数のスキルを持ち、スキルは複数の社員に共有される」のような多対多は外部キー列だけでは表せない。間に「組み合わせ」を記録するテーブルを置く。
具体例 【場面】「学生は複数の講義をとり、講義は複数の学生が受ける」多対多関係を、中間テーブル enrollments(学生IDと講義IDの組)で表現する。
studentsテーブル enrollmentsテーブル coursesテーブル
id | name student_id | course_id id | title
---+----- -----------+---------- ---+--------
1 | 佐藤 <-- 1 | 20 --> 20 | 数学
1 | 21 21 | 物理
Railsの has_many :through / has_and_belongs_to_many がこれ。
注意点 関連自体に属性(取得日など)を持たせたい場合、中間テーブルが独立したエンティティに育つことが多い。
Dependent Mapping
一言で 単独では存在しない従属オブジェクトの読み書きを、親オブジェクトに任せる。
使いどころ アルバムの「曲(Track)」のように、必ず親(Album)に紐づき親経由でしかアクセスされない子。子に独立したマッパーを用意せず、親がまとめてロード・保存する。
具体例 【場面】「注文(Order)」と、それにぶら下がる「注文明細(LineItem)」。明細は注文なしには存在せず、自分でDBアクセスせず親のOrderにロード・保存を任せる。
class Order
# 明細の読み書きはOrderが一括で面倒を見る(LineItemは自分でDB操作しない)
def load_line_items
rows = db.query("SELECT * FROM line_items WHERE order_id = ?", id)
@line_items = rows.map { |r| LineItem.new(r) }
end
end
注意点 従属オブジェクトを他から参照したくなった瞬間に破綻する。所有者がただ1つで、子のライフサイクルが親に完全従属する場合に限る。
Embedded Value
一言で 小さな値オブジェクトを、専用テーブルにせず親テーブルの複数の列に展開して埋め込む。
使いどころ Money(amount, currency) や DateRange(start, end) のような値オブジェクトを別テーブルにすると結合が増えて面倒。親の行に列として平らに展開する。
具体例 【場面】Customer が持つ Address という値オブジェクトを、専用テーブルに分けず、customers テーブルの zip/prefecture/city カラムに展開して埋め込む。読み込み時にまた Address に組み立て直す。
customersテーブル オブジェクト側
id | name | zip | prefecture customer.address
---+------+---------+----------- => Address(zip:..., prefecture:...)
1 | 田中 | 1000001 | 東京都
注意点 値オブジェクト単位で検索・集計したい場合は列で表現できて好都合。構造が複雑・可変なら次の Serialized LOB が向く。
Serialized LOB
一言で オブジェクトのまとまりを直列化し、1つの大きな列(LOB: Large OBject)にまとめて保存する。直列化先はテキスト(CLOB=JSON・XMLなど。可読・可搬)かバイナリ(BLOB=コンパクトで速いが中身が読めず可搬性に劣る)を選べる。
使いどころ 細かい構造を全部列にマッピングするのが大変で、かつDB側で個別検索する必要のないデータ(設定値、履歴スナップショット、複雑なグラフ構造)に向く。
具体例 【場面】ユーザーの細かな設定(通知ON/OFF、テーマ色など)を1カラムずつ作らず、まとめてJSON文字列にして settings という1カラム(テキスト型)に保存する。Railsの serialize や jsonb カラム。
usersテーブル
id | name | settings
---+------+------------------------------------
1 | 田中 | {"theme":"dark","notify":true} ← JSONを丸ごと1カラムに
注意点 保存・読み込みは楽だが、中身のキーでSQL検索・集計・結合ができない(DBから見ると不透明な塊)。
継承の3パターン
オブジェクトの継承ツリーをテーブルに落とす方法が3つあり、本書の中でも対比が分かりやすい部分です。
Single Table Inheritance
一言で 継承ツリーのすべてのクラスを、1つのテーブルにまとめて格納する。
具体例 【場面】Player を親に、サッカー選手 Footballer(所属クラブを持つ)とクリケット選手 Cricketer(打率を持つ)がある。これを1つの players テーブルにまとめ、type 列でどのクラスかを区別する。使わない列はNULLになる。Rails の STI がこれ。
players(1テーブルに全種類)
id | type | name | club | batting_avg
---+-------------+------+---------+------------
1 | Footballer | 田中 | FC東京 | (NULL)
2 | Cricketer | 鈴木 | (NULL) | 35.2
注意点 結合不要で最速だが、無関係な列がNULLだらけになりテーブルが横に広くなる。
Class Table Inheritance
一言で クラスごとにテーブルを作り、共通部分は親、固有部分は子に置いて結合で復元する。
具体例 【場面】共通項目は親 players テーブル、各クラス固有の項目は子テーブルに分け、同じ id で結合して1人を復元する。
players(共通) footballers(固有) cricketers(固有)
id | name id | club id | batting_avg
---+----- ---+------ ---+-----------
1 | 田中 <同id> 1 | FC東京
# Footballer 1人を得るには players と footballers をJOINする
注意点 正規化されてきれいだが、1オブジェクトの読み書きに複数テーブルの結合が必要で遅くなりがち。
Concrete Table Inheritance
一言で 抽象的な親は持たず、具象クラスごとに共通列も含めた独立テーブルを作る。
具体例 【場面】共通の親テーブルを作らず、具象クラスごとに完結した独立テーブルを持つ。各テーブルが共通項目 name も自前で持つ。
footballers(完結) cricketers(完結)
id | name | club id | name | batting_avg
---+------+------ ---+------+-----------
1 | 田中 | FC東京 1 | 鈴木 | 35.2
# name を各テーブルが個別に持つ/JOIN不要で速い
注意点 共通列が各テーブルに重複し、親型でまとめて検索するには全テーブルをUNIONする必要がある。
継承パターン比較
観点 Single Table Class Table Concrete Table
テーブル数 1つ 親 + 子それぞれ 具象クラスごと
結合(JOIN) 不要(速い) 必要(遅い) 不要(速い)
NULL列 多い なし なし
列の重複 なし なし 共通列が重複
親型でまとめて検索 容易 可能だが要注意 ※ 困難(UNION必要)
※ Class Table の「親型でまとめて検索」は、どのクラスか事前に分からないポリモーフィックな読み込みだと複数の子テーブルへのJOIN/クエリが必要になり、性能上は Concrete Table の UNION に近い負荷がかかる。
迷ったらまず Single Table がシンプル。NULLが気になり正規化したいなら Class Table、各クラスを独立に最速で扱うなら Concrete Table。
Inheritance Mappers
一言で 上記の継承マッピング処理を、継承構造に沿ったマッパークラス群で整理する設計。
使いどころ 継承をDBに保存する処理はサブクラスごとに重複コードになりがち。マッパー自身も継承階層を持たせ、共通処理を親マッパーに、固有処理を子マッパーに置いて整理する。
具体例 【場面】継承階層のマッピングを、クラスごとのMapperに分け、共通のロード・保存は親Mapperにまとめる。Mapper側もオブジェクト側と並行した継承構造を持つ。
class AbstractPlayerMapper # 共通のSQL組み立て・ロード/保存
def load(row); ...; end
end
class FootballerMapper < AbstractPlayerMapper # Footballer固有部分だけ実装
end
注意点 マッパー階層が増えて構造が複雑になる。単純なケースでは過剰。
6
O-R Metadata Mapping Patterns
オブジェクトとテーブルの対応づけを、コードにベタ書きせず「設定(メタデータ)」として外に切り出し 、O/Rマッピングを汎用化・自動化する。Repository は現代のアプリで最も広く使われる。
Metadata Mapping
一言で 「どのクラスのどの属性が、どのテーブルのどの列に対応するか」をメタデータとして持ち、マッピング処理を自動生成する。
使いどころ テーブルごとに手書きのマッピングコードを書くのは退屈で間違いやすい。対応関係をメタデータ(設定ファイル・規約・アノテーション)にまとめ、SQL生成や読み書きを共通の仕組みに任せる。ORMの心臓部。
具体例 【場面】「User クラスの name フィールドは users テーブルの name 列」という対応表(メタデータ)を持ち、それを読んでマッピングを自動生成する。ActiveRecordがDBスキーマを読んで属性を生やすのがこの発想。
mapping = {
User => { table: "users",
columns: { name: "name", email: "email_address" } }
}
# この表を解釈してSQLとオブジェクトを自動変換する(フィールドが増えても手書きしない)
注意点 コード量が激減する反面、内部が「魔法」のようになり、性能問題や意図しないクエリの原因を追いにくくなる。
Query Object
一言で データベースへの問い合わせ条件を、SQL文字列ではなくオブジェクトとして表現する。
使いどころ あちこちに生のSQLが散らばると保守しづらい。検索条件をオブジェクトとして組み立て、最後にSQLへ変換することで、オブジェクトの言葉で検索を書ける。
具体例 【場面】「東京在住で過去30日に購入したユーザー」のような検索条件を、SQL文字列を散らかさずオブジェクトに組み立て、最後にまとめてSQLへ変換する。条件を部品のように足していける。
query = UserQuery.new
.where(:prefecture, "=", "東京都")
.where(:purchased_at, ">", 30.days.ago)
users = query.execute # ここで初めてSQLに変換して実行
ActiveRecord の User.where(active: true).where("age > ?", 20) のメソッドチェーンがこの考え方。
注意点 DB構造を隠蔽でき再利用しやすいが、複雑なクエリ表現を組むためのオブジェクト構造を作り込むコストがかかる。
Repository
一言で オブジェクトの集合(コレクション)のように振る舞う窓口を用意し、その裏でDBアクセスを隠蔽する。
使いどころ ビジネスロジックから「DBに問い合わせている」感をなくしたいとき。repository.find_active_users のように、メモリ上のコレクションを操作するインターフェースを提供し、内部では Query Object などでアクセスする。永続化の詳細をドメイン層から切り離せる。
具体例 【場面】呼び出し側はSQLを意識せず、user_repository.find_active_users のようにコレクションを操作する感覚でデータを取り出す。裏側のDBアクセス(Query Objectやマッパー)はRepositoryの中に隠れる。
class UserRepository
def find_active_users
# 中ではQuery ObjectやSQLを使うが、呼び出し側からは隠れている
UserQuery.new.where(:active, "=", true).execute
end
end
users = UserRepository.new.find_active_users
注意点 ドメインロジックと永続化を明確に分離でき、DDD の文脈で現代のアプリケーションでは最も広く採用されている 。一方、ActiveRecord方式に比べるとクラスやレイヤーが増え、小規模なアプリではかえって冗長になりやすい。
7
Web Presentation Patterns
Web画面の入力処理(Controller)と表示(View)の責務分割 。大枠を与えるのが MVC。あとは「Controllerをどう配置するか」(Page ↔ Front)と「Viewをどう組み立てるか」(Template ↔ Transform)の2軸の具体化。
Model View Controller (MVC)
一言で アプリを「データとロジック(Model)」「表示(View)」「入力処理・制御(Controller)」の3役割に分けて疎結合に保つ、最も基本的な構成。
使いどころ 表示ロジックと業務ロジックが混ざると、画面変更のたびにロジックを壊すリスクが生まれる。役割を分け、同じデータを複数画面で見せたり、デザイン変更とロジック変更を独立して進めたりできる。
具体例 【場面】会員一覧画面で「DBから会員を取りに行く」「画面を組み立てる」「リクエストを受けて両者をつなぐ」を別々のファイルが担当する。Railsの3ディレクトリがそのまま3役割に対応する。
# app/controllers(Controller:入力を受け、Modelを呼び、Viewへ渡す)
class UsersController < ApplicationController
def index
@users = User.active # Modelに問い合わせ → views/users/index.html.erb が表示
end
end
# app/models(Model:データと業務ロジック)
class User < ApplicationRecord
scope :active, -> { where(active: true) }
end
注意点 「3つに分ける」大方針にすぎず、ControllerやViewの具体的な置き方までは決めてくれない。それを補うのが以下のパターン。
Page Controller
一言で Webページ(または1つのアクション)ごとに、専用のControllerを1つ用意する。
使いどころ 「この画面が来たらこう処理する」が分かりやすく、画面とコードが1対1で結びつくため、小〜中規模で直感的に作りたいときに向く。
具体例 【場面】「会員詳細を表示」「会員を更新」など画面・操作ごとに、専用の窓口(アクション)を1つずつ用意する。routes.rbのURLと、コントローラのメソッドが1対1で結びつく。
# config/routes.rb(URLごとに窓口を割り当てる)
get "/users/:id", to: "users#show"
patch "/users/:id", to: "users#update"
# 画面・操作ごとに1メソッド=1窓口
class UsersController < ApplicationController
def show
@user = User.find(params[:id]) # この画面専用の処理
end
end
注意点 認証チェックや共通ヘッダ処理など、全ページ共通の処理が各Controllerに重複しがち。
Front Controller
一言で すべてのリクエストをまず1つの入口で受け止め、そこから適切な処理へ振り分ける。
使いどころ 認証・ログ記録・ルーティングといった共通処理を入口に集約できる。共通処理が多い場合や、URLと処理の対応を柔軟に変えたい大規模アプリで効果を発揮する。
具体例 【場面】どのURLへのリクエストも、まず1つの入口が受け止め、認証やログを通したうえで担当アクションへ振り分ける。Railsではルーター(ディスパッチャ)が入口にあたり、共通処理は before_action に集約する。
# config/routes.rb:全リクエストはまずこのルーターが受け、振り分ける
Rails.application.routes.draw do
resources :users # /users/:id → 該当アクションへディスパッチ
end
# 共通処理(認証・ログ)は1か所に集約してから各処理へ流す
class ApplicationController < ActionController::Base
before_action :authenticate! # 全リクエスト共通の入口処理
end
Spring の DispatcherServlet も同じ役割。
対比 Page Controllerは「画面ごとに窓口を分散」、Front Controllerは「窓口を1つに集約して中で振り分け」という正反対の発想。実際のフレームワークは、Front Controllerで一括受付しつつ内部でPage Controller的なアクションへ渡す組み合わせが主流。
Template View
一言で 完成形に近いHTMLのひな型を用意し、埋め込み箇所に動的な値を差し込んで画面を作る。
使いどころ 最終的な見た目を見ながらマークアップを書けるため直感的。HTMLが主体で、変化する部分だけを動的にしたい一般的な画面に向く。
具体例 【場面】会員詳細ページのHTMLを先に完成形に近い形で書き、名前や登録日など変化する箇所だけ差し込み口(<%= %>)を開けておく。デザインを見ながらマークアップできる。
<!-- app/views/users/show.html.erb(HTMLが主役、穴に値を埋める) -->
<h1><%= @user.name %> さん</h1>
<p>登録日:<%= @user.created_at.to_date %></p>
<% if @user.active? %>
<span class="badge">利用中</span>
<% end %>
Rails の ERB / Haml、Spring の Thymeleaf がこれ。
注意点 ひな型の中に分岐やループを書きすぎると、表示と処理が混ざって読みにくくなる。ロジックは控えめに。
Transform View
一言で 入力データを1要素ずつ受け取り、変換ルールに従って出力(HTMLなど)へ「変換」していく。
使いどころ ひな型起点ではなくデータ起点で「このデータが来たらこう出力する」変換規則を積み上げて画面を作る。複数の出力フォーマットへ同じデータを変換したい場合に向く。
具体例 【場面】スマホアプリ向けに会員情報をJSONで返したい。HTMLのひな型ではなく「このデータが来たら、このキーでこう出力する」という変換規則をデータ起点で書き並べる。RailsのJbuilderが近い。
# app/views/users/show.json.jbuilder(@user を1要素ずつ出力へ変換)
json.id @user.id
json.name @user.name
json.orders @user.orders do |order|
json.total order.total_price # 子要素も同じ要領で変換規則を積み上げる
end
古典的にはXMLをXSLTでHTML変換する方式が代表例。
対比 Template Viewは「ひな型に穴を空けて値を埋める(見た目が主役)」、Transform Viewは「データを変換規則で出力へ作り変える(データと変換が主役)」という出発点が逆の関係。
Two Step View
一言で 画面生成を「①論理的な画面構造を組み立てる」「②それを実際のHTMLへ変換する」の2段階に分ける。
使いどころ いったん抽象的な画面構造を作り、後段で統一的にHTML化する。サイト全体の見た目を1か所で一括変更したいときに効果的。
具体例 【場面】各画面は「中身」だけを書き、ヘッダ・フッタ・全体の枠は共通レイアウトが受け持つ。第1段で中身を組み、第2段で共通レイアウトに流し込んで最終HTMLにする。レイアウトを差し替えれば全画面の見た目を一括変更できる。
<!-- 第2段:全画面共通の枠 app/views/layouts/application.html.erb -->
<html>
<body>
<header>共通ヘッダ</header>
<%= yield %> <!-- 第1段で組んだ各画面の中身が流し込まれる -->
</body>
</html>
<!-- 第1段:この画面の中身だけ app/views/users/show.html.erb -->
<h1><%= @user.name %></h1>
テーマ切り替えやマルチブランド対応にも応用できる。
注意点 2段階に分けるぶん仕組みが複雑。特定の1画面だけ特別な見た目にしたい場合は扱いにくい。
Application Controller
一言で 「次にどの画面・どの処理へ進むか」という画面遷移やフローの判断を、一手に引き受ける専用の制御役。
使いどころ 入力受付(Front/Page Controller)とは別に、「状態がこうなら次はこの画面」というアプリ全体の流れを1か所に集約する。複数画面にまたがる入力ウィザードや、状態で遷移先が変わる申請フローに向く。
具体例 【場面】住所入力→確認→完了の3ステップ登録ウィザードで、「今どのステップか・次はどこへ進むか」を各アクションに散らさず、1か所のフロー定義に集約する。状態と次の遷移先の対応表(ステートマシン)として持つ。
# 「現在の状態 → 次の画面」の対応を1か所に集約
class RegistrationFlow
STEPS = { address: :confirm, confirm: :done } # どのステップの次はどこか
def next_step(current) = STEPS.fetch(current) # 遷移先の判断はここだけが知る
end
# 各アクションは「次どこ?」をこのフローに尋ねるだけ
redirect_to step_path(flow.next_step(:address)) # → confirm へ
注意点 画面遷移が単純なアプリでは不要で過剰。遷移ロジックが各Controllerに散らばって管理しきれなくなったときに検討するとよい。
8
Distribution Patterns
大原則は 「ネットワーク越しの呼び出しは、メモリ内のメソッド呼び出しに比べて桁違いに遅い」 。ゆえに 呼び出し回数を減らす=1回でまとめてやり取りする「粗粒度」な設計 が鉄則。
Remote Facade
一言で 細かいオブジェクト群の前に、ネットワーク越し用の「まとめ役」となる粗粒度の窓口を置く。
使いどころ 小さなオブジェクトの細かいメソッドをそのままネットワーク越しに呼ぶと往復回数が爆発して遅い。複数の細かい操作を1回の呼び出しにまとめるファサードを用意して通信回数を減らす。
具体例 【場面】スマホアプリから「注文画面に必要な情報(注文・顧客・配送先)」を取りたい。細かいgetterを1回ずつ呼ぶと往復が増えるので、1回でまとめて返す粗い窓口を用意する。
# NG: 1項目ごとに往復が発生する
api.get_order_id(42); api.get_customer_name(42); api.get_shipping_address(42)
# Remote Facade: 1回の呼び出しで画面に必要な分をまとめて返す
class OrderFacade
def order_summary(order_id)
order = Order.find(order_id)
{ id: order.id,
customer_name: order.customer.name,
shipping_address: order.shipping_address.to_s }
end
end
Web API のエンドポイント設計そのもの。
注意点 ファサードはあくまで「通信の窓口」であり、業務ロジックを持たせないのが原則。
Data Transfer Object (DTO)
一言で プロセス間でデータを運ぶためだけの、まとめて詰め込める入れ物オブジェクト。
使いどころ Remote Facade で「1回でまとめて返す」ために、複数の値を1つにまとめた運搬用オブジェクトが必要。DTO はロジックを持たず、データの保持・転送だけに使う。
具体例 【場面】サーバーからクライアントへ、注文1件分のデータをまとめて運ぶ入れ物。1回のAPIレスポンスで、関連する顧客名や明細まで1つのJSONに詰めて返す。
{
"order_id": 42,
"customer_name": "佐藤太郎",
"total": 12800,
"line_items": [
{ "product_name": "Tシャツ", "quantity": 2, "price": 3900 },
{ "product_name": "靴下", "quantity": 1, "price": 5000 }
]
}
注意点 内部のドメインオブジェクトと DTO を分けると詰め替え(マッピング)のコードが増える。手間に見えるが、APIの見た目と内部設計を独立して変更できる利点がある。
9
Offline Concurrency Patterns
「画面を開いて編集し、しばらくしてから保存する」など複数のDBトランザクションをまたぐ長い処理(ビジネストランザクション) で、その間ずっとDBロックを握れない。DBの外側で競合をどう防ぐかがテーマ。
楽観ロックと悲観ロックの違い(重要)
両者は「競合への構え方」が正反対です。
楽観(Optimistic)ロック : 「衝突はめったに起きない」と楽観的に構える。編集中はロックせず 、保存する瞬間に「自分が読んだ後に他人が変更していないか」をチェック。問題なければ保存、誰かが先に変更していたら保存を拒否 する。
悲観(Pessimistic)ロック : 「衝突は起きるもの」と悲観的に構える。編集を始める時点でロックを取り 、他人がそのデータに触れないようにする。確実に防げるが、その間ほかの人は待たされる。
ざっくり、楽観は「ぶつかってから気づく(後で検出)」、悲観は「ぶつからないよう最初から通せんぼ(事前に予約)」 。競合がまれなら楽観、頻繁なら悲観が向きます。
Optimistic Offline Lock
一言で 保存時に競合を検出し、衝突していたらそのトランザクションを失敗させて防ぐ。
使いどころ 同時編集がまれで、編集中ずっとロックを握って他人を待たせたくないときに使う。性能と同時実行性に優れる。
具体例 【場面】2人が同じ記事を同時に編集する。ロックは取らず、保存時に「読み込んだときのバージョンと一致するか」を確認し、ズレていたら他人の更新を上書きせず弾く。
# articles に lock_version 列を持たせ、読み込み時の version を WHERE 条件に入れる
UPDATE articles
SET title = ?, lock_version = lock_version + 1
WHERE id = ? AND lock_version = ?
# 更新行数が0なら他人が先に更新済み → StaleObjectError
# Rails では lock_version 列があれば save! 時にこの判定が自動で走る
注意点 衝突は保存ボタンを押すまで分からないため、長時間かけた編集が最後に弾かれることがある。
Pessimistic Offline Lock
一言で 編集を始める時点でロックを取り、同時に1つのビジネストランザクションだけがデータに触れられるようにする。
使いどころ 競合が頻繁、あるいはやり直しのコストが高すぎるため、後で弾くのではなく最初から防ぎたいときに使う。
具体例 【場面】月次の経理締めなど、衝突したらやり直しのコストが大きい処理。作業を始める前に対象行のロックを取り、他のトランザクションを待たせる。
# SELECT ... FOR UPDATE でロックを取得し、コミットまで他者の更新を待たせる
ActiveRecord::Base.transaction do
account = Account.lock.find(account_id) # FOR UPDATE
account.balance -= 1000
account.save!
end # commit でロック解放。他のトランザクションはここまでブロックされる
社内Wikiの「○○さんが編集中です(チェックアウト)」方式も同じ発想。
注意点 ロックを取った人が放置・離席するとデータが長時間ふさがれる。ロック管理(タイムアウト、デッドロック回避)の実装が複雑になる。
Coarse-Grained Lock
一言で 関連する複数のオブジェクトを、まとめて1つのロックで管理する。
使いどころ 「注文」と「明細」のように一体で扱うべきオブジェクト群を個別ロックすると煩雑で一貫性も保ちにくい。代表となる1つにロックをかけ、関連物すべてを覆う。
具体例 【場面】注文と、その明細行をまとめて1つの単位として守りたい。明細を1行ずつロックするのではなく、親「注文」のバージョンを共有ロックとして使い、グループ全体を1つのロックで覆う。
# 明細それぞれに lock_version を持たせず、親 order の lock_version を全員で共有
order = Order.find(order_id)
order.line_items.first.quantity = 3 # 子を変更
order.save! # 判定するのは親 order のバージョンだけ。子の変更も含めて一括で守られる
DDDの集約(アグリゲート)単位でロックする考え方に近い。
注意点 ロック単位が大きくなるぶん同時実行性は下がる。
Implicit Lock
一言で ロックの取得を、各開発者が手動で書くのではなく、フレームワークや基盤コードに自動でやらせる。
使いどころ ロック取得を毎回手書きすると、誰か1人が書き忘れただけで仕組み全体が崩れる。ロック処理を共通基盤に押し込み、開発者が意識しなくても自動でかかるようにする。
具体例 【場面】ロックの取得を各開発者の手書きに任せると、1人が書き忘れただけで並行性バグが出る。ロック処理を共通基盤(基底クラスやフレームワーク)に隠し、普通に保存するだけで自動的にロックがかかるようにする。
# 開発者は明示的なロック処理を書かない
article = Article.find(id)
article.title = "新タイトル"
article.save! # ← この中で lock_version の照合が自動的に走る(書き忘れようがない)
対象は楽観ロックに限らず、悲観ロックの取得・解放も含め、あらゆるロック操作を基盤に任せられる。
注意点 便利な反面、ロックが「裏で勝手に」動くため挙動がブラックボックス化し、問題が起きたとき原因を追いにくい。
10
Session State Patterns
Webアプリは本来ステートレスだが、「カートの中身」「複数ページにまたがる入力」などユーザーの一連の操作にまたがる状態 を保持する必要がある。それをクライアント / サーバ / DB のどこに置くか、スケール性・耐障害性・性能のトレードオフで選ぶ。
Client Session State
一言で セッション状態をクライアント側(ブラウザなど)に持たせる。
使いどころ サーバ側に状態を持たないため、サーバを増やしても「どのサーバに繋がっても同じ状態」が保てる(スケールが容易)。状態が小さいときに向く。
具体例 【場面】入力中のフォーム内容を、サーバーに保存せずブラウザ側に持たせる。サーバーはセッションを覚えず、状態はクライアントが毎回送ってくる。
<!-- hidden フィールドやクッキー、URLパラメータに状態を埋め込む -->
<input type="hidden" name="wizard_step" value="2">
<input type="hidden" name="plan" value="pro">
# 受け取り側: step = params[:wizard_step].to_i(状態はリクエストから読む)
JWTにユーザー情報を入れてクライアントが持ち歩くのも同じ。
注意点 クライアントが改ざんできるため信用できず(署名や検証が必要)、毎回サーバへ送るのでデータ量が大きいと通信が重い。
Server Session State
一言で セッション状態をサーバ側のメモリ等に、シリアライズした形で保持する。
使いどころ クライアントには小さなセッションIDだけ渡し、実体はサーバが持つ。実装がシンプルで、状態が大きくても通信を圧迫しない。最も手軽。
具体例 【場面】ログイン後のカートの中身などを、サーバーのメモリ(セッションストア)に保持する。クライアントはセッションIDだけを持ち、中身はサーバー側が覚える。
# クライアントは session_id だけクッキーで持つ。中身はサーバー(メモリやRedis)が保持
session[:cart] = { items: [{ product_id: 1, qty: 2 }] }
# 次のリクエストでは session_id から状態を引き当てる
cart = session[:cart]
注意点 サーバが状態を持つため、複数台にするとスティッキーセッションや共有ストアが必要になり、スケールやフェイルオーバーが難しくなる。
Database Session State
一言で セッション状態を、データベースにコミット済みのデータとして保存する。
使いどころ 状態をDBに置くことで、どのサーバからでも参照でき、サーバが落ちても消えない。スケールと耐障害性を両立したいときに向く。
具体例 【場面】申込ウィザードの途中状態を、メモリではなくDBのテーブルに保存する。サーバーを複数台に増やしても、どのサーバーが受けても同じ状態を読める。
# セッション状態を専用テーブルに保存(アプリサーバーはステートレスでいられる)
SessionState.upsert(session_id: sid, key: "wizard_step", value: "2")
# 次のリクエストはどのサーバーでも、DBから状態を読み直せる
step = SessionState.find_by(session_id: sid, key: "wizard_step").value
注意点 毎回DBへの読み書きが発生して性能負荷がかかり、「正式な業務データ」と「一時的なセッションデータ」がDB上で混在して整理しづらくなることがある。
11
Base Patterns(基礎パターン)
他のパターンの土台となる汎用的で小さな部品群 。特定の層に限定されず、あらゆる場所で再利用される道具箱。中でも Value Object (不変・同値性で比較)は重要。
Gateway
一言で 外部システムやリソースを、シンプルなオブジェクトのインターフェースで包む薄いラッパー。
使いどころ 外部API・DB・メッセージキューなど「外の世界」へのアクセスの複雑さをGatewayの中に閉じ込め、呼び出し側はメソッドを呼ぶだけにする。テスト時に差し替えれば本物の外部システムなしでテストできる。
具体例 【場面】外部の天気APIを叩く処理が業務ロジックのあちこちに散らばると、仕様変更やテストが大変。Gatewayでアクセスを1か所に閉じ込める。
class WeatherGateway
def temperature(city)
res = Net::HTTP.get(URI("https://api.example.com/weather?city=#{city}"))
JSON.parse(res)["temp_c"]
end
end
# 利用側はAPIのURLやJSON構造を知らなくてよい
temp = WeatherGateway.new.temperature("Tokyo")
注意点 1つの外部リソースにつき1つのGatewayが基本。
Mapper
一言で 2つのサブシステムを、お互いに相手を知らせないまま橋渡しするオブジェクト。
使いどころ 2つの部品を連携させたいが直接依存させたくないときに使う。Gatewayと違い、Mapperは橋渡しされる両側のどちらからも参照されず、第三者として間に立つ。代表例がO/Rマッパー。
具体例 【場面】DBの users テーブル(first_name/last_name 列)と、業務オブジェクトの User(name を持つ)は形が違う。両者を互いに知らせず、間に立つMapperだけが変換を知る。
class UserMapper
def to_domain(row) # DBの行 → 業務オブジェクト
User.new(name: "#{row['first_name']} #{row['last_name']}")
end
def to_row(user) # 業務オブジェクト → DBの行
first, last = user.name.split(" ", 2)
{ "first_name" => first, "last_name" => last }
end
end
注意点 中間層が増えるため設計は複雑になる。完全な分離が必要な場面に限る。
Layer Supertype
一言で ある層の全クラスに共通する機能をまとめた、その層用の基底クラス。
使いどころ 同じ層のクラスが似た処理(IDの保持、共通ユーティリティ)を繰り返し持つことがある。それを親クラスにまとめ、各クラスが継承して重複を減らす。
具体例 【場面】すべての業務オブジェクトが id と「同じ型・同じidなら同一」という判定を共通で持つ。各クラスに書くのは重複なので、共通の親クラスに1度だけ書いて全員が継承する。
class DomainObject
attr_reader :id
def ==(other)
other.is_a?(self.class) && id == other.id
end
end
class Customer < DomainObject; end
class Product < DomainObject; end # どちらも == を継承して持つ
Railsの ApplicationRecord がこれ。
注意点 共通化しすぎると基底クラスが肥大化する。本当に層全体で共通な責務だけを置く。
Separated Interface
一言で インターフェース(定義)と実装を別のパッケージに分けて、依存方向を制御する。
使いどころ 上位の層が下位の実装に直接依存すると変更の波及やテストのやりにくさを招く。インターフェースだけを利用側に置き、実装を別パッケージに置くことで依存の向きを分離できる。
具体例 【場面】業務ロジック層は「メール送信したい」だけで、SendGridという実装の詳細に依存したくない。インターフェース(契約)を業務層側に置き、実装は別パッケージに置く。業務層は契約だけを見る。
# 業務ロジック層に置く「契約」
module Notifier
def notify(user, message); end
end
# インフラ層に置く「実装」(業務層はこのクラスを名指ししない)
class SendGridNotifier
include Notifier
def notify(user, message) = SendGrid::Client.new.send(to: user.email, body: message)
end
# 業務層は Notifier 型として受け取るだけ
class WelcomeService
def initialize(notifier:) = @notifier = notifier
def run(user) = @notifier.notify(user, "ようこそ")
end
注意点 パッケージ分割の手間が増える。本当に切りたい境界にだけ適用する。
Registry
一言で よく使うオブジェクトやサービスを見つけるための、よく知られた置き場所(グローバルな窓口)。
使いどころ あちこちから必要になる共通オブジェクト(設定、現在のユーザー、共有サービス)を引数で引き回さずに取得したいときに使う。
具体例 【場面】アプリ全体で共有したい設定値や現在のユーザーを、引数でバケツリレーするのは面倒。よく知られた1か所(Registry)に置いて、どこからでも取り出せるようにする。
class Registry
def self.current_user = @current_user
def self.current_user=(u) = @current_user = u
end
Registry.current_user = login_user
puts Registry.current_user.name # 遠く離れた処理からでも取り出せる
注意点 実質グローバル変数なので依存関係が見えにくくテストも難しくなる。多用は避ける。
Value Object
一言で 同一性(どのインスタンスか)ではなく、保持する値そのものが等しいかで比較される、不変の小さなオブジェクト。
使いどころ 金額・日付・座標・色のように「値が同じなら同じものとして扱いたい」概念を表す。定義の核心は同値性 (中身の値がすべて等しければ等しい。IDやメモリ位置で比較しない)で、Entity(IDで区別され状態が変化する)との対比がこの一点。不変性は定義上の必須要件ではなく「実務上ほぼ必須の強い推奨」 で、不変だからこそ安心して共有・コピーでき、ハッシュのキーにもできる(DDD以降は不変性を本質とみなす流儀も広く定着)。
具体例 【場面】2次元の座標 Point は「同じxとyなら同じ点」として扱いたい(同値性)。また一度作った点は書き換えず、移動は新しい点を返す(不変性)。
class Point
attr_reader :x, :y
def initialize(x, y)
@x, @y = x, y
freeze # 不変にする
end
def ==(other) # 値が同じなら等しい(IDでは比較しない)
other.is_a?(Point) && x == other.x && y == other.y
end
def move(dx, dy) # 自身を変えず新しい点を返す
Point.new(x + dx, y + dy)
end
end
Point.new(1, 2) == Point.new(1, 2) # => true
お金や期間も典型例。
注意点 等価判定(==)とハッシュ値を値ベースで正しく実装する必要がある。可変にすると共有時のバグの温床になるため不変を徹底する。
Money
一言で 金額と通貨をセットで扱い、誤差なく安全に計算するための専用の Value Object。
使いどころ お金を float/double で扱うと丸め誤差で会計が狂う。「100円 + 100ドル」のような通貨違いの加算も防ぎたい。金額(整数や BigDecimal で誤差なく保持)と通貨を1つにまとめ、安全な四則演算と通貨チェックを提供する。
具体例 【場面】金額をfloatで持つと誤差が出るし、円とドルをうっかり足すと事故になる。金額と通貨をセットにし、計算はBigDecimal、通貨が違う加算は例外にする。
class Money
attr_reader :amount, :currency
def initialize(amount, currency)
@amount = BigDecimal(amount.to_s)
@currency = currency
end
def +(other)
raise "通貨が違います" if currency != other.currency
Money.new(amount + other.amount, currency)
end
end
Money.new("100.50", :JPY) + Money.new("0.50", :JPY) # => 101 JPY
Money.new("100", :JPY) + Money.new("1", :USD) # => 例外
注意点 端数処理(丸め)の方針を明示的に決める必要がある。内部表現は浮動小数点を避ける。
Special Case
一言で 「特別な状況」を、ifでの分岐ではなく専用のサブクラスとして表現する。
使いどころ nullや例外的なケースのたびに呼び出し側でチェックや分岐が散らばるとコードが読みにくい。特別なケースを通常と同じインターフェースを持つオブジェクトにすれば、呼び出し側はチェックなしで同じように扱える。代表例が Null Object 。
具体例 【場面】顧客が見つからないとき nil を返すと、呼び出し側が毎回 if customer.nil? を書く羽目になる。代わりに「未登録客」を表すオブジェクトを返せば、呼び出し側は分岐なしに同じメソッドを呼べる(Null Object)。
class NullCustomer
def name = "ゲスト"
def premium? = false
end
def find_customer(id)
Customer.find_by(id: id) || NullCustomer.new
end
puts find_customer(999).name # => "ゲスト"(nilチェック不要)
注意点 ケースごとにクラスが増える。本来エラーにすべき場面まで握りつぶさないよう注意する。
Plugin
一言で 使う実装クラスをコードではなく設定で選べるようにし、環境ごとに差し替え可能にする。
使いどころ 環境(本番/テスト)や顧客ごとに実装を切り替えたいとき、コード中の分岐は散らばって管理しづらい。「どの実装を使うか」を設定ファイル等に集約し、実行時に組み立てる。Separated Interfaceと組み合わせて使われることが多い。
具体例 【場面】本番はS3、テストはローカルディスクにファイルを保存したい。利用側のコードは変えず、設定で「どの実装を使うか」を切り替え、起動時に差し込む(plug-in)。
# 設定で実装クラス名を指定(コードではなく設定で決める)
config = { storage: "S3Storage" } # テスト時は "LocalDiskStorage"
# 起動時に設定を読んで実装を差し込む
storage = Object.const_get(config[:storage]).new
storage.save("report.pdf", data) # 利用側は同じI/Fを呼ぶだけ
注意点 設定とリフレクション的な仕組みが必要で追いかけにくさが増す。差し替えが本当に必要な箇所に絞る。
Service Stub
一言で テスト時に、本物の外部サービスの代わりとなる軽量な偽物(スタブ)に差し替える。
使いどころ 外部の課金API・税率計算などは、遅い・不安定・お金がかかるためテストに不向き。同じインターフェースを持つ偽物に差し替えれば、外部に触れず高速かつ安定したテストができる。
具体例 【場面】テストのたびに本物の決済APIを叩くと、遅いし課金も発生する。同じインターフェースを持つ「いつも成功を返す偽物」に差し替えれば、外部に依存せず高速にテストできる。
# 本物と同じ I/F を持つスタブ(実際には通信しない)
class StubPaymentService
def charge(amount)
{ status: "success", amount: amount } # 常に成功を返す
end
end
# テストでは本物の代わりにスタブを注入する
result = StubPaymentService.new.charge(1000)
expect(result[:status]).to eq("success")
注意点 スタブの挙動が本物と乖離すると、テストは通るのに本番で壊れる。現実に即した範囲で単純化する。
Record Set
一言で DBのテーブルや問い合わせ結果を、メモリ上でそのまま表現するオブジェクト(行と列の集まり)。
使いどころ 多くの環境(特に旧来のGUIツールやデータバインディング)は表形式データを前提にUI部品やレポート機能を提供する。Record Setはその表をメモリ上のオブジェクトとして扱い、UIと素直に結びつける。
具体例 【場面】DBの検索結果を、表形式(行と列)のまま扱えるオブジェクト。.NETの DataTable が典型で、行をたどり列名で値を取り出せる。画面のグリッド表示などにそのまま流し込める。
// SQLの結果を表(行×列)としてメモリに保持する
DataTable customers = adapter.GetCustomers();
foreach (DataRow row in customers.Rows)
{
Console.WriteLine(row["name"] + ": " + row["email"]); // 列名で値を取り出せる
}
注意点 データを「表」として扱うため、リッチなドメインロジックを持たせにくい。
12
Rails での具体例
本書のパターンの多くは Rails が標準で採用しているため、普段書いているコードがそのままパターンの実例になっています。身近な対応関係を押さえると理解が一気に進みます。
PoEAA パターン Rails での対応物
Active Record class Foo < ApplicationRecord。Railsの永続化機構そのもの
Layer Supertype ApplicationRecord / ApplicationController
Identity Field 各テーブルの id カラム、Foo.find(id)
Foreign Key Mapping belongs_to / has_one / has_many
Association Table Mapping has_many :through / has_and_belongs_to_many
Single Table Inheritance STI(type カラム)
Query Object where(...).order(...) のスコープ・メソッドチェーン
Unit of Work / Identity Map transaction 内の変更追跡、リクエスト内のクエリキャッシュ
Lazy Load アソシエーションの遅延読み込み(裏返しが N+1問題 、includes で eager load)
Front Controller config/routes.rb + ルーティングのディスパッチ
Page Controller コントローラの各アクション(#show, #create …)
Template View ERB / Haml / Slim のビュー
Two Step View レイアウト(application.html.erb)+ 各ビューの合成
Optimistic Offline Lock lock_version カラムによる楽観ロック
Serialized LOB serialize / JSON型カラム
Service Layer (標準にはない)app/services/ 配下の Service オブジェクトとして自前で導入
Repository (標準にはない)DDDを取り入れる場合に自前で導入
Railsが「Active Record方式」を全面採用していることが上の表から分かります。すなわちモデルがデータ・ロジック・永続化を兼ねる。これは小〜中規模では強力ですが、ロジックが肥大化するとモデルが「Fat Model」化するため、一定以上の規模のアプリでは Service Layer (業務操作の調整役)や Value Object (金額・期間など)を補助的に導入してロジックを逃がす、というのがパターンの実践的な使い方になります。
13
パターンの選び方ガイド
カタログは「引く」道具だが、実務で本当に難しいのはどれを選ぶか 。本書前半(解説章)の価値の多くはこの判断ロジックにある。各層の選択を横断的にまとめる。
基本姿勢:単純な方から始め、複雑さが正当化されたら移行する
パターン選択は「表示・ドメイン・データソースの各層で代表を1つ選ぶ」作業であり、最も影響が大きいのはドメインロジックの構成 。ここでの選択が他の層に波及する。原則は「最初から重厚な作りにしない。単純な選択から始め、複雑さが実際に増して初めて重い方へ移行する」。
ドメインロジック:複雑さで選ぶ(最重要の分岐)
判断軸はドメインロジックの複雑さ 。Fowlerの核心的主張は「単純なうちは Domain Model の初期コストが引き合わず Transaction Script が有利。しかし複雑さが増すほど両者のコストが逆転する 」(複雑さに対するコスト曲線が交差するイメージ)。
ロジックがほぼCRUD・手続き的で量も少ない → Transaction Script
業務ルールが複雑で相互に絡み合う/成長していく → Domain Model
表形式データ環境での折衷 → Table Module
Service Layer は上乗せの判断 。「UI・API・バッチなど複数の入口から同じ操作を使う」「トランザクション境界を明確に引きたい」場合に、上記のどれかの上に重ねる。
データソース:ドメイン側の選択に従う
Active Record ↔ Data Mapper の分岐 : テーブルとオブジェクトの形がほぼ一致し単純 → Active Record /複雑な Domain Model でDBスキーマとずれる → Data Mapper 。「Domain Model を選んだら Data Mapper が自然な相方」と覚えるとよい。
継承マッピング3種 : まず単純な Single Table → NULLの多さや正規化が気になれば Class Table → 各クラスを独立に扱い親型でのまとめ検索が不要なら Concrete Table 。
対になるパターンの早見表
選択 こちら(太字)を選ぶ条件 もう一方を選ぶ条件
Transaction Script ↔ Domain Model ロジックが複雑・成長する 単純・CRUD中心なら Transaction Script
Active Record ↔ Data Mapper ドメインが複雑でDBと形がずれる 一致していて単純なら Active Record
Page Controller ↔ Front Controller 共通処理が多い・大規模・柔軟なルーティング 小規模で画面と1対1が分かりやすいなら Page Controller
Template View ↔ Transform View 複数フォーマットへ変換/データ起点 HTML主体で見た目を見ながら作るなら Template View
楽観 ↔ 悲観ロック 衝突を未然に防ぎたい/やり直しが現実的でない(競合が頻繁・長いトランザクション) 競合がまれで待たせたくないなら楽観ロック
Client / Server / Database Session State サーバ複数台で耐障害性も欲しい 状態が小さくスケール優先=Client/手軽さ優先=Server
Fowlerの選択原則・箴言
分散の第一法則「オブジェクトを分散させるな(Don't distribute your objects)」 : 性能向上を狙って安易にプロセス/サーバ境界でオブジェクトを分割するな、という戒め。ネットワーク越し呼び出しは桁違いに遅く、分散はむしろ性能を悪化させやすい。分散は「どうしても必要な境界」に限り、その境界では Remote Facade + DTO で粗粒度化して緩和する。
単純に始め、必要になってから複雑さを足す : YAGNI的態度。先回りの過剰設計を避ける。
パターンは出発点であって終着点ではない : 文脈に合わせて調整して使うもので、教条的に当てはめない。
ありがちな誤選択(兆候)
単純なCRUDアプリに、最初から重厚な Domain Model + Data Mapper を導入する(オーバーエンジニアリング)。
性能を狙ってオブジェクトを安易に分散し、かえって遅くなる(第一法則違反)。
競合がまれな場面で悲観ロックを使い、同時実行性を不必要に落とす。
Domain Model を選んだのにロジックを Service Layer に書き込み、ドメインが貧弱な「手続きの寄せ集め」になる(Anemic Domain Model)。
14
読み方ガイド(初学者向け)
まず全体像(1章)とレイヤ化だけ理解する。 細かいパターン名は後でよい。「アプリは表示・ドメイン・データソースの3層に分かれ、各パターンはそのどこかを担う」が腑に落ちれば十分。
自分が今使っているフレームワークのパターンから入る。 Railsなら Active Record / MVC / Query Object。「いつも書いているあれに名前がついていた」という発見が理解を加速する(12章の表が入口)。
対になるパターンをセットで覚える。 Active Record ↔ Data Mapper、Transaction Script ↔ Domain Model、Page Controller ↔ Front Controller、Template View ↔ Transform View、楽観ロック ↔ 悲観ロック。違いを言えれば設計判断ができる。
「正解」ではなく「トレードオフ」で読む。 どのパターンにも向き・不向きがある。本書の価値は選択肢とその代償を言語化してくれる点にある。
辞書として使う。 設計に迷ったらカタログで該当カテゴリを引く。通読より「引く」道具。
現代における位置づけ
ここで命名されたパターンは、現在のORM・Webフレームワーク・API設計の共通語彙として定着している。「DTOを返す」「Repositoryを切る」「楽観ロックをかける」といった会話はすべて本書由来。
一方、当時の前提(XML設定、重厚なアプリケーションサーバ、SOAP)は古い。個々のパターンの本質(責務分割・トレードオフ)を学び、実装の細部は現代の道具に読み替える のが正しい付き合い方。
マイクロサービスやクラウドネイティブの文脈でも、Distribution Patterns(粗粒度呼び出し)や Session State Patterns(状態をどこに置くか)の判断軸はそのまま生きている。