hexagonal-architecture
ポートとアダプターパターンに基づくシステムの設計・実装・リファクタリングを行い、明確なドメイン境界と依存性逆転を実現します。TypeScript、Java、Kotlin、Go のサービスにわたるテスト可能なユースケースの整理もサポートします。
description の原文を見る
设计、实现并重构端口与适配器系统,具有清晰的领域边界、依赖反转以及跨 TypeScript、Java、Kotlin 和 Go 服务的可测试用例编排。
SKILL.md 本文
六边形アーキテクチャ
六角形アーキテクチャ(ポート&アダプタパターン)は、ビジネスロジックをフレームワーク、トランスポート層、永続化の詳細から独立させます。コアアプリケーションは抽象的なポートに依存し、アダプタは境界の外側でこれらのポートを実装します。
適用シーン
- 長期的なメンテナンス性とテスト可能性が必要な新機能を構築する場合。
- レイヤード化またはフレームワーク依存のコードをリファクタリングする際、ドメインロジックがI/O関心事と混在している場合。
- 同じユースケースに対して複数のインターフェース(HTTP、CLI、キューワーカー、スケジュールタスク)をサポートする場合。
- ビジネスルールを書き直さずにインフラストラクチャ(データベース、外部API、メッセージバス)を置き換える場合。
境界、ドメイン駆動設計、密結合サービスのリファクタリング、またはアプリケーションロジックを特定のライブラリから分離する必要がある場合に、このスキルを使用します。
コアコンセプト
- ドメインモデル:ビジネスルールと実体/値オブジェクト。フレームワークのインポートなし。
- ユースケース(アプリケーション層):ドメイン動作とワークフローステップのオーケストレーション。
- インバウンドポート:アプリケーション機能の契約(コマンド/クエリ/ユースケースインターフェース)。
- アウトバウンドポート:アプリケーションが必要とする依存関係の契約(リポジトリ、ゲートウェイ、イベント発行者、クロック、UUIDなど)。
- アダプタ:ポートのインフラストラクチャと配信実装(HTTPコントローラ、データベースリポジトリ、キューコンシューマ、SDKラッパー)。
- コンポジションルート:具体的なアダプタをユースケースにバインドする単一の接続ポイント。
アウトバウンドポートインターフェースは通常アプリケーション層に配置され(抽象がドメイン層に本当に属する場合のみドメイン層)、インフラストラクチャアダプタはそれを実装します。
依存関係の方向は常に内向きです:
- アダプタ -> アプリケーション/ドメイン
- アプリケーション -> ポートインターフェース(インバウンド/アウトバウンド契約)
- ドメイン -> ドメイン抽象のみ(フレームワークまたはインフラストラクチャ依存なし)
- ドメイン -> 外部依存なし
動作方法
ステップ1:ユースケース境界をモデル化する
明確な入出力DTOを持つ単一のユースケースを定義します。トランスポート詳細(Express req、GraphQL context、タスクペイロードラッパー)はこの境界の外に保ちます。
ステップ2:最初にアウトバウンドポートを定義する
各副作用をポートとして識別します:
- 永続化(
UserRepositoryPort) - 外部呼び出し(
BillingGatewayPort) - 横断的関心事(
LoggerPort、ClockPort)
ポートは技術ではなく能力をモデル化する必要があります。
ステップ3:純粋なオーケストレーションでユースケースを実装する
ユースケースクラス/関数はコンストラクタ/パラメータを通じてポートを受け取ります。これはアプリケーション層の不変量を検証し、ドメインルールを調整し、純粋なデータ構造を返します。
ステップ4:境界でアダプタを構築する
- インバウンドアダプタはプロトコル入力をユースケース入力に変換します。
- アウトバウンドアダプタはアプリケーション契約を具体的なAPI/ORM/クエリビルダにマップします。
- マッピングはユースケース内ではなくアダプタ内に保ちます。
ステップ5:コンポジションルートですべてを接続する
アダプタをインスタンス化してからユースケースに注入します。この接続を一元化して、隠れたサービスロケータの動作を避けます。
ステップ6:境界でテストする
- フェイクポートでユースケースのユニットテストを行います。
- 実際のインフラストラクチャ依存でアダプタの統合テストを行います。
- インバウンドアダプタを通じてユーザー向けフローのエンドツーエンドテストを行います。
アーキテクチャ図
flowchart LR
Client["Client (HTTP/CLI/Worker)"] --> InboundAdapter["Inbound Adapter"]
InboundAdapter -->|"calls"| UseCase["UseCase (Application Layer)"]
UseCase -->|"uses"| OutboundPort["OutboundPort (Interface)"]
OutboundAdapter["Outbound Adapter"] -->|"implements"| OutboundPort
OutboundAdapter --> ExternalSystem["DB/API/Queue"]
UseCase --> DomainModel["DomainModel"]
推奨モジュールレイアウト
明確な境界を持つ機能優先のオーガナイゼーションを使用します:
src/
features/
orders/
domain/
Order.ts
OrderPolicy.ts
application/
ports/
inbound/
CreateOrder.ts
outbound/
OrderRepositoryPort.ts
PaymentGatewayPort.ts
use-cases/
CreateOrderUseCase.ts
adapters/
inbound/
http/
createOrderRoute.ts
outbound/
postgres/
PostgresOrderRepository.ts
stripe/
StripePaymentGateway.ts
composition/
ordersContainer.ts
TypeScript の例
ポート定義
export interface OrderRepositoryPort {
save(order: Order): Promise<void>;
findById(orderId: string): Promise<Order | null>;
}
export interface PaymentGatewayPort {
authorize(input: { orderId: string; amountCents: number }): Promise<{ authorizationId: string }>;
}
ユースケース
type CreateOrderInput = {
orderId: string;
amountCents: number;
};
type CreateOrderOutput = {
orderId: string;
authorizationId: string;
};
export class CreateOrderUseCase {
constructor(
private readonly orderRepository: OrderRepositoryPort,
private readonly paymentGateway: PaymentGatewayPort
) {}
async execute(input: CreateOrderInput): Promise<CreateOrderOutput> {
const order = Order.create({ id: input.orderId, amountCents: input.amountCents });
const auth = await this.paymentGateway.authorize({
orderId: order.id,
amountCents: order.amountCents,
});
// markAuthorized returns a new Order instance; it does not mutate in place.
const authorizedOrder = order.markAuthorized(auth.authorizationId);
await this.orderRepository.save(authorizedOrder);
return {
orderId: order.id,
authorizationId: auth.authorizationId,
};
}
}
アウトバウンドアダプタ
export class PostgresOrderRepository implements OrderRepositoryPort {
constructor(private readonly db: SqlClient) {}
async save(order: Order): Promise<void> {
await this.db.query(
"insert into orders (id, amount_cents, status, authorization_id) values ($1, $2, $3, $4)",
[order.id, order.amountCents, order.status, order.authorizationId]
);
}
async findById(orderId: string): Promise<Order | null> {
const row = await this.db.oneOrNone("select * from orders where id = $1", [orderId]);
return row ? Order.rehydrate(row) : null;
}
}
コンポジションルート
export const buildCreateOrderUseCase = (deps: { db: SqlClient; stripe: StripeClient }) => {
const orderRepository = new PostgresOrderRepository(deps.db);
const paymentGateway = new StripePaymentGateway(deps.stripe);
return new CreateOrderUseCase(orderRepository, paymentGateway);
};
多言語マッピング
異なるエコシステムでも同じ境界ルールを使用します。構文と接続方法のみが変わります。
- TypeScript/JavaScript
- ポート:
application/ports/*をインターフェース/型として。 - ユースケース:コンストラクタ/パラメータインジェクション付きのクラス/関数。
- アダプタ:
adapters/inbound/*、adapters/outbound/*。 - コンポジション:明示的なファクトリ/コンテナモジュール(隠れたグローバル変数なし)。
- ポート:
- Java
- パッケージ:
domain、application.port.in、application.port.out、application.usecase、adapter.in、adapter.out。 - ポート:
application.port.*のインターフェース。 - ユースケース:プレーンなクラス(Spring
@Serviceはオプション、必須ではない)。 - コンポジション:Spring設定または手動ワイヤリング;接続ロジックはドメイン/ユースケースクラスの外に保つ。
- パッケージ:
- Kotlin
- モジュール/パッケージはJavaの分割をミラー(
domain、application.port、application.usecase、adapter)。 - ポート:Kotlinインターフェース。
- ユースケース:コンストラクタインジェクション付きのクラス(Koin/Dagger/Spring/手動)。
- コンポジション:モジュール定義または専用コンポジション関数;サービスロケータパターンを避ける。
- モジュール/パッケージはJavaの分割をミラー(
- Go
- パッケージ:
internal/<feature>/domain、application、ports、adapters/inbound、adapters/outbound。 - ポート:コンシューマアプリケーションパッケージが所有する小型インターフェース。
- ユースケース:インターフェースフィールドと明示的な
New...コンストラクタを持つ構造体。 - コンポジション:
cmd/<app>/main.goで接続(または専用接続パッケージ)、コンストラクタを明示的に保つ。
- パッケージ:
回避すべきアンチパターン
- ドメイン実体がORMモデル、Webフレームワーク型、またはSDKクライアントをインポートする。
- ユースケースが
req、res、またはキューメタデータから直接読み込む。 - ユースケースがドメイン/アプリケーションマッピングを経由せずにデータベース行を直接返す。
- アダプタがユースケースポートを通じてではなく相互に直接呼び出す。
- 依存関係接続を複数ファイルに分散させ、隠れたグローバルシングルトンを使用する。
マイグレーションハンドブック
- 頻繁に変更され、問題を引き起こす垂直スライス(単一エンドポイント/タスク)を選択します。
- 明示的な入出力型を持つユースケース境界を抽出します。
- 既存のインフラストラクチャ呼び出しを囲むアウトバウンドポートを導入します。
- オーケストレーションロジックをコントローラ/サービスからユースケースに移動します。
- 古いアダプタを保持しますが、新しいユースケースに委譲させます。
- 新しい境界の周りにテストを追加します(ユニットテスト + アダプタ統合テスト)。
- スライスごとに繰り返します;完全な書き直しを避けます。
既存システムのリファクタリング
- 絞扼者パターン:現在のエンドポイントを保持し、一度に1つのユースケースを新しいポート/アダプタにルーティングします。
- 大規模なリライトなし:機能スライスごとにマイグレーションし、特性化テストで動作を保持します。
- ファサードを先に構築:内部実装を置き換える前に、従来のサービスをアウトバウンドポートの後ろでラップします。
- コンポジション凍結:接続を早期に一元化し、新しい依存関係がドメイン/ユースケース層に漏れないようにします。
- スライス選択ルール:変更頻度が高く、影響範囲が狭いプロセスを優先します。
- ロールバック経路:マイグレーションされた各スライスに対して、本番動作が検証されるまで可逆スイッチまたはルーティング切り替えを保持します。
テストガイドライン(同じ六角形境界)
- ドメインテスト:実体/値オブジェクトを純粋なビジネスルールとしてテスト(モック、フレームワークセットアップなし)。
- ユースケースユニットテスト:アウトバウンドポートのフェイク/スタブでオーケストレーションをテスト;ビジネス結果とポート相互作用をアサート。
- アウトバウンドアダプタ契約テスト:ポートレベルで共有契約スイートを定義し、各アダプタ実装に対して実行します。
- インバウンドアダプタテスト:プロトコルマッピングを検証(HTTP/CLI/キューペイロードからユースケース入力へ、および出力/エラーをプロトコルに戻す)。
- アダプタ統合テスト:実際のインフラストラクチャ(データベース/API/キュー)に対して実行、シリアライゼーション、スキーマ/クエリ動作、再試行とタイムアウトをテスト。
- エンドツーエンドテスト:インバウンドアダプタ -> ユースケース -> アウトバウンドアダプタを通じて、重要なユーザージャーニーをカバー。
- リファクタリング安全性:抽出前に特性化テストを追加;新しい境界動作が安定して等価になるまで保持。
ベストプラクティスチェックリスト
- ドメインとアプリケーション層は内部型とポートのみをインポートします。
- 各外部依存はアウトバウンドポートで表現されます。
- 検証は境界で発生します(インバウンドアダプタ + ユースケース不変量)。
- 不変変換を使用(新しい値/実体を返す、共有状態を修正しない)。
- エラーは境界間で変換されます(インフラストラクチャエラー -> アプリケーション/ドメインエラー)。
- コンポジションルートは明示的で監査しやすい。
- ユースケースは単純なメモリフェイクポートでテスト可能。
- リファクタリングは動作保持テスト付きの1つの垂直スライスから始まります。
- 言語/フレームワーク固有の内容はアダプタ内に留まり、ドメインルールに入らないようにします。
ライセンス: MIT(寛容ライセンスのため全文を引用しています) · 原本リポジトリ
詳細情報
- 作者
- affaan-m
- ライセンス
- MIT
- 最終更新
- 不明
Source: https://github.com/affaan-m/everything-claude-code / ライセンス: MIT
関連スキル
doubt-driven-development
重要な判断はすべて、本番環境への展開前に新しい視点から対抗的レビューを実施します。速度より正確性が重要な場合、不慣れなコードを扱う場合、本番環境・セキュリティに関わるロジック・取り消し不可の操作など影響度が高い場合、または後でバグを修正するよりも今検証する方が効率的な場合に活用してください。
apprun-skills
TypeScriptを使用したAppRunアプリケーションのMVU設計に関する総合的なガイダンスが得られます。コンポーネントパターン、イベントハンドリング、状態管理(非同期ジェネレータを含む)、パラメータと保護機能を備えたルーティング・ナビゲーション、vistestを使用したテストに対応しています。AppRunコンポーネントの設計・レビュー、ルートの配線、状態フローの管理、AppRunテストの作成時に活用してください。
desloppify
コードベースのヘルスチェックと技術負債の追跡ツールです。コード品質、技術負債、デッドコード、大規模ファイル、ゴッドクラス、重複関数、コードスメル、命名規則の問題、インポートサイクル、結合度の問題についてユーザーが質問した場合に使用してください。また、ヘルススコアの確認、次の改善項目の提案、クリーンアップ計画の作成をリクエストされた際にも対応します。29言語に対応しています。
debugging-and-error-recovery
テストが失敗したり、ビルドが壊れたり、動作が期待と異なったり、予期しないエラーが発生したりした場合に、体系的な根本原因デバッグをガイドします。推測ではなく、根本原因を見つけて修正するための体系的なアプローチが必要な場合に使用してください。
test-driven-development
テスト駆動開発により実装を進めます。ロジックの実装、バグの修正、動作の変更など、あらゆる場面で活用できます。コードが正常に動作することを証明する必要がある場合、バグ報告を受けた場合、既存機能を修正する予定がある場合に使用してください。
incremental-implementation
変更を段階的に実施します。複数のファイルに影響する機能や変更を実装する場合に使用してください。大量のコードを一度に書こうとしている場合や、タスクが一度では完結できないほど大きい場合に活用します。