<CodeLearn/>
設計パターン レッスン4

アーキテクチャパターン

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の派生として、MVPMVVMがあります。 それぞれ、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など
  • プロジェクトの規模と複雑性に応じて適切なアーキテクチャを選ぶ