モジュール設計
パッケージ・ライブラリレベルでの責任分離と依存関係管理
モジュールとは何か
モジュールの定義と役割
モジュールは、関連する機能をグループ化し、明確なインターフェースを通じて他のモジュールと連携する独立した単位です。
良いモジュールの特徴
- 高凝集:関連する機能が1つのモジュールに集約
- 疎結合:他のモジュールへの依存が最小限
- 明確な責任:モジュールの目的が明確
- 安定したインターフェース:公開APIが頻繁に変更されない
- 独立性:単体でテスト・理解・再利用可能
モジュール境界の設計
責任によるモジュール分割
モジュールの境界は、責任の境界と一致すべきです。異なる理由で変更される機能は、異なるモジュールに属すべきです。
// ECサイトのモジュール構成例 project/ ├── modules/ │ ├── auth/ # 認証・認可の責任 │ │ ├── index.ts │ │ ├── services/ │ │ │ ├── AuthService.ts │ │ │ └── TokenService.ts │ │ ├── models/ │ │ │ └── User.ts │ │ └── interfaces/ │ │ └── IAuthProvider.ts │ │ │ ├── catalog/ # 商品カタログの責任 │ │ ├── index.ts │ │ ├── services/ │ │ │ ├── ProductService.ts │ │ │ └── CategoryService.ts │ │ ├── models/ │ │ │ ├── Product.ts │ │ │ └── Category.ts │ │ └── repositories/ │ │ └── ProductRepository.ts │ │ │ ├── cart/ # ショッピングカートの責任 │ │ ├── index.ts │ │ ├── services/ │ │ │ └── CartService.ts │ │ ├── models/ │ │ │ ├── Cart.ts │ │ │ └── CartItem.ts │ │ └── rules/ │ │ └── CartBusinessRules.ts │ │ │ ├── order/ # 注文処理の責任 │ │ ├── index.ts │ │ ├── services/ │ │ │ ├── OrderService.ts │ │ │ └── OrderValidator.ts │ │ ├── models/ │ │ │ └── Order.ts │ │ └── workflows/ │ │ └── OrderWorkflow.ts │ │ │ └── payment/ # 決済処理の責任 │ ├── index.ts │ ├── services/ │ │ └── PaymentService.ts │ ├── adapters/ │ │ ├── StripeAdapter.ts │ │ └── PayPalAdapter.ts │ └── interfaces/ │ └── IPaymentGateway.ts
公開APIの設計
モジュールインターフェースの原則
モジュールの公開APIは、内部実装を隠蔽し、必要最小限の機能のみを公開すべきです。
❌ 過度に公開されたAPI
// cart/index.ts
export * from './models/Cart';
export * from './models/CartItem';
export * from './services/CartService';
export * from './rules/CartBusinessRules';
export * from './utils/CartCalculator';
export * from './validators/CartValidator';
export * from './constants/CartConstants';
// 使用側で内部実装に依存してしまう
import {
Cart,
CartItem,
CartCalculator,
CART_MAX_ITEMS
} from '@modules/cart';
const calculator = new CartCalculator();
const total = calculator.calculateWithTax(items);
✅ 適切に制限されたAPI
// cart/index.ts
// 公開APIを明確に定義
export { CartService } from './services/CartService';
export { Cart, CartItem } from './types';
export type { ICartService } from './interfaces';
// cart/services/CartService.ts
export class CartService implements ICartService {
private calculator: CartCalculator;
private validator: CartValidator;
constructor() {
// 内部依存は隠蔽
this.calculator = new CartCalculator();
this.validator = new CartValidator();
}
addItem(cartId: string, item: CartItem): Promise {
// ビジネスルールは内部で処理
this.validator.validateItem(item);
// ...
}
checkout(cartId: string): Promise {
// 複雑な計算ロジックは隠蔽
const cart = await this.getCart(cartId);
const total = this.calculator.calculateTotal(cart);
// ...
}
}
// 使用側はシンプルなインターフェースのみ使用
import { CartService } from '@modules/cart';
const cartService = new CartService();
await cartService.addItem(cartId, item);
const result = await cartService.checkout(cartId);
依存関係の管理
モジュール間の依存ルール
モジュール間の依存関係は、システムの保守性に大きな影響を与えます。明確なルールに従って管理する必要があります。
// 依存関係の可視化
/*
[Presentation Layer]
↓
[Application Layer]
↓
[Domain Layer] ← 他の層はこれに依存
↑
[Infrastructure Layer]
*/
// 良い依存関係の例
// domain/models/Order.ts
export class Order {
constructor(
private id: string,
private customerId: string,
private items: OrderItem[]
) {}
// ドメインロジックのみ
calculateTotal(): Money {
return this.items.reduce(
(sum, item) => sum.add(item.getSubtotal()),
Money.zero()
);
}
}
// application/services/OrderService.ts
import { Order } from '@domain/models/Order';
import { IOrderRepository } from '@domain/interfaces/IOrderRepository';
import { IPaymentGateway } from '@domain/interfaces/IPaymentGateway';
export class OrderService {
constructor(
private orderRepository: IOrderRepository,
private paymentGateway: IPaymentGateway
) {}
async createOrder(orderData: CreateOrderDTO): Promise {
// ドメインモデルを使用
const order = new Order(
generateId(),
orderData.customerId,
orderData.items
);
// インターフェースを通じて依存
await this.paymentGateway.charge(order.calculateTotal());
await this.orderRepository.save(order);
return order;
}
}
// infrastructure/repositories/PostgresOrderRepository.ts
import { IOrderRepository } from '@domain/interfaces/IOrderRepository';
import { Order } from '@domain/models/Order';
export class PostgresOrderRepository implements IOrderRepository {
async save(order: Order): Promise {
// PostgreSQL固有の実装
// ドメイン層のインターフェースを実装
}
}
依存関係の原則
- 上位層は下位層に依存してはいけない
- ドメイン層は他の層に依存しない
- 循環依存を避ける
- 安定したモジュールに依存する
- インターフェースを通じて依存する
モジュールの凝集性
機能的凝集の実現
モジュール内の要素は、共通の目的のために協力して動作すべきです。
// 高凝集なモジュールの例:Emailモジュール
// email/types.ts
export interface Email {
to: string[];
cc?: string[];
bcc?: string[];
subject: string;
body: string;
attachments?: Attachment[];
}
export interface EmailTemplate {
name: string;
subject: string;
bodyTemplate: string;
variables: Record;
}
// email/services/EmailBuilder.ts
export class EmailBuilder {
private email: Partial = {};
to(...addresses: string[]): this {
this.email.to = addresses;
return this;
}
subject(subject: string): this {
this.email.subject = subject;
return this;
}
body(body: string): this {
this.email.body = body;
return this;
}
build(): Email {
if (!this.email.to || !this.email.subject || !this.email.body) {
throw new Error('Required fields missing');
}
return this.email as Email;
}
}
// email/services/TemplateEngine.ts
export class TemplateEngine {
render(template: EmailTemplate): string {
let body = template.bodyTemplate;
Object.entries(template.variables).forEach(([key, value]) => {
body = body.replace(new RegExp(`{{${key}}}`, 'g'), value);
});
return body;
}
}
// email/services/EmailService.ts
export class EmailService {
constructor(
private sender: IEmailSender,
private templateEngine: TemplateEngine
) {}
async sendEmail(email: Email): Promise {
await this.sender.send(email);
}
async sendTemplate(
template: EmailTemplate,
recipients: string[]
): Promise {
const body = this.templateEngine.render(template);
const email = new EmailBuilder()
.to(...recipients)
.subject(template.subject)
.body(body)
.build();
await this.sendEmail(email);
}
}
// email/index.ts - 公開API
export { EmailService } from './services/EmailService';
export { EmailBuilder } from './services/EmailBuilder';
export type { Email, EmailTemplate } from './types';
モジュールのバージョニング
後方互換性の維持
モジュールのAPIを変更する際は、既存の利用者への影響を最小限に抑える必要があります。
// セマンティックバージョニングの適用
// package.json
{
"name": "@myapp/auth",
"version": "2.1.0",
"exports": {
".": "./dist/index.js",
"./legacy": "./dist/legacy.js"
}
}
// 非推奨APIの段階的な削除
export class AuthService {
// 新しいAPI
async authenticate(credentials: Credentials): Promise {
// 実装
}
// 後方互換性のため維持(非推奨)
/**
* @deprecated Use authenticate() instead. Will be removed in v3.0.0
*/
async login(username: string, password: string): Promise {
console.warn('login() is deprecated. Use authenticate() instead.');
const result = await this.authenticate({ username, password });
return result.user;
}
}
モジュールのテスト戦略
モジュールテストの観点
- 単体テスト:モジュール内の各コンポーネント
- 統合テスト:モジュール間の連携
- 契約テスト:公開APIの仕様遵守
- 回帰テスト:後方互換性の確認