モジュールとは何か

モジュールの定義と役割

モジュールは、関連する機能をグループ化し、明確なインターフェースを通じて他のモジュールと連携する独立した単位です。

良いモジュールの特徴

  • 高凝集:関連する機能が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の仕様遵守
  • 回帰テスト:後方互換性の確認