【TypeScript】ヘキサゴナルアーキテクチャの実装 - 柔軟で保守性の高いアプリ設計
2024-10-26
2024-10-26
ヘキサゴナルアーキテクチャの概要と利点
ヘキサゴナルアーキテクチャ(またはポート&アダプタアーキテクチャ)は、ソフトウェアシステムの内部ロジックと外部のインターフェースを分離する設計手法です。このアーキテクチャの中心にあるのは、ビジネスロジックを司る「アプリケーションコア」で、外部との接点である「ポート」や「アダプタ」を介して、依存性の逆転を実現します。 ヘキサゴナルアーキテクチャを採用することで、次のようなメリットが得られます。
- 拡張性と柔軟性
内部ロジックが外部インターフェースから独立しているため、データベースやユーザーインターフェースの変更が容易です。 - テストの容易さ
各ポートやアダプタをモック化することで、ビジネスロジックの単体テストが可能となります。 - 保守性の向上
コードが明確に分離されるため、他の部分に影響を与えずに特定の機能を修正できます。
ヘキサゴナルアーキテクチャの基本構造
ヘキサゴナルアーキテクチャは主に次の3つの層で構成されます。
- アプリケーションコア
ビジネスロジックの処理を担当します。この層は他の層に依存せず、純粋なロジックのみが含まれます。 - ポート
アプリケーションが外部とやり取りするためのインターフェースです。入力ポートはアプリケーションに情報を渡し、出力ポートはアプリケーションから外部に情報を提供します。 - アダプタ
外部システムとアプリケーションを接続する役割を持つ層です。例えば、データベースやAPIなどの外部システムとのやり取りを行います。 次に、TypeScript
でこれらの層を実装する方法を見ていきます。
TypeScriptでのヘキサゴナルアーキテクチャの実装
アプリケーションコアの実装
アプリケーションコアはシステムの中心であり、ユースケースやビジネスロジックを表現します。例えば、UserServiceというユースケースを実装し、ユーザーの作成や取得を行うロジックをコア層で定義します。
// src/core/usecases/UserService.ts
import { UserRepository } from '../ports/UserRepository';
import { User } from '../entities/User';
export class UserService {
constructor(private userRepository: UserRepository) {}
async createUser(user: User): Promise<User> {
// ビジネスロジックに基づいたユーザー作成処理
return await this.userRepository.save(user);
}
async getUserById(id: string): Promise<User | null> {
// ビジネスロジックに基づいたユーザー取得処理
return await this.userRepository.findById(id);
}
}
この例では、UserService
がユーザーを作成するビジネスロジックを実装しています。UserRepositoryインターフェースは後述するポート層で定義され、外部の依存を隠蔽しています。
ポートの実装
ポートは、アプリケーションコアが依存するインターフェースを定義する役割を持ちます。ここでは、UserRepositoryというリポジトリインターフェースを定義し、データの永続化方法を隠蔽します。
// src/core/ports/UserRepository.ts
import { User } from '../entities/User';
export interface UserRepository {
findById(id: string): Promise<User | null>;
save(user: User): Promise<User>;
}
このUserRepository
インターフェースにより、データの保存方法が具体的にどのように実装されているかに関わらず、アプリケーションコアは抽象的な操作のみを意識してビジネスロジックを記述できます。
アダプタの実装
アダプタは、ポートのインターフェースを満たしつつ、外部システムとの接続を担当します。例えば、データベースにユーザー情報を保存するためのUserRepositoryの実装を以下のように行います。
// src/adapters/repositories/UserRepositoryImpl.ts
import { UserRepository } from '../../core/ports/UserRepository';
import { User } from '../../core/entities/User';
export class UserRepositoryImpl implements UserRepository {
async findById(id: string): Promise<User | null> {
// 実際のデータベース操作(例:SQLクエリ)を実装
}
async save(user: User): Promise<User> {
// 実際のデータベース操作(例:インサート)を実装
}
}
UserRepositoryImplはUserRepositoryインターフェースを実装し、具体的なデータ操作を担当するアダプタとして動作します。
インターフェースの実装(外部エンドポイント)
インターフェース(プレゼンテーション層)は、アプリケーションの外部との接点であり、リクエストの受け付けやレスポンスの提供を行います。例えば、REST APIでユーザー情報の取得エンドポイントを以下のように定義します。
// src/interfaces/controllers/UserController.ts
import { UserService } from '../../core/usecases/UserService';
import { UserRepositoryImpl } from '../../adapters/repositories/UserRepositoryImpl';
const userService = new UserService(new UserRepositoryImpl());
export const getUserHandler = async (req, res) => {
const userId = req.params.id;
const user = await userService.getUserById(userId);
if (user) {
res.json(user);
} else {
res.status(404).json({ message: 'User not found' });
}
};
このように、UserControllerではビジネスロジックを持たず、あくまでUserServiceやUserRepositoryImplといった他の層に処理を委譲します。
TypeScriptでヘキサゴナルアーキテクチャを実装する際のポイント
- 依存性逆転の原則
アプリケーションコアが外部の実装に依存しないよう、すべての依存性をポートインターフェースを通じて注入します。
- 型安全なインターフェース設計
TypeScript
のインターフェースを用いて依存関係を明示的に定義することで、型安全なインタラクションが可能になります。 - モックの利用によるテスト容易性
ポートを介した依存関係の分離により、外部サービスをモック化し、ビジネスロジックのテストが容易になります。
まとめ
TypeScript
を用いたヘキサゴナルアーキテクチャの実装により、各コンポーネントが疎結合なまま維持され、柔軟かつ拡張性のあるシステム設計が実現できます。ポートとアダプタを明確に分離することで、アプリケーションコアが他の層に依存せず、保守性が向上します。依存関係逆転の原則と型安全なインターフェースを活用しながら、堅牢なアーキテクチャ設計に役立ててください。