はじめに
ソフトウェア設計の記事やコードレビューで、Repository、Entity、Service、ViewModel といった用語がよく登場します。しかし、これらは文脈(DDD、Clean Architecture、MVC など)によって意味が微妙に異なり、混乱しやすいポイントです。
この記事では、設計でよく使われる用語を 「何を表すか」「どこに属するか」「何と混同しやすいか」 の3つの観点で整理します。
用語の全体マップ
まず、各用語がどのレイヤーに属するかを俯瞰します。
+--------------------------------------------+
| プレゼンテーション層 |
| View / ViewModel / Presenter / Controller |
+--------------------------------------------+
| アプリケーション層 |
| UseCase / ApplicationService |
+--------------------------------------------+
| ドメイン層 |
| Entity / ValueObject / DomainService |
| Aggregate / DomainEvent |
+--------------------------------------------+
| インフラストラクチャ層 |
| Repository(実装) / Gateway / DAO |
+--------------------------------------------+
なお Repository と Gateway は、インターフェースを内側の層に、実装をインフラ層に置く用語です。上の図に置いたのは実装側で、インターフェースはそれぞれドメイン層・アプリケーション層に属します(詳しくは後述します)。
ドメイン層の用語
Entity(エンティティ)
同一性(ID)を持つオブジェクト。 属性が変わっても、ID が同じなら同じオブジェクトとみなされます。
class User {
constructor(
readonly id: string, // ← 同一性を持つ
private name: string,
private email: string
) {}
changeName(newName: string) {
this.name = newName;
// name が変わっても、id が同じなら同じ User
}
}
例: ユーザー、注文、商品、記事
Value Object(値オブジェクト)
同一性を持たず、値そのものが意味を持つオブジェクト。 すべての属性が同じなら等しいとみなされます。イミュータブル(不変)に設計します。
class Money {
constructor(
readonly amount: number,
readonly currency: string
) {}
add(other: Money): Money {
if (this.currency !== other.currency) {
throw new Error("通貨が異なります");
}
return new Money(this.amount + other.amount, this.currency);
// 新しいインスタンスを返す(イミュータブル)
}
equals(other: Money): boolean {
return this.amount === other.amount && this.currency === other.currency;
}
}
例: 金額、住所、メールアドレス、日付範囲、座標
Entity との違い: Entity は「ID で区別」、Value Object は「値で区別」。2つの Money(100, "JPY") は同じですが、2つの User は ID が異なれば別物です。
Aggregate(集約)
関連する Entity と Value Object をまとめた単位。 外部からのアクセスは必ず Aggregate Root(集約ルート) を経由します。
// Order が Aggregate Root
class Order {
private items: OrderItem[] = [];
private status: "processing" | "completed" = "processing";
constructor(
readonly id: string,
readonly userId: string
) {}
static create(id: string, userId: string): Order {
return new Order(id, userId);
}
// Product は別の集約なので、実体ではなく ID と値を受け取る
addItem(productId: string, price: number, quantity: number) {
// OrderItem に直接アクセスさせず、Order 経由で操作する
this.items.push(new OrderItem(productId, price, quantity));
}
getTotal(): number {
return this.items.reduce((sum, item) => sum + item.getSubtotal(), 0);
}
isCompleted(): boolean {
// 状態判定は Order 自身が持つ
return this.status === "completed";
}
}
class OrderItem {
constructor(
readonly productId: string,
readonly price: number,
readonly quantity: number
) {}
getSubtotal(): number {
return this.price * this.quantity;
}
}
ポイント: OrderItem は Order を経由してのみ操作されます。これにより、ビジネスルールの一貫性が保たれます。
もう1つのポイント: addItem が Product オブジェクトそのものではなく productId を受け取っている点に注目してください。Product は Order とは別の集約なので、他の集約は ID で参照するのが原則です。集約同士を直接つなぐと、境界が曖昧になり、トランザクションの範囲も広がってしまいます。
Domain Service(ドメインサービス)
特定の Entity や Value Object に属さないドメインロジック を担当するクラスです。
class TransferService {
transfer(from: Account, to: Account, amount: Money) {
// 送金ロジックは from にも to にも属さない
from.withdraw(amount);
to.deposit(amount);
}
}
Entity に置くか、Domain Service に置くかの判断: そのロジックが1つの Entity の責務として自然かを考えます。複数の Entity にまたがるなら Domain Service が適切です。
注意: この例のように複数の集約を1つのトランザクションで更新するのは、DDD の「1トランザクション1集約」という原則とは緊張関係にあります。実務では、片方の更新をドメインイベント経由の結果整合性で扱う設計も検討します。
Domain Event(ドメインイベント)
ドメインで起きた出来事を表すオブジェクトです。
class OrderPlaced {
constructor(
readonly orderId: string,
readonly userId: string,
readonly occurredAt: Date
) {}
}
例: 「注文が確定した」「ユーザーが登録した」「在庫が切れた」
アプリケーション層の用語
UseCase(ユースケース)
アプリケーション固有のビジネスルールを調整する層。 ドメインオブジェクトを組み合わせて、1つのユースケースを実現します。
class PlaceOrderUseCase {
constructor(
private orderRepository: OrderRepository,
private paymentService: PaymentService,
private mailer: Mailer
) {}
execute(orderId: string, userId: string, items: CartItem[]): Order {
const order = Order.create(orderId, userId);
items.forEach((i) => order.addItem(i.productId, i.price, i.quantity));
this.paymentService.charge(order.getTotal());
this.orderRepository.save(order);
this.mailer.sendConfirmation(userId, order);
return order;
}
}
注意: UseCase 自体はビジネスロジックを持たず、ドメインオブジェクトの調整役に徹します。
Application Service(アプリケーションサービス)
UseCase とほぼ同義で使われることが多いです。トランザクション管理や認可チェックなど、ドメインロジック以外のアプリケーション固有の処理を担当します。
Domain Service との違い: Domain Service はドメインロジック、Application Service はアプリケーションの調整役です。
プレゼンテーション層の用語
Controller(コントローラ)
外部からのリクエストを受け取り、適切な UseCase に振り分ける 役割です。MVC や Web API で使われます。
class OrderController {
constructor(private placeOrder: PlaceOrderUseCase) {}
// HTTP POST /orders
async create(req: Request, res: Response) {
const order = this.placeOrder.execute(
crypto.randomUUID(),
req.userId,
req.body.items
);
res.status(201).json({ orderId: order.id });
}
}
ポイント: Controller はリクエストの受付とレスポンスの返却だけを行い、ビジネスロジックは UseCase に委譲します。
View(ビュー)
ユーザーに情報を表示する層。 HTML テンプレート、React コンポーネント、モバイルの画面など。
// React の場合
function OrderDetail({ order }: { order: OrderViewModel }) {
return (
<div>
<h1>注文 #{order.id}</h1>
<p>合計: {order.formattedTotal}</p>
</div>
);
}
ViewModel(ビューモデル)
View に表示するために整形されたデータ。 ドメインオブジェクトをそのまま View に渡すのではなく、表示用に変換したものです。
ただしこの用語も文脈で意味が変わります。
| 文脈 | ViewModel の意味 |
|---|---|
| Clean Architecture | Presenter が生成する、表示用に整形されたデータだけの入れ物 |
| MVVM | 表示用データに加え、View の状態と操作(Command)を持つ能動的なオブジェクト。データバインディングで View と同期する |
以下は Clean Architecture 寄りの、データ変換に徹した例です。
class OrderViewModel {
constructor(private order: Order) {}
get id(): string {
return this.order.id;
}
get formattedTotal(): string {
return `¥${this.order.getTotal().toLocaleString()}`;
}
get statusLabel(): string {
return this.order.isCompleted() ? "完了" : "処理中";
}
}
なぜ必要か: ドメインオブジェクトを直接 View に渡すと、表示ロジック(フォーマット、ラベル変換)がドメインに漏れ出します。ViewModel で変換を吸収することで、関心の分離を保てます。
MVVM で使う場合: 上の例に加えて、onSubmit() のような View からの操作を受けるメソッドや、View に変更を通知する仕組み(Observable、useState、LiveData など)を持たせます。
Presenter(プレゼンター)
UseCase やドメインの出力を、View が扱える形に変換する 役割です。もともとは MVP(Model-View-Presenter) の用語で、Clean Architecture にも取り込まれています。
MVP と Clean Architecture での違い:
- MVP の Presenter: View からの入力を受け取り、Model を操作し、結果を View に反映する。入力と出力の両方を担当します(Controller に相当する役割も兼ねます)
- Clean Architecture の Presenter: UseCase の出力(Output Data)を受け取り、ViewModel に変換する 出力側専用の役割です。入力側は Controller が担当します
インフラストラクチャ層の用語
Repository(リポジトリ)
Entity の永続化と取得を抽象化するインターフェース。 データベースやファイルなど、具体的な保存先を隠蔽します。
// ドメイン層: インターフェース(抽象)
interface OrderRepository {
findById(id: string): Order | null;
save(order: Order): void;
}
// インフラ層: 実装(具象)
class MySqlOrderRepository implements OrderRepository {
findById(id: string): Order | null {
// MySQL からデータを取得して Order を組み立てる
throw new Error("not implemented");
}
save(order: Order): void {
// Order を MySQL に保存する
throw new Error("not implemented");
}
}
ポイント: インターフェースはドメイン層に、実装はインフラ層に置きます(DIP: 依存性逆転の原則)。
また、MySqlOrderRepository という実装クラスは、ドメインの語彙には存在しない人工的なクラスです。これは GRASP の Pure Fabrication(純粋な作り物)の典型例で、GRASP の原著でも永続化の責務を担うクラスが例として挙げられています。
Gateway(ゲートウェイ)
外部システムとの通信を抽象化する インターフェースです。Repository がデータの永続化に特化しているのに対し、Gateway は外部 API やメッセージキューなど、より広い範囲の外部サービスとの連携を担当します。
// インターフェース(アプリケーション層 or ドメイン層に置く)
interface PaymentGateway {
charge(amount: number, currency: string): PaymentResult;
}
// 実装(インフラ層に置く)
class StripeGateway implements PaymentGateway {
charge(amount: number, currency: string): PaymentResult {
// Stripe API を呼び出す
throw new Error("not implemented");
}
}
Repository との違い: Repository は「ドメインオブジェクトの保存・取得」、Gateway は「外部サービスとの通信」。
ポイント: Repository と同じく、Gateway もインターフェースと実装を分けて配置します。Clean Architecture では、Gateway のインターフェースをユースケース層に定義し、実装を外側の層に置きます。
DAO(Data Access Object)
データベースへのアクセスを抽象化する オブジェクトです。Repository と似ていますが、DAO はデータベースのテーブル構造に寄った設計になる傾向があります。
Repository との違い: Repository はドメインオブジェクト単位、DAO はテーブル単位で操作する傾向があります。
Model — 特定のレイヤーに属さない用語
Model(モデル)
最も多義的な用語です。特定のレイヤーに紐づく用語ではなく、文脈によって指すものが変わります。
| 文脈 | Model の意味 |
|---|---|
| MVC | ビジネスロジックとデータを持つ層全体 |
| Rails / Laravel | ActiveRecord パターンの DB テーブルに対応するクラス |
| DDD | Entity + Value Object + Domain Service の総称 |
| 機械学習 | 学習済みのアルゴリズム |
注意: 「Model」は文脈を確認しないと意味が特定できません。チーム内で共通認識を持つことが重要です。
用語の比較表
| 用語 | 責務 | レイヤー | 主な出典 |
|---|---|---|---|
| Entity | 同一性を持つドメインオブジェクト | ドメイン | DDD |
| Value Object | 値で区別されるイミュータブルなオブジェクト | ドメイン | DDD |
| Aggregate | Entity と Value Object をまとめた整合性の単位 | ドメイン | DDD |
| Domain Service | Entity に属さないドメインロジック | ドメイン | DDD |
| Domain Event | ドメインで起きた出来事 | ドメイン | DDD |
| UseCase | ユースケースの調整役 | アプリケーション | Clean Architecture |
| Application Service | アプリケーション固有の処理の調整役 | アプリケーション | DDD |
| Controller | リクエストの受付と振り分け | プレゼンテーション | MVC |
| View | ユーザーへの表示 | プレゼンテーション | MVC / MVVM |
| ViewModel | 表示用に整形されたデータ(MVVM では操作も持つ) | プレゼンテーション | Clean Architecture / MVVM |
| Presenter | 出力を表示用に変換(MVP では入力も担当) | プレゼンテーション | MVP / Clean Architecture |
| Repository | Entity の永続化と取得の抽象化 | ドメイン(IF) / インフラ(実装) | DDD |
| Gateway | 外部システムとの通信の抽象化 | アプリケーション(IF) / インフラ(実装) | Clean Architecture |
| DAO | データベースアクセスの抽象化 | インフラ | J2EE Patterns |
| Model | 文脈による(ドメイン全体 / ORM クラス) | 文脈による | MVC / Rails / DDD |
よくある混同と整理
Repository vs DAO
- Repository: ドメインオブジェクト(Aggregate)単位で操作。ドメイン層のインターフェース
- DAO: テーブル単位で CRUD 操作。インフラ層のクラス
Domain Service vs Application Service
- Domain Service: ドメインロジック(送金計算、在庫チェックなど)
- Application Service: アプリケーションの流れ(トランザクション、認可、通知の調整)
Controller vs Presenter
Clean Architecture では、入力側と出力側で役割が分かれます。
- Controller: リクエストを受け取って UseCase に渡す(入力側)
- Presenter: UseCase の結果を View 用に変換する(出力側)
一方、MVP の Presenter は入力・出力の両方を担当するため、Controller に相当する役割も兼ねます。「Presenter」という語を見たら、どちらの文脈かを確認してください。
Entity(DDD)vs Model(Rails)
- DDD の Entity: ビジネスロジックを持つ。DB を知らない
- Rails の Model: ActiveRecord パターン。DB テーブルに直結
まとめ
設計用語は文脈(DDD、Clean Architecture、MVC、特定のフレームワーク)によって意味が変わります。重要なのは、チーム内で 「この用語はこの意味で使う」 という共通認識を持つことです。
各用語がどのレイヤーに属し、何の責務を持つかを理解しておくと、設計の議論やコードレビューがスムーズになります。