mcpField
C
code-architecture

code-architecture

github.com/curiositech開発257157 回インストール2 日前に更新
リポジトリ

code-architecture について

code-architectureは、単一のデプロイ単位内でコードを整理するためのスキルです。新規プロジェクトの設計や既存コードの診断に向けて、レイヤード、ヘキサゴナル、機能別の構成、依存関係やテスト配置を解説します。

主な機能

  1. 01クリーン/ヘキサゴナルアーキテクチャ
  2. 02機能別のコード構成
  3. 03依存性逆転と注入
  4. 04モジュール境界と公開API
  5. 05循環依存の診断
  6. 06アーキテクチャ層ごとのテスト計画

活用シーン

  1. 01新規プロジェクトのアーキテクチャ選定
  2. 02成長するアプリのモジュール整理
  3. 03肥大化したコントローラーや循環依存の診断
  4. 04ビジネスロジックを分離するテスト構成

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/curiositech/some_claude_skills --skill code-architecture

インストール後、Claude Code で/code-architectureを入力して読み込まれたか確認します。

同梱ファイル

SKILL.md と一緒にインストールされる参考資料やスクリプト。

.claude-plugin/
plugin.json
references/
architecture-patterns.md
dependency-inversion.md

詳細

コードアーキテクチャ

現実に直面しても通用するアーキテクチャパターンとコードの構成です。目標は、書かれてから18か月後も理解し、テストし、変更できるコードです。ここではモジュールおよびアプリケーションレベルでの決定を扱います。サービスの分割方法でも、スキーマの設計方法でもなく、単一のデプロイ可能な単位内でコードをどう構成するかを扱います。

使用するタイミング

次のような場合に使用します:

  • 新しいプロジェクトでアーキテクチャパターンを選ぶ(クリーン、ヘキサゴナル、機能ベース、垂直スライス)
  • 既存コードのアーキテクチャ上の問題を診断する(肥大化したコントローラー、循環依存、絡み合ったビジネスロジック)
  • 依存性逆転を適用して、密結合したコードを整理する
  • フォルダー構造とモジュール構成を決める
  • モジュール規模でSOLID原則を適用する(クラス規模だけでなく)
  • 依存性注入コンテナーを設定する
  • モジュール境界とバレルエクスポートを定義する
  • アーキテクチャのレイヤーに応じてテストピラミッドを設計する

次の用途には使用しません:

  • モノリスをマイクロサービスに分割する(microservices-patternsを使用してください)
  • データベーススキーマの設計、正規化、クエリの最適化(database-design-patternsを使用してください)
  • フレームワーク固有のルーティングやミドルウェアの設定
  • CI/CDパイプラインのアーキテクチャ

中核となる決定:どのアーキテクチャパターンを選ぶか?

flowchart TD
    Start[New project or restructuring?] --> Team{Team size?}

    Team -->|Solo / 2 dev| Size{App complexity?}
    Team -->|3-8 dev| MedComplex{Domain complexity?}
    Team -->|8+ dev| Large[Feature-based / Vertical Slice]

    Size -->|Simple CRUD, low domain logic| Simple[Flat + MVC or Feature-Based]
    Size -->|Complex domain, long-lived| Hex[Hexagonal Architecture]

    MedComplex -->|Low: thin business layer| FeatureBased[Feature-Based Organization]
    MedComplex -->|High: rich business rules| Clean[Clean Architecture]
    MedComplex -->|Mixed: CRUD + some complex| Vertical[Vertical Slice Architecture]

    Simple --> SCheck{Will it grow?}
    SCheck -->|No, short-lived| MVC[Layer-based MVC is fine]
    SCheck -->|Yes| FeatureBased

    Large --> Testing{Testing rigor?}
    Testing -->|Low: mostly E2E| Vertical
    Testing -->|High: unit + integration| Clean

    Clean --> ClNote[Entities → Use Cases → Interface Adapters → Frameworks]
    Hex --> HexNote[Domain core ↔ Ports ↔ Adapters]
    FeatureBased --> FBNote[features/X contains all layers for X]
    Vertical --> VSNote[Each slice owns its full stack top to bottom]

クリーンアーキテクチャのレイヤー

古典的なオニオン型です。各リングは内側のみに依存し、外側には依存しません。

graph TB
    subgraph Frameworks ["Frameworks & Drivers (outer)"]
        DB[(Database)]
        HTTP[HTTP Server]
        UI[UI / CLI]
        External[External APIs]
    end

    subgraph Adapters ["Interface Adapters"]
        Controllers[Controllers / Routes]
        Presenters[Presenters]
        Gateways[Repository Implementations]
        DTOs[DTOs / View Models]
    end

    subgraph UseCases ["Use Cases (Application Layer)"]
        UC1[CreateOrder]
        UC2[ProcessPayment]
        UC3[NotifyUser]
        Ports[Repository Interfaces]
    end

    subgraph Entities ["Entities (Domain)"]
        Order[Order]
        User[User]
        Payment[Payment]
        Rules[Business Rules]
    end

    HTTP --> Controllers
    Controllers --> UC1
    UC1 --> Ports
    Gateways --> Ports
    DB --> Gateways
    UC1 --> Order

依存関係のルール:ソースコードの依存関係は内側にのみ向かいます。エンティティはユースケースについて何も知りません。ユースケースはコントローラーについて何も知りません。コントローラーはデータベースドライバーについて何も知りません。

各レイヤーに含まれるもの:

レイヤー含まれるもの例
エンティティエンタープライズのビジネスルールOrder、Payment、ドメインイベント、値オブジェクト
ユースケースアプリケーション固有のビジネスロジックCreateOrderUseCase、RefundPaymentUseCase
インターフェースアダプターユースケースと外部形式の間でデータを変換するものコントローラー、プレゼンター、リポジトリー実装
フレームワーク外部ツール、データベース、UIフレームワークExpress、Postgres、React

ヘキサゴナルアーキテクチャ(ポートとアダプター)

多くのチームにとって、クリーンアーキテクチャよりもシンプルな考え方です。すべてがポート(インターフェース)を介して接続され、アダプターがそれを実装します。

graph LR
    subgraph Domain ["Domain (Hexagon)"]
        Logic[Business Logic]
        DPorts[Driven Ports<br/>interfaces the domain calls]
    end

    subgraph Driving ["Driving Adapters<br/>(what drives the app)"]
        HTTP2[HTTP Controller]
        CLI2[CLI Command]
        Test[Test Harness]
    end

    subgraph Driven ["Driven Adapters<br/>(what the app drives)"]
        DB2[(PostgreSQL Adapter)]
        Email[SendGrid Adapter]
        Cache[Redis Adapter]
    end

    HTTP2 -->|calls| Logic
    CLI2 -->|calls| Logic
    Test -->|calls| Logic
    Logic -->|calls port| DPorts
    DPorts -.->|implemented by| DB2
    DPorts -.->|implemented by| Email
    DPorts -.->|implemented by| Cache

ヘキサゴナルがクリーンアーキテクチャより適している場合:インフラを簡単に差し替える必要がある場合(テストではインメモリー、本番環境ではPostgresを使用するなど)、またはビジネスロジックが十分に豊富で分離する価値はあるものの、クリーンアーキテクチャのような完全なレイヤー分割までは必要ない場合です。


機能ベースとレイヤーベースの構成

レイヤーベース(従来のMVC)
src/
  controllers/
    user.controller.ts
    order.controller.ts
    payment.controller.ts
  services/
    user.service.ts
    order.service.ts
    payment.service.ts
  repositories/
    user.repository.ts
    order.repository.ts
  models/
    user.model.ts
    order.model.ts

問題点:「orders」機能を追加または変更するには、4つのディレクトリーにあるファイルを編集する必要があります。ordersドメインを理解するには、フォルダー間を行き来する必要があります。

機能ベース(成長中のアプリに推奨)
src/
  features/
    orders/
      orders.controller.ts
      orders.service.ts
      orders.repository.ts
      orders.schema.ts
      orders.types.ts
      orders.test.ts
      index.ts           ← public API (barrel export)
    users/
      users.controller.ts
      users.service.ts
      ...
      index.ts
    payments/
      ...
  shared/
    database/
    logger/
    config/

利点:注文に関するすべてのコードが同じ場所にまとまります。機能を削除するには、フォルダーを1つ削除するだけです。開発者がordersドメインについて理解するには、1つのディレクトリーを見るだけで済みます。

ルール:機能がインポートできるのはshared/と、他の機能の公開されたindex.tsのみです。互いの内部には決して立ち入りません。


アンチパターン:コントローラーにビジネスロジックを置く(肥大化したコントローラー)

初心者:「リクエストが入ってくる場所がコントローラーだから、ロジックもそこに置きます。便利です。」

// Fat controller — don't do this
app.post('/orders', async (req, res) => {
  const { userId, items } = req.body;

  // Business logic #1: calculate total
  const total = items.reduce((sum, item) => sum + item.price * item.qty, 0);

  // Business logic #2: apply discount
  const user = await db.users.findById(userId);
  const discount = user.memberSince < oneYearAgo ? 0.1 : 0;
  const finalTotal = total * (1 - discount);

  // Business logic #3: check inventory
  for (const item of items) {
    const stock = await db.inventory.findById(item.productId);
    if (stock.quantity < item.qty) {
      return res.status(409).json({ error: 'Out of stock' });
    }
  }

  // Side effects mixed in
  const order = await db.orders.create({ userId, items, total: finalTotal });
  await emailService.send(user.email, 'order-confirmation', { order });
  await inventory.decrement(items);

  res.json(order);
});

エキスパート:コントローラーはHTTPアダプターです。HTTPからドメインへ変換し、ユースケースまたはサービスを呼び出してから、ドメインからHTTPレスポンスへ変換します。すべてのビジネスロジックは、HTTPコンテキストなしでテストできるユースケースまたはドメインサービスに置くべきです。

// Lean controller
app.post('/orders', async (req, res) => {
  try {
    const command = CreateOrderCommand.fromRequest(req.body);  // Validate + map
    const order = await createOrderUseCase.execute(command);   // All logic here
    res.status(201).json(OrderPresenter.toJSON(order));        // Map to response
  } catch (error) {
    errorHandler(res, error);
  }
});

// Use case: testable, framework-agnostic
class CreateOrderUseCase {
  constructor(
    private readonly orderRepo: OrderRepository,
    private readonly inventoryService: InventoryService,
    private readonly discountPolicy: DiscountPolicy,
    private readonly notifications: NotificationPort,
  ) {}

  async execute(command: CreateOrderCommand): Promise<Order> {
    const user = await this.orderRepo.findUser(command.userId);
    const discount = this.discountPolicy.calculate(user);
    await this.inventoryService.reserveItems(command.items);
    const order = Order.create(command.items, discount);
    await this.orderRepo.save(order);
    await this.notifications.orderCreated(order, user);
    return order;
  }
}

検出方法:ロジックが約20行を超えるコントローラー、データベースモデルを直接インポートするコントローラー、ネストしたif-elseのビジネス条件を含むコントローラーです。


アンチパターン:アーキテクチャの宇宙飛行士(抽象化のための抽象化)

初心者:「Repository インターフェース、Repository の実装、Service、ServiceInterface、Service を作成する Factory、EventBus、CQRS のコマンドハンドラーを追加します。これでエンタープライズ品質です。」

エキスパート:抽象化にはすべてコストがかかります。ファイルが増え、間接性が高まり、オンボーディングが難しくなり、保守するものも増えます。抽象化は、テスト、差し替え、コード再利用を可能にするときに効果を発揮する投資です。変更されることのない単一の実装しかない UserServiceInterface を追加するなら、抽象化のコストを払うだけで、そのメリットは得られません。

次のように問いかけてください:「この抽象化によって、ほかの方法では実現できない何が可能になるのか?」

  • Repository インターフェース → テストで実際の DB をインメモリのものに差し替えられます。すぐに効果があります。
  • Service インターフェース → Service が常に 1 つしかないなら、これは形式的な手続きです。
  • Factory パターン → オブジェクトの生成が複雑な場合や、複数の戦略がある場合に効果があります。
  • イベントバス → 多くのコンポーネントが互いを知らずにドメインイベントに反応する必要がある場合に効果があります。

検出方法:*Interface.ts、*Abstract.ts、*Factory.ts という名前のファイルがあり、実装する側も呼び出す側も 1 つだけで、その実装が変わらない場合です。

変遷:Enterprise Java(2005~2015 年)では、あらゆるものを抽象化するのが標準でした。Spring Framework はこれを推奨しました。2015 年以降の Node.js と Go のコミュニティは、「退屈なテクノロジー」の原則を掲げてこれに反発しました。2026 年において、適切な抽象化のレベルは状況によって異なります。抽象化をまったくしないのも、最大限に行うのも適切ではありません。


アンチパターン:循環依存

初心者:「Order モジュールは Users について知る必要があり、User モジュールはユーザーの注文を確認する必要があります。だから、互いにインポートします。」

// orders/order.service.ts
import { UserService } from '../users/user.service';   // Order → User

// users/user.service.ts
import { OrderService } from '../orders/order.service'; // User → Order

// Node.js will silently give you `undefined` at runtime
// Jest will give you cryptic "Cannot access before initialization" errors

エキスパート:循環依存は、ドメインモデリングに問題があることを示しています。本当に互いを必要とする 2 つのモジュールは、1 つのモジュールに統合するか、両方が依存する 3 つ目のモジュールを共有するか、イベントやインターフェースを介して通信する必要があります。

解決策:

  1. 統合する:Order と User が本当に切り離せないなら、accounts/ にまとめます
  2. 共通部分を抽出する:両方からインポートできる order-summary/ を作成します
  3. インターフェースで依存関係を逆転する:User モジュールで OrderSummaryPort インターフェースを定義し、Orders が実装します。User が Orders をインポートすることはありません
  4. イベント駆動にする:User が OrderService を直接呼び出すのではなく、OrderCreated イベントに反応します
# Detect circular deps
npx madge --circular src/
npx dpdm --circular src/index.ts

# ESLint rule (add to .eslintrc)
# "import/no-cycle": "error"

検出方法:起動時にモジュールの値が undefined になるランタイムエラー、ESLint の import/no-cycle 違反、madge の循環依存出力です。


実践での依存関係逆転

SOLID の D は、具象ではなく抽象に依存するという原則です。モジュール規模で適用すると、次のようになります。

// Bad: Use case is coupled to Postgres
class CreateOrderUseCase {
  constructor(private readonly db: PostgresConnection) {}

  async execute(cmd: CreateOrderCommand) {
    await this.db.query('INSERT INTO orders ...');
  }
}

// Good: Use case depends on an interface
interface OrderRepository {
  save(order: Order): Promise<void>;
  findById(id: OrderId): Promise<Order | null>;
}

class CreateOrderUseCase {
  constructor(private readonly orderRepo: OrderRepository) {}

  async execute(cmd: CreateOrderCommand) {
    const order = Order.create(cmd);
    await this.orderRepo.save(order);  // No SQL, no Postgres, no coupling
  }
}

// Production: Postgres implements the interface
class PostgresOrderRepository implements OrderRepository {
  async save(order: Order): Promise<void> { /* Postgres SQL */ }
  async findById(id: OrderId): Promise<Order | null> { /* Postgres SQL */ }
}

// Tests: in-memory implements the same interface
class InMemoryOrderRepository implements OrderRepository {
  private orders = new Map<string, Order>();
  async save(order: Order) { this.orders.set(order.id, order); }
  async findById(id: OrderId) { return this.orders.get(id) ?? null; }
}

DI コンテナーが有効な場合:配線が必要な依存関係が多数あり、手作業での配線がエラーを招きやすい、または繰り返しが多い場合です。NestJS、InversifyJS(TypeScript)、Python の dependency-injector、Spring(Java)が該当します。

DI コンテナーが過剰な場合:シンプルなスクリプト、依存関係の少ない小規模サービス、サーバーレス関数、Go プロジェクト(コンストラクターインジェクションが慣用的で十分です)が該当します。


モジュール境界とバレルエクスポート

各機能/モジュールは、index.ts を介して公開 API を提供する必要があります。

// features/orders/index.ts — public API
export { CreateOrderUseCase } from './create-order.use-case';
export { OrderRepository } from './order.repository.interface';
export type { Order, OrderStatus } from './order.entity';
// NOT exported: internal helpers, SQL queries, implementation details

// Other modules import from the public API only
import { CreateOrderUseCase } from '@/features/orders';
// NOT: import from '@/features/orders/create-order.use-case'

ESLint で強制する:

// .eslintrc
{
  "rules": {
    "import/no-internal-modules": ["error", {
      "allow": ["**/*.test.ts", "**/index.ts"]
    }]
  }
}

テストアーキテクチャ(テストピラミッドでの配置)

各アーキテクチャ層には、適したテストの種類があります。

層テストの種類速度カバレッジ
エンティティ/ドメイン単体テスト瞬時90%+
ユースケース/アプリケーションモックを使った単体テスト高速80%+
インターフェースアダプター統合テスト中速70%+
フレームワーク/外部E2E/コントラクトテスト低速主要なパス

原則:ビジネスロジックのテストでは、サーバーの起動、データベースへの接続、ネットワーク呼び出しを必要としないようにしてください。ユースケースのテストで実際のデータベースが必要なら、アーキテクチャによってインフラストラクチャ上の関心事がドメインに漏れています。


参考資料

  • references/architecture-patterns.md — 詳細なパターンについては、クリーンアーキテクチャのレイヤー、ヘキサゴナルアーキテクチャのポートとアダプター、垂直スライス、実例を含む機能ベースの構成を参照してください
  • references/dependency-inversion.md — TypeScript と Python の IoC コンテナー、コンストラクターインジェクションのパターン、DI が過剰となる場合、一般的な DI のアンチパターンについては、こちらを参照してください

コメント