アーキテクチャパターン
MVC、クリーンアーキテクチャ、DDDなど、アプリケーション全体の設計を学ぼう
アーキテクチャパターンとは
アーキテクチャパターンは、アプリケーション全体の構造を決める大規模な設計パターンです。 デザインパターンが「クラスやオブジェクト間の関係」を扱うのに対し、 アーキテクチャパターンは「システム全体のレイヤーや責務の分け方」を扱います。
適切なアーキテクチャを選ぶことで、大規模なアプリケーションでも保守性・テスト容易性・チーム開発の効率を維持できます。
MVC (Model-View-Controller)
MVCは最も広く知られたアーキテクチャパターンで、 アプリケーションを3つの層に分離します。Ruby on Rails、Laravel、Spring MVCなど多くのフレームワークが採用しています。
Model(モデル)
データとビジネスロジックを管理。DBアクセスやバリデーションを担当。
View(ビュー)
ユーザーに表示するUI部分。データの表示・入力を担当。
Controller(コントローラー)
ユーザー入力を受け取り、ModelとViewを仲介する。
// MVC の例(Express風の実装)
// Model - データとビジネスロジック
interface Todo {
id: number;
title: string;
completed: boolean;
}
class TodoModel {
private todos: Todo[] = [];
private nextId = 1;
getAll(): Todo[] {
return [...this.todos];
}
create(title: string): Todo {
const todo: Todo = {
id: this.nextId++,
title,
completed: false,
};
this.todos.push(todo);
return todo;
}
toggleComplete(id: number): Todo | undefined {
const todo = this.todos.find((t) => t.id === id);
if (todo) todo.completed = !todo.completed;
return todo;
}
}
// View - 表示ロジック(ここではJSON応答)
class TodoView {
renderList(todos: Todo[]): string {
return JSON.stringify({ todos, count: todos.length });
}
renderOne(todo: Todo): string {
return JSON.stringify(todo);
}
renderError(message: string): string {
return JSON.stringify({ error: message });
}
}
// Controller - リクエスト処理と仲介
class TodoController {
constructor(
private model: TodoModel,
private view: TodoView
) {}
handleGetAll(): string {
const todos = this.model.getAll();
return this.view.renderList(todos);
}
handleCreate(title: string): string {
if (!title.trim()) {
return this.view.renderError("タイトルは必須です");
}
const todo = this.model.create(title);
return this.view.renderOne(todo);
}
handleToggle(id: number): string {
const todo = this.model.toggleComplete(id);
if (!todo) return this.view.renderError("TODOが見つかりません");
return this.view.renderOne(todo);
}
}MVP と MVVM
MVCの派生として、MVPとMVVMがあります。 それぞれ、ViewとModelの結合をさらに緩くする工夫がされています。
MVP (Model-View-Presenter)
ViewとModelが完全に分離。Presenterが仲介する。
- • ViewはPresenterへのイベント通知のみ
- • Presenterが全てのロジックを制御
- • テストしやすい(Viewをモック化できる)
- • Android開発で多く採用
MVVM (Model-View-ViewModel)
データバインディングでViewとViewModelを自動同期。
- • ViewModelがUI状態を公開
- • データバインディングで自動反映
- • React, Vue, Angularが近い
- • フロントエンド開発で主流
// MVVM的なアプローチ(React + カスタムフック)
// Model - ドメインロジック
interface User {
id: string;
name: string;
email: string;
}
async function fetchUsers(): Promise<User[]> {
const res = await fetch("/api/users");
return res.json();
}
// ViewModel - UI状態とロジック(カスタムフック)
function useUserList() {
const [users, setUsers] = useState<User[]>([]);
const [loading, setLoading] = useState(true);
const [searchQuery, setSearchQuery] = useState("");
useEffect(() => {
fetchUsers().then((data) => {
setUsers(data);
setLoading(false);
});
}, []);
const filteredUsers = users.filter((u) =>
u.name.toLowerCase().includes(searchQuery.toLowerCase())
);
return { users: filteredUsers, loading, searchQuery, setSearchQuery };
}
// View - UIの表示のみ
function UserListPage() {
const { users, loading, searchQuery, setSearchQuery } = useUserList();
if (loading) return <p>読み込み中...</p>;
return (
<div>
<input
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
placeholder="検索..."
/>
<ul>
{users.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
</div>
);
}レイヤードアーキテクチャ(階層型)
レイヤードアーキテクチャは、アプリケーションを 複数の水平層に分割するパターンです。各層は直下の層のみを呼び出します。
// 典型的な3層(4層)アーキテクチャ
// ┌─────────────────────────────┐
// │ Presentation Layer │ ← Controller / Route Handler
// │ (プレゼンテーション層) │
// ├─────────────────────────────┤
// │ Application Layer │ ← UseCase / Service
// │ (アプリケーション層) │
// ├─────────────────────────────┤
// │ Domain Layer │ ← Entity / ValueObject
// │ (ドメイン層) │
// ├─────────────────────────────┤
// │ Infrastructure Layer │ ← Repository / External API
// │ (インフラストラクチャ層) │
// └─────────────────────────────┘
// 例: ユーザー登録フロー
// Infrastructure Layer(データアクセス)
class UserRepository {
async save(user: { name: string; email: string }) {
// DBへの保存
console.log("DB保存:", user);
return { id: "1", ...user };
}
async findByEmail(email: string) {
// DBからの検索
return null;
}
}
// Domain Layer(ビジネスルール)
class UserEntity {
constructor(
public readonly name: string,
public readonly email: string
) {
if (!name || name.length < 2) {
throw new Error("名前は2文字以上必要です");
}
if (!email.includes("@")) {
throw new Error("メールアドレスの形式が不正です");
}
}
}
// Application Layer(ユースケース)
class RegisterUserUseCase {
constructor(private userRepo: UserRepository) {}
async execute(name: string, email: string) {
// 既存ユーザーチェック
const existing = await this.userRepo.findByEmail(email);
if (existing) {
throw new Error("このメールアドレスは既に登録されています");
}
// ドメインオブジェクト生成(バリデーション含む)
const user = new UserEntity(name, email);
// 永続化
return await this.userRepo.save({
name: user.name,
email: user.email,
});
}
}
// Presentation Layer(APIハンドラ)
async function handleRegister(req: Request) {
const body = await req.json();
const useCase = new RegisterUserUseCase(new UserRepository());
try {
const user = await useCase.execute(body.name, body.email);
return new Response(JSON.stringify(user), { status: 201 });
} catch (e: any) {
return new Response(
JSON.stringify({ error: e.message }),
{ status: 400 }
);
}
}クリーンアーキテクチャ
クリーンアーキテクチャは、Robert C. Martinが提唱した 「依存関係を内側に向ける」設計方針です。ビジネスロジック(ドメイン)が フレームワークやDBなどの外部技術に依存しないようにします。
// クリーンアーキテクチャの同心円(内側 → 外側)
//
// ┌───────────────────────────────────────┐
// │ Frameworks & Drivers │ Express, React, PostgreSQL
// │ ┌───────────────────────────────┐ │
// │ │ Interface Adapters │ │ Controllers, Gateways, Presenters
// │ │ ┌───────────────────────┐ │ │
// │ │ │ Application Layer │ │ │ Use Cases
// │ │ │ ┌───────────────┐ │ │ │
// │ │ │ │ Entities │ │ │ │ Domain Models
// │ │ │ └───────────────┘ │ │ │
// │ │ └───────────────────────┘ │ │
// │ └───────────────────────────────┘ │
// └───────────────────────────────────────┘
//
// ルール: 依存の方向は「外側 → 内側」のみ// クリーンアーキテクチャの実装例
// Entity(最内層 - 外部に依存しない)
class Order {
constructor(
public readonly id: string,
public readonly items: { productId: string; quantity: number }[],
public readonly status: "pending" | "confirmed" | "shipped"
) {}
getTotalItems(): number {
return this.items.reduce((sum, item) => sum + item.quantity, 0);
}
canCancel(): boolean {
return this.status === "pending";
}
}
// Use Case(アプリケーション層 - Entityに依存)
// ポートとしてインターフェースを定義
interface OrderRepository {
findById(id: string): Promise<Order | null>;
save(order: Order): Promise<void>;
}
interface NotificationService {
notify(userId: string, message: string): Promise<void>;
}
class CancelOrderUseCase {
constructor(
private orderRepo: OrderRepository,
private notifier: NotificationService
) {}
async execute(orderId: string, userId: string): Promise<void> {
const order = await this.orderRepo.findById(orderId);
if (!order) throw new Error("注文が見つかりません");
if (!order.canCancel()) throw new Error("この注文はキャンセルできません");
const cancelled = new Order(order.id, order.items, "pending");
await this.orderRepo.save(cancelled);
await this.notifier.notify(userId, "注文がキャンセルされました");
}
}
// Infrastructure(最外層 - Use Caseのインターフェースを実装)
class PostgresOrderRepository implements OrderRepository {
async findById(id: string): Promise<Order | null> {
// PostgreSQLからデータ取得
return null;
}
async save(order: Order): Promise<void> {
// PostgreSQLにデータ保存
}
}
class EmailNotificationService implements NotificationService {
async notify(userId: string, message: string): Promise<void> {
// メールで通知
}
}DDD(ドメイン駆動設計)の基礎
DDD(Domain-Driven Design)は、 ビジネスの問題領域(ドメイン)を中心にソフトウェアを設計する手法です。 Eric Evansが提唱しました。主要な構成要素を見てみましょう。
Entity(エンティティ)
一意のIDを持つオブジェクト。例: ユーザー、注文、商品
Value Object(値オブジェクト)
IDを持たず値で比較されるオブジェクト。例: 住所、金額、メールアドレス
Aggregate(集約)
関連するEntityとValue Objectをまとめた単位。Aggregate Rootを通じてアクセスする
Repository(リポジトリ)
Aggregateの永続化と取得を抽象化するインターフェース
Domain Service(ドメインサービス)
EntityやValue Objectに属さないドメインロジック
// DDD 構成要素の例 - ECサイトの注文ドメイン
// Value Object(値で比較。不変。)
class Money {
constructor(
readonly amount: number,
readonly currency: string
) {
if (amount < 0) throw new Error("金額は0以上です");
}
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(一意のIDを持つ)
class OrderItem {
constructor(
readonly productId: string,
readonly productName: string,
readonly price: Money,
readonly quantity: number
) {}
getSubtotal(): Money {
return new Money(
this.price.amount * this.quantity,
this.price.currency
);
}
}
// Aggregate Root(外部からはここを通じてアクセス)
class Order {
private items: OrderItem[] = [];
constructor(
readonly id: string,
readonly customerId: string,
private status: "draft" | "placed" | "shipped" = "draft"
) {}
addItem(item: OrderItem): void {
if (this.status !== "draft") {
throw new Error("確定済みの注文には追加できません");
}
this.items.push(item);
}
getTotal(): Money {
return this.items.reduce(
(sum, item) => sum.add(item.getSubtotal()),
new Money(0, "JPY")
);
}
place(): void {
if (this.items.length === 0) {
throw new Error("商品がありません");
}
this.status = "placed";
}
}どのアーキテクチャを選ぶべきか
アーキテクチャの選択はプロジェクトの規模・複雑性・チーム体制によって変わります。
シンプルなMVCやレイヤードアーキテクチャで十分。過度な抽象化は避ける。
レイヤードアーキテクチャ + SOLID原則。テスト容易性を意識した依存性注入。
クリーンアーキテクチャやDDD。ドメインの複雑性をモデリングし、長期的な保守性を確保。
各サービスにDDDの境界づけられたコンテキスト。サービス間はAPIで疎結合に。
まとめ
- MVC - Model/View/Controllerの3層分離。Webフレームワークの基本
- MVP/MVVM - MVCの派生。フロントエンドではMVVM的アプローチが主流
- レイヤードアーキテクチャ - 水平層に分割。上位層は下位層のみに依存
- クリーンアーキテクチャ - 依存の方向を内側に統一。ドメインが外部技術に依存しない
- DDD - ビジネスドメインを中心に設計。Entity、Value Object、Aggregateなど
- プロジェクトの規模と複雑性に応じて適切なアーキテクチャを選ぶ