技術選定で失敗しないための「3つの視点」と判断フレームワーク
プロジェクトの技術選定は後から変更が困難な重要な決断です。経験から学んだ技術選定の考え方と、実践的な判断基準、評価フレームワークを徹底解説します。
Clean Architectureの基本原則から実装方法まで詳しく解説。依存性逆転の原則、各層の責務、実践的な実装例を通じて、保守性の高いアプリケーション設計を学びます。
現代のソフトウェア開発において、Clean Architectureは保守性、テスタビリティ、拡張性を実現するための重要な設計パターンです。Uncle Bob(Robert C. Martin)によって提唱されたこのアーキテクチャは、ビジネスロジックを外部の詳細から独立させることで、変更に強い設計を実現します。
本記事では、Clean Architecture の基本原則から実践的な実装方法まで、2025 年の最新トレンドを踏まえて徹底的に解説します。
Clean Architecture は、ソフトウェアシステムを同心円状の層に分離し、内側の層が外側の層に依存しないように設計するアーキテクチャパターンです。この設計により、ビジネスロジックがフレームワークやデータベース、UI などの詳細から独立し、変更に強いシステムを構築できます。
チャートを読み込み中...
Clean Architecture は以下の 4 つの重要な原則に基づいています:
原則 | 説明 | 効果 |
---|---|---|
フレームワーク独立性 | ビジネスロジックが特定のフレームワークに依存しない | フレームワークの変更が容易 |
テスタビリティ | ビジネスルールが外部要素なしでテスト可能 | ユニットテストの実装が簡単 |
UI独立性 | UIの変更がシステムの他の部分に影響しない | UI技術の変更が容易 |
データベース独立性 | ビジネスルールがデータベースに依存しない | データベースの変更が容易 |
エンティティ層は最も内側の層で、ビジネスの核となるルールとデータを含みます。この層は外部の変更から完全に独立している必要があります。
// domain/entities/User.ts
export class User {
constructor(
private readonly id: string,
private readonly email: string,
private readonly name: string,
private readonly createdAt: Date
) {}
// ビジネスルール:メールアドレスの検証
static validateEmail(email: string): boolean {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(email);
}
// ビジネスルール:パスワードの強度チェック
static validatePasswordStrength(password: string): {
isValid: boolean;
errors: string[];
} {
const errors: string[] = [];
if (password.length < 8) {
errors.push('パスワードは8文字以上である必要があります');
}
if (!/[A-Z]/.test(password)) {
errors.push('大文字を含む必要があります');
}
if (!/[a-z]/.test(password)) {
errors.push('小文字を含む必要があります');
}
if (!/[0-9]/.test(password)) {
errors.push('数字を含む必要があります');
}
return {
isValid: errors.length === 0,
errors
};
}
// ゲッターメソッド
getId(): string {
return this.id;
}
getEmail(): string {
return this.email;
}
getName(): string {
return this.name;
}
}
ユースケース層は、アプリケーション固有のビジネスルールを含みます。エンティティを操作し、データの流れを調整します。
// application/usecases/CreateUserUseCase.ts
import { User } from '../../domain/entities/User';
import { IUserRepository } from '../../domain/repositories/IUserRepository';
import { IEmailService } from '../../domain/services/IEmailService';
export interface CreateUserInput {
email: string;
name: string;
password: string;
}
export interface CreateUserOutput {
id: string;
email: string;
name: string;
createdAt: Date;
}
export class CreateUserUseCase {
constructor(
private readonly userRepository: IUserRepository,
private readonly emailService: IEmailService
) {}
async execute(input: CreateUserInput): Promise<CreateUserOutput> {
// 入力検証
if (!User.validateEmail(input.email)) {
throw new Error('無効なメールアドレスです');
}
const passwordValidation = User.validatePasswordStrength(input.password);
if (!passwordValidation.isValid) {
throw new Error(
`パスワードが要件を満たしていません: ${passwordValidation.errors.join(', ')}`
);
}
// 重複チェック
const existingUser = await this.userRepository.findByEmail(input.email);
if (existingUser) {
throw new Error('このメールアドレスは既に使用されています');
}
// ユーザー作成
const user = new User(
this.generateId(),
input.email,
input.name,
new Date()
);
// 保存
await this.userRepository.save(user);
// ウェルカムメール送信
await this.emailService.sendWelcomeEmail(user.getEmail(), user.getName());
// 出力の作成
return {
id: user.getId(),
email: user.getEmail(),
name: user.getName(),
createdAt: new Date()
};
}
private generateId(): string {
return `user_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
}
}
この層は、ユースケースと外部世界をつなぐアダプターの役割を果たします。コントローラー、プレゼンター、ゲートウェイなどが含まれます。
// 従来の密結合な実装
class UserController {
async createUser(req: Request, res: Response) {
const { email, name, password } = req.body;
// 直接データベース操作
const user = await db.users.create({
email,
name,
password: await bcrypt.hash(password, 10)
});
// 直接メール送信
await sendEmail({
to: email,
subject: 'Welcome!',
body: `Hello ${name}`
});
res.json(user);
}
}
// Clean Architectureに基づく実装
class UserController {
constructor(
private readonly createUserUseCase: CreateUserUseCase
) {}
async createUser(req: Request, res: Response) {
try {
const input: CreateUserInput = {
email: req.body.email,
name: req.body.name,
password: req.body.password
};
const output = await this.createUserUseCase.execute(input);
res.status(201).json({
success: true,
data: output
});
} catch (error) {
res.status(400).json({
success: false,
error: error.message
});
}
}
}
// 従来の密結合な実装
class UserController {
async createUser(req: Request, res: Response) {
const { email, name, password } = req.body;
// 直接データベース操作
const user = await db.users.create({
email,
name,
password: await bcrypt.hash(password, 10)
});
// 直接メール送信
await sendEmail({
to: email,
subject: 'Welcome!',
body: `Hello ${name}`
});
res.json(user);
}
}
// Clean Architectureに基づく実装
class UserController {
constructor(
private readonly createUserUseCase: CreateUserUseCase
) {}
async createUser(req: Request, res: Response) {
try {
const input: CreateUserInput = {
email: req.body.email,
name: req.body.name,
password: req.body.password
};
const output = await this.createUserUseCase.execute(input);
res.status(201).json({
success: true,
data: output
});
} catch (error) {
res.status(400).json({
success: false,
error: error.message
});
}
}
}
最外層には、具体的な実装技術が配置されます。データベース、Web フレームワーク、外部サービスとの連携などです。
// infrastructure/repositories/UserRepository.ts
import { IUserRepository } from '../../domain/repositories/IUserRepository';
import { User } from '../../domain/entities/User';
import { PrismaClient } from '@prisma/client';
export class UserRepository implements IUserRepository {
constructor(private readonly prisma: PrismaClient) {}
async save(user: User): Promise<void> {
await this.prisma.user.create({
data: {
id: user.getId(),
email: user.getEmail(),
name: user.getName(),
createdAt: new Date()
}
});
}
async findByEmail(email: string): Promise<User | null> {
const userData = await this.prisma.user.findUnique({
where: { email }
});
if (!userData) {
return null;
}
return new User(
userData.id,
userData.email,
userData.name,
userData.createdAt
);
}
async findById(id: string): Promise<User | null> {
const userData = await this.prisma.user.findUnique({
where: { id }
});
if (!userData) {
return null;
}
return new User(
userData.id,
userData.email,
userData.name,
userData.createdAt
);
}
}
Clean Architecture の核心は、依存性逆転の原則です。内側の層は外側の層に依存せず、代わりにインターフェースを通じて連携します。
チャートを読み込み中...
// domain/repositories/IUserRepository.ts
import { User } from '../entities/User';
export interface IUserRepository {
save(user: User): Promise<void>;
findByEmail(email: string): Promise<User | null>;
findById(id: string): Promise<User | null>;
update(user: User): Promise<void>;
delete(id: string): Promise<void>;
}
// domain/services/IEmailService.ts
export interface IEmailService {
sendWelcomeEmail(email: string, name: string): Promise<void>;
sendPasswordResetEmail(email: string, token: string): Promise<void>;
sendAccountDeletionConfirmation(email: string): Promise<void>;
}
Clean Architecture の大きな利点の 1 つは、高いテスタビリティです。各層が独立しているため、モックを使用したユニットテストが容易に実装できます。
// tests/usecases/CreateUserUseCase.test.ts
import { CreateUserUseCase } from '../../application/usecases/CreateUserUseCase';
import { IUserRepository } from '../../domain/repositories/IUserRepository';
import { IEmailService } from '../../domain/services/IEmailService';
describe('CreateUserUseCase', () => {
let useCase: CreateUserUseCase;
let mockUserRepository: jest.Mocked<IUserRepository>;
let mockEmailService: jest.Mocked<IEmailService>;
beforeEach(() => {
// モックの作成
mockUserRepository = {
save: jest.fn(),
findByEmail: jest.fn(),
findById: jest.fn(),
update: jest.fn(),
delete: jest.fn()
};
mockEmailService = {
sendWelcomeEmail: jest.fn(),
sendPasswordResetEmail: jest.fn(),
sendAccountDeletionConfirmation: jest.fn()
};
useCase = new CreateUserUseCase(mockUserRepository, mockEmailService);
});
it('正常にユーザーを作成できる', async () => {
// Arrange
const input = {
email: 'test@example.com',
name: 'Test User',
password: 'StrongPass123'
};
mockUserRepository.findByEmail.mockResolvedValue(null);
// Act
const result = await useCase.execute(input);
// Assert
expect(result.email).toBe(input.email);
expect(result.name).toBe(input.name);
expect(mockUserRepository.save).toHaveBeenCalledTimes(1);
expect(mockEmailService.sendWelcomeEmail).toHaveBeenCalledWith(
input.email,
input.name
);
});
it('既存のメールアドレスの場合はエラーを投げる', async () => {
// Arrange
const input = {
email: 'existing@example.com',
name: 'Test User',
password: 'StrongPass123'
};
mockUserRepository.findByEmail.mockResolvedValue({} as any);
// Act & Assert
await expect(useCase.execute(input)).rejects.toThrow(
'このメールアドレスは既に使用されています'
);
});
});
より複雑な例として、EC サイトの注文処理システムを Clean Architecture で実装してみましょう。
// domain/entities/Order.ts
export class Order {
private items: OrderItem[] = [];
private status: OrderStatus = OrderStatus.PENDING;
constructor(
private readonly id: string,
private readonly customerId: string,
private readonly createdAt: Date
) {}
addItem(productId: string, quantity: number, price: number): void {
const existingItem = this.items.find(item => item.productId === productId);
if (existingItem) {
existingItem.quantity += quantity;
} else {
this.items.push(new OrderItem(productId, quantity, price));
}
}
getTotalAmount(): number {
return this.items.reduce((total, item) => total + item.getSubtotal(), 0);
}
canBeCancelled(): boolean {
return this.status === OrderStatus.PENDING ||
this.status === OrderStatus.CONFIRMED;
}
cancel(): void {
if (!this.canBeCancelled()) {
throw new Error('この注文はキャンセルできません');
}
this.status = OrderStatus.CANCELLED;
}
// その他のビジネスロジック...
}
// domain/entities/OrderItem.ts
export class OrderItem {
constructor(
public readonly productId: string,
public quantity: number,
public readonly price: number
) {}
getSubtotal(): number {
return this.quantity * this.price;
}
}
// domain/entities/OrderStatus.ts
export enum OrderStatus {
PENDING = 'PENDING',
CONFIRMED = 'CONFIRMED',
PROCESSING = 'PROCESSING',
SHIPPED = 'SHIPPED',
DELIVERED = 'DELIVERED',
CANCELLED = 'CANCELLED'
}
// application/usecases/CreateOrderUseCase.ts
export class CreateOrderUseCase {
constructor(
private readonly orderRepository: IOrderRepository,
private readonly productRepository: IProductRepository,
private readonly inventoryService: IInventoryService,
private readonly paymentService: IPaymentService,
private readonly notificationService: INotificationService
) {}
async execute(input: CreateOrderInput): Promise<CreateOrderOutput> {
// 1. 商品の存在確認と在庫チェック
const products = await this.validateAndGetProducts(input.items);
// 2. 注文の作成
const order = new Order(
this.generateOrderId(),
input.customerId,
new Date()
);
// 3. 注文アイテムの追加
for (const item of input.items) {
const product = products.find(p => p.id === item.productId)!;
order.addItem(item.productId, item.quantity, product.price);
}
// 4. 在庫の予約
await this.inventoryService.reserveItems(
input.items.map(item => ({
productId: item.productId,
quantity: item.quantity
}))
);
try {
// 5. 支払い処理
const paymentResult = await this.paymentService.processPayment({
amount: order.getTotalAmount(),
customerId: input.customerId,
paymentMethodId: input.paymentMethodId
});
if (!paymentResult.success) {
throw new Error('支払い処理に失敗しました');
}
// 6. 注文の保存
await this.orderRepository.save(order);
// 7. 通知送信
await this.notificationService.sendOrderConfirmation(
input.customerId,
order.getId()
);
return {
orderId: order.getId(),
totalAmount: order.getTotalAmount(),
status: 'CONFIRMED'
};
} catch (error) {
// エラー時は在庫予約を解除
await this.inventoryService.releaseReservation(
input.items.map(item => ({
productId: item.productId,
quantity: item.quantity
}))
);
throw error;
}
}
private async validateAndGetProducts(items: OrderItemInput[]): Promise<Product[]> {
const products: Product[] = [];
for (const item of items) {
const product = await this.productRepository.findById(item.productId);
if (!product) {
throw new Error(`商品が見つかりません: ${item.productId}`);
}
const stock = await this.inventoryService.getAvailableStock(item.productId);
if (stock < item.quantity) {
throw new Error(`在庫が不足しています: ${product.name}`);
}
products.push(product);
}
return products;
}
private generateOrderId(): string {
return `ORD_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
}
}
Clean Architecture を実装する際の推奨プロジェクト構造を紹介します。
src/
├── domain/ # ビジネスロジック層
│ ├── entities/ # エンティティ
│ │ ├── User.ts
│ │ ├── Order.ts
│ │ └── Product.ts
│ ├── repositories/ # リポジトリインターフェース
│ │ ├── IUserRepository.ts
│ │ └── IOrderRepository.ts
│ ├── services/ # ドメインサービスインターフェース
│ │ ├── IEmailService.ts
│ │ └── IPaymentService.ts
│ └── value-objects/ # 値オブジェクト
│ ├── Email.ts
│ └── Money.ts
├── application/ # アプリケーション層
│ ├── usecases/ # ユースケース
│ │ ├── CreateUserUseCase.ts
│ │ └── CreateOrderUseCase.ts
│ └── dto/ # データ転送オブジェクト
│ ├── CreateUserDto.ts
│ └── CreateOrderDto.ts
├── infrastructure/ # インフラストラクチャ層
│ ├── repositories/ # リポジトリ実装
│ │ ├── UserRepository.ts
│ │ └── OrderRepository.ts
│ ├── services/ # サービス実装
│ │ ├── EmailService.ts
│ │ └── StripePaymentService.ts
│ ├── database/ # データベース設定
│ │ ├── prisma/
│ │ └── migrations/
│ └── config/ # 設定ファイル
│ └── database.config.ts
├── presentation/ # プレゼンテーション層
│ ├── controllers/ # コントローラー
│ │ ├── UserController.ts
│ │ └── OrderController.ts
│ ├── routes/ # ルーティング
│ │ └── index.ts
│ └── middleware/ # ミドルウェア
│ ├── auth.ts
│ └── validation.ts
└── tests/ # テスト
├── unit/
├── integration/
└── e2e/
Clean Architecture の実装を段階的に進める際の推奨アプローチです。
エンティティとビジネスルールの定義
アプリケーションロジックの実装
コントローラーとプレゼンターの実装
データベースと外部サービスの統合
ユニットテストと統合テストの実装
Clean Architecture は保守性を重視する設計ですが、パフォーマンスへの配慮も重要です。
// infrastructure/cache/RedisCacheService.ts
export class RedisCacheService implements ICacheService {
constructor(private readonly redis: Redis) {}
async get<T>(key: string): Promise<T | null> {
const data = await this.redis.get(key);
return data ? JSON.parse(data) : null;
}
async set<T>(key: string, value: T, ttl?: number): Promise<void> {
const data = JSON.stringify(value);
if (ttl) {
await this.redis.setex(key, ttl, data);
} else {
await this.redis.set(key, data);
}
}
async delete(key: string): Promise<void> {
await this.redis.del(key);
}
}
// application/usecases/GetUserUseCase.ts
export class GetUserUseCase {
constructor(
private readonly userRepository: IUserRepository,
private readonly cacheService: ICacheService
) {}
async execute(userId: string): Promise<User> {
// キャッシュチェック
const cacheKey = `user:${userId}`;
const cachedUser = await this.cacheService.get<User>(cacheKey);
if (cachedUser) {
return cachedUser;
}
// データベースから取得
const user = await this.userRepository.findById(userId);
if (!user) {
throw new Error('ユーザーが見つかりません');
}
// キャッシュに保存(TTL: 1時間)
await this.cacheService.set(cacheKey, user, 3600);
return user;
}
}
Clean Architecture 実装時によく遭遇する問題と解決策を紹介します。
問題 | 症状 | 原因 | 解決策 |
---|---|---|---|
循環依存 | コンパイルエラー、import cycle | 層の境界が曖昧 | インターフェースの適切な配置、依存性の方向を再確認 |
過度な抽象化 | 単純な処理が複雑に | 原則の過剰適用 | 規模に応じた適切なレベルの抽象化 |
パフォーマンス劣化 | レスポンス時間の増加 | 過度な層の分離 | クリティカルパスの最適化、適切なキャッシング |
テストの複雑化 | モックが大量に必要 | インターフェースの粒度が細かすぎる | インターフェースの統合、テストヘルパーの作成 |
// ❌ 循環依存が発生するパターン
// domain/services/UserService.ts
import { UserRepository } from '../../infrastructure/repositories/UserRepository';
// infrastructure/repositories/UserRepository.ts
import { UserService } from '../../domain/services/UserService';
// ✅ インターフェースで解決
// domain/repositories/IUserRepository.ts
export interface IUserRepository {
findById(id: string): Promise<User | null>;
}
// domain/services/UserService.ts
import { IUserRepository } from '../repositories/IUserRepository';
export class UserService {
constructor(private readonly userRepository: IUserRepository) {}
}
// infrastructure/repositories/UserRepository.ts
import { IUserRepository } from '../../domain/repositories/IUserRepository';
export class UserRepository implements IUserRepository {
// 実装
}
// application/usecases/GetProductsUseCase.ts
export class GetProductsUseCase {
private cache = new Map<string, CacheEntry<Product[]>>();
constructor(
private readonly productRepository: IProductRepository,
private readonly cacheService: ICacheService
) {}
async execute(params: GetProductsParams): Promise<Product[]> {
const cacheKey = this.generateCacheKey(params);
// メモリキャッシュチェック
const memoryCache = this.cache.get(cacheKey);
if (memoryCache && !this.isExpired(memoryCache)) {
return memoryCache.data;
}
// Redis キャッシュチェック
const redisCache = await this.cacheService.get<Product[]>(cacheKey);
if (redisCache) {
this.cache.set(cacheKey, {
data: redisCache,
timestamp: Date.now()
});
return redisCache;
}
// データベースから取得
const products = await this.productRepository.findByParams(params);
// キャッシュに保存(非同期)
this.saveToCache(cacheKey, products);
return products;
}
private async saveToCache(key: string, data: Product[]): Promise<void> {
// メモリキャッシュ
this.cache.set(key, {
data,
timestamp: Date.now()
});
// Redisキャッシュ(非ブロッキング)
this.cacheService.set(key, data, 3600).catch(err => {
console.error('Cache save error:', err);
});
}
private isExpired(entry: CacheEntry<any>): boolean {
return Date.now() - entry.timestamp > 60000; // 1分
}
}
// application/usecases/BulkCreateOrdersUseCase.ts
export class BulkCreateOrdersUseCase {
constructor(
private readonly orderRepository: IOrderRepository,
private readonly inventoryService: IInventoryService,
private readonly eventBus: IEventBus
) {}
async execute(orderInputs: CreateOrderInput[]): Promise<BulkCreateResult> {
const results: OrderResult[] = [];
const batchSize = 100;
// バッチ処理で効率化
for (let i = 0; i < orderInputs.length; i += batchSize) {
const batch = orderInputs.slice(i, i + batchSize);
// 並列処理
const batchResults = await Promise.all(
batch.map(async (input) => {
try {
// 在庫の一括確認
const inventory = await this.inventoryService.checkBulk(
input.items.map(item => ({
productId: item.productId,
quantity: item.quantity
}))
);
if (!inventory.allAvailable) {
return {
success: false,
orderId: null,
error: '在庫不足',
input
};
}
// 注文作成
const order = await this.createOrder(input);
return {
success: true,
orderId: order.id,
error: null,
input
};
} catch (error) {
return {
success: false,
orderId: null,
error: error.message,
input
};
}
})
);
results.push(...batchResults);
// バッチごとにイベント発行
await this.eventBus.publish(
new BulkOrdersProcessedEvent(batchResults)
);
}
return {
total: orderInputs.length,
successful: results.filter(r => r.success).length,
failed: results.filter(r => !r.success).length,
results
};
}
}
// domain/events/OrderEvents.ts
export class OrderCreatedEvent {
constructor(
public readonly orderId: string,
public readonly customerId: string,
public readonly totalAmount: number,
public readonly items: OrderItem[]
) {}
}
// application/eventHandlers/OrderEventHandler.ts
export class OrderEventHandler {
constructor(
private readonly emailService: IEmailService,
private readonly analyticsService: IAnalyticsService,
private readonly inventoryService: IInventoryService
) {}
@EventHandler(OrderCreatedEvent)
async handleOrderCreated(event: OrderCreatedEvent): Promise<void> {
// 非同期で並列処理
await Promise.allSettled([
this.sendOrderConfirmationEmail(event),
this.trackOrderAnalytics(event),
this.updateInventory(event)
]);
}
private async sendOrderConfirmationEmail(event: OrderCreatedEvent): Promise<void> {
try {
await this.emailService.sendOrderConfirmation({
orderId: event.orderId,
customerId: event.customerId,
amount: event.totalAmount
});
} catch (error) {
// エラーログだけ記録し、処理は継続
console.error('Email sending failed:', error);
}
}
}
// domain/errors/DomainErrors.ts
export abstract class DomainError extends Error {
constructor(
message: string,
public readonly code: string,
public readonly statusCode: number = 400
) {
super(message);
this.name = this.constructor.name;
}
}
export class ValidationError extends DomainError {
constructor(message: string, public readonly fields?: Record<string, string[]>) {
super(message, 'VALIDATION_ERROR', 400);
}
}
export class NotFoundError extends DomainError {
constructor(resource: string, id: string) {
super(`${resource} with id ${id} not found`, 'NOT_FOUND', 404);
}
}
export class BusinessRuleViolationError extends DomainError {
constructor(message: string) {
super(message, 'BUSINESS_RULE_VIOLATION', 422);
}
}
// application/usecases/BaseUseCase.ts
export abstract class BaseUseCase<TInput, TOutput> {
protected abstract validate(input: TInput): Promise<void>;
protected abstract process(input: TInput): Promise<TOutput>;
async execute(input: TInput): Promise<TOutput> {
try {
await this.validate(input);
return await this.process(input);
} catch (error) {
if (error instanceof DomainError) {
throw error;
}
// 予期しないエラーをラップ
throw new DomainError(
'An unexpected error occurred',
'INTERNAL_ERROR',
500
);
}
}
}
// infrastructure/di/Container.ts
import { Container } from 'inversify';
import { TYPES } from './types';
export class DIContainer {
private container: Container;
constructor() {
this.container = new Container();
this.registerServices();
}
private registerServices(): void {
// Repositories
this.container.bind<IUserRepository>(TYPES.UserRepository)
.to(UserRepository)
.inSingletonScope();
this.container.bind<IOrderRepository>(TYPES.OrderRepository)
.to(OrderRepository)
.inSingletonScope();
// Services
this.container.bind<IEmailService>(TYPES.EmailService)
.to(EmailService)
.inSingletonScope();
this.container.bind<ICacheService>(TYPES.CacheService)
.to(RedisCacheService)
.inSingletonScope();
// Use Cases
this.container.bind<CreateUserUseCase>(TYPES.CreateUserUseCase)
.to(CreateUserUseCase)
.inTransientScope();
this.container.bind<CreateOrderUseCase>(TYPES.CreateOrderUseCase)
.to(CreateOrderUseCase)
.inTransientScope();
}
get<T>(serviceIdentifier: symbol): T {
return this.container.get<T>(serviceIdentifier);
}
}
// 使用例
const container = new DIContainer();
const createUserUseCase = container.get<CreateUserUseCase>(TYPES.CreateUserUseCase);
// domain/aggregates/Order.ts
export class Order {
private items: OrderItem[] = [];
private events: DomainEvent[] = [];
constructor(
private readonly id: OrderId,
private readonly customerId: CustomerId,
private status: OrderStatus
) {}
static create(customerId: CustomerId): Order {
const order = new Order(
OrderId.generate(),
customerId,
OrderStatus.PENDING
);
order.addEvent(new OrderCreatedEvent(order.id.value, customerId.value));
return order;
}
addItem(product: Product, quantity: Quantity): void {
// ビジネスルールの検証
if (this.status !== OrderStatus.PENDING) {
throw new BusinessRuleViolationError(
'確定済みの注文には商品を追加できません'
);
}
if (quantity.value > product.maxOrderQuantity) {
throw new BusinessRuleViolationError(
`最大注文数量(${product.maxOrderQuantity})を超えています`
);
}
const existingItem = this.items.find(
item => item.productId.equals(product.id)
);
if (existingItem) {
existingItem.increaseQuantity(quantity);
} else {
this.items.push(new OrderItem(product.id, quantity, product.price));
}
this.addEvent(new OrderItemAddedEvent(
this.id.value,
product.id.value,
quantity.value
));
}
getUncommittedEvents(): DomainEvent[] {
return [...this.events];
}
markEventsAsCommitted(): void {
this.events = [];
}
}
// infrastructure/messaging/EventBusAdapter.ts
export class EventBusAdapter implements IEventBus {
constructor(
private readonly kafkaProducer: KafkaProducer,
private readonly schemaRegistry: SchemaRegistry
) {}
async publish(event: DomainEvent): Promise<void> {
const schema = await this.schemaRegistry.getSchema(event.constructor.name);
const payload = this.serialize(event, schema);
await this.kafkaProducer.send({
topic: this.getTopicName(event),
messages: [{
key: event.aggregateId,
value: payload,
headers: {
eventType: event.constructor.name,
version: schema.version,
timestamp: Date.now().toString()
}
}]
});
}
private getTopicName(event: DomainEvent): string {
// イベントタイプからトピック名を生成
return `domain.events.${event.constructor.name.toLowerCase()}`;
}
}
Clean Architecture は、ソフトウェアの保守性と拡張性を大幅に向上させる強力な設計パターンです。本記事で解説した内容をまとめます:
Clean Architecture は学習曲線が高いですが、一度理解すれば、保守性の高い堅牢なシステムを構築できます。実際のプロジェクトでは、チームの技術レベルやプロジェクトの規模に応じて、適切なレベルで適用することが重要です。
本記事で紹介したトラブルシューティングやパフォーマンス最適化のテクニックを活用することで、実践的な Clean Architecture の実装が可能になります。