EXERCISE 90分

ストーリー

高橋アーキテクト
Month 1 の集大成だ
高橋アーキテクト
ここまで学んだすべての知識 — コード品質、SOLID原則、デザインパターン、リファクタリング — を総動員して、このコードをレビューし、リファクタリングしてほしい
高橋アーキテクト
コードレビューでは”何が悪いか”だけでなく、“なぜ悪いのか”と”どう直すべきか”を説明できることが重要だ。設計者として、根拠を持って提案できるようになろう

ミッション概要

ミッションテーマ目安時間
Mission 1コードレビュー:問題の発見と言語化20分
Mission 2設計の改善:SOLID原則の適用20分
Mission 3パターンの適用:適切なパターンの選択20分
Mission 4総合リファクタリング:完成形を実装30分

対象コード:オンラインショップの注文システム

class OnlineShop {
  private orders: any[] = [];
  private inventory: Map<string, number> = new Map();

  processOrder(
    customerId: string,
    customerEmail: string,
    customerType: string,
    items: { productId: string; name: string; price: number; quantity: number }[],
    paymentMethod: string,
    cardNumber: string,
    shippingAddress: string,
    couponCode: string | null
  ): string {
    // 在庫チェック
    for (const item of items) {
      const stock = this.inventory.get(item.productId) ?? 0;
      if (stock < item.quantity) {
        return `ERROR: ${item.name} is out of stock`;
      }
    }

    // 小計計算
    let subtotal = 0;
    for (const item of items) {
      subtotal += item.price * item.quantity;
    }

    // 割引計算
    let discount = 0;
    if (couponCode === 'SAVE10') {
      discount = subtotal * 0.1;
    } else if (couponCode === 'SAVE20') {
      discount = subtotal * 0.2;
    } else if (couponCode === 'FLAT1000') {
      discount = 1000;
    }

    // 顧客タイプ別割引
    if (customerType === 'vip') {
      discount += subtotal * 0.05;
    } else if (customerType === 'premium') {
      discount += subtotal * 0.03;
    }

    // 送料計算
    let shippingFee = 0;
    if (subtotal - discount < 5000) {
      if (shippingAddress.includes('沖縄') || shippingAddress.includes('北海道')) {
        shippingFee = 1500;
      } else {
        shippingFee = 800;
      }
    }

    // 税金計算
    const taxableAmount = subtotal - discount + shippingFee;
    const tax = Math.round(taxableAmount * 0.1);
    const total = taxableAmount + tax;

    // 決済処理
    if (paymentMethod === 'credit_card') {
      if (cardNumber.length !== 16) {
        return 'ERROR: Invalid card number';
      }
      console.log(`Charging credit card ****${cardNumber.slice(-4)}: ${total}`);
    } else if (paymentMethod === 'bank_transfer') {
      console.log(`Bank transfer request: ${total}`);
    } else if (paymentMethod === 'cod') {
      if (total > 300000) {
        return 'ERROR: COD not available for orders over 300,000';
      }
      console.log(`COD order: ${total}`);
    }

    // 在庫更新
    for (const item of items) {
      const current = this.inventory.get(item.productId) ?? 0;
      this.inventory.set(item.productId, current - item.quantity);
    }

    // 注文記録
    const order = {
      id: `ORD-${Date.now()}`,
      customerId,
      items,
      subtotal,
      discount,
      shippingFee,
      tax,
      total,
      status: 'confirmed',
      createdAt: new Date(),
    };
    this.orders.push(order);

    // メール送信
    console.log(`Sending confirmation email to ${customerEmail}`);
    console.log(`Subject: Order ${order.id} Confirmed`);
    console.log(`Body: Thank you for your order! Total: ${total}`);

    // ポイント付与
    if (customerType !== 'guest') {
      const points = Math.floor(total * 0.01);
      console.log(`Adding ${points} points to customer ${customerId}`);
    }

    return `SUCCESS: Order ${order.id} created. Total: ${total}`;
  }
}

Mission 1: コードレビュー — 問題の発見と言語化(20分)

要件

上記のコードを分析し、以下の観点で問題点を一覧にしてください。

  1. コードスメル — どのコードスメルが見られるか
  2. SOLID原則違反 — どの原則に違反しているか
  3. 品質上の問題 — テスト容易性、保守性、信頼性の問題

各問題について「何が悪いか」「なぜ悪いか」「どう直すべきか」を説明してください。

解答

コードスメル

スメル箇所説明
長すぎるメソッドprocessOrder80行以上の巨大メソッド
パラメータが多すぎるprocessOrder の引数8個の引数、意味のまとまりがない
プリミティブ執着customerType, paymentMethodstring で表現、型安全でない
スイッチ文の乱用couponCode, customerType, paymentMethodif-else の連鎖が3箇所
特性の横恋慕割引計算、送料計算本来別のオブジェクトが持つべきロジック
神クラスOnlineShop在庫、注文、決済、通知を全て担当

SOLID原則違反

原則違反内容
SRP在庫管理、価格計算、決済処理、メール送信、ポイント付与が1メソッドに
OCP新しいクーポン・決済方法の追加で既存メソッドの修正が必要
DIPconsole.log による直接依存、外部サービスのインターフェースなし

品質上の問題

  • テスト容易性: console.log による副作用、外部依存の注入不可
  • 信頼性: エラー処理が文字列返却、例外なし
  • 保守性: 変更理由が多すぎ、影響範囲が不明

Mission 2: 設計の改善 — SOLID原則の適用(20分)

要件

以下のインターフェースとクラス構造を設計してください(実装は不要、インターフェース定義のみ)。

  1. 責任を分離したクラス構造(SRP)
  2. 拡張可能なクーポン・決済の仕組み(OCP)
  3. 抽象に依存するサービス間の関係(DIP)
解答
// === 値オブジェクト ===
type CustomerType = 'guest' | 'regular' | 'premium' | 'vip';
type OrderStatus = 'pending' | 'confirmed' | 'shipped' | 'delivered' | 'cancelled';

interface OrderItem {
  productId: string;
  name: string;
  price: number;
  quantity: number;
}

interface ShippingAddress {
  fullAddress: string;
  region: string;
}

// === ポート(インターフェース) ===
interface InventoryService {
  checkAvailability(items: OrderItem[]): boolean;
  reserveItems(items: OrderItem[]): void;
  releaseItems(items: OrderItem[]): void;
}

interface PricingService {
  calculateSubtotal(items: OrderItem[]): number;
  calculateDiscount(subtotal: number, customerType: CustomerType, couponCode: string | null): number;
  calculateShipping(subtotal: number, discount: number, address: ShippingAddress): number;
  calculateTax(taxableAmount: number): number;
}

interface PaymentGateway {
  charge(amount: number, paymentDetails: PaymentDetails): PaymentResult;
}

interface NotificationService {
  sendOrderConfirmation(email: string, order: Order): void;
}

interface LoyaltyService {
  addPoints(customerId: string, amount: number): void;
}

interface OrderRepository {
  save(order: Order): void;
  findById(id: string): Order | null;
}

// === クーポン(OCP) ===
interface CouponStrategy {
  readonly code: string;
  calculateDiscount(subtotal: number): number;
}

// === 決済(OCP) ===
interface PaymentMethod {
  readonly name: string;
  validate(details: PaymentDetails): boolean;
  process(amount: number, details: PaymentDetails): PaymentResult;
}

// === ユースケース ===
interface PlaceOrderUseCase {
  execute(request: PlaceOrderRequest): PlaceOrderResult;
}

Mission 3: パターンの適用 — 適切なパターンの選択(20分)

要件

Mission 2 の設計に基づいて、以下のパターンを具体的に適用してください。

  1. Strategy パターン — クーポン割引の種類切り替え
  2. Factory パターン — 決済方法の生成
  3. Observer パターン — 注文完了後の通知
解答
// === Strategy: クーポン割引 ===
class PercentageCoupon implements CouponStrategy {
  constructor(readonly code: string, private rate: number) {}
  calculateDiscount(subtotal: number): number {
    return subtotal * this.rate;
  }
}

class FixedAmountCoupon implements CouponStrategy {
  constructor(readonly code: string, private amount: number) {}
  calculateDiscount(subtotal: number): number {
    return Math.min(this.amount, subtotal);
  }
}

class CouponRegistry {
  private coupons: Map<string, CouponStrategy> = new Map();

  register(coupon: CouponStrategy): void {
    this.coupons.set(coupon.code, coupon);
  }

  find(code: string): CouponStrategy | undefined {
    return this.coupons.get(code);
  }
}

// 登録
const couponRegistry = new CouponRegistry();
couponRegistry.register(new PercentageCoupon('SAVE10', 0.1));
couponRegistry.register(new PercentageCoupon('SAVE20', 0.2));
couponRegistry.register(new FixedAmountCoupon('FLAT1000', 1000));

// === Factory: 決済方法 ===
class CreditCardPayment implements PaymentMethod {
  readonly name = 'credit_card';
  validate(details: PaymentDetails): boolean {
    return details.cardNumber !== undefined && details.cardNumber.length === 16;
  }
  process(amount: number, details: PaymentDetails): PaymentResult {
    return { success: true, transactionId: `cc-${Date.now()}` };
  }
}

class BankTransferPayment implements PaymentMethod {
  readonly name = 'bank_transfer';
  validate(details: PaymentDetails): boolean { return true; }
  process(amount: number, details: PaymentDetails): PaymentResult {
    return { success: true, transactionId: `bt-${Date.now()}` };
  }
}

class CodPayment implements PaymentMethod {
  readonly name = 'cod';
  validate(details: PaymentDetails): boolean { return true; }
  process(amount: number, details: PaymentDetails): PaymentResult {
    if (amount > 300000) return { success: false, error: 'COD limit exceeded' };
    return { success: true, transactionId: `cod-${Date.now()}` };
  }
}

class PaymentMethodFactory {
  private methods: Map<string, PaymentMethod> = new Map();

  register(method: PaymentMethod): void {
    this.methods.set(method.name, method);
  }

  create(name: string): PaymentMethod {
    const method = this.methods.get(name);
    if (!method) throw new Error(`Unknown payment method: ${name}`);
    return method;
  }
}

// === Observer: 注文完了通知 ===
interface OrderEventHandler {
  onOrderPlaced(order: Order): void;
}

class EmailNotificationHandler implements OrderEventHandler {
  onOrderPlaced(order: Order): void {
    console.log(`Sending confirmation email for ${order.id}`);
  }
}

class LoyaltyPointHandler implements OrderEventHandler {
  onOrderPlaced(order: Order): void {
    const points = Math.floor(order.total * 0.01);
    console.log(`Adding ${points} points`);
  }
}

class OrderEventDispatcher {
  private handlers: OrderEventHandler[] = [];

  subscribe(handler: OrderEventHandler): void {
    this.handlers.push(handler);
  }

  dispatchOrderPlaced(order: Order): void {
    for (const handler of this.handlers) {
      handler.onOrderPlaced(order);
    }
  }
}

Mission 4: 総合リファクタリング — 完成形を実装(30分)

要件

Mission 1-3 の成果を統合し、PlaceOrderUseCase を完成させてください。元のコードと同じ振る舞いを、SOLID原則とデザインパターンに基づいた美しい設計で実現してください。

解答
interface PlaceOrderRequest {
  customerId: string;
  customerEmail: string;
  customerType: CustomerType;
  items: OrderItem[];
  paymentMethodName: string;
  paymentDetails: PaymentDetails;
  shippingAddress: ShippingAddress;
  couponCode: string | null;
}

interface PlaceOrderResult {
  success: boolean;
  orderId?: string;
  total?: number;
  error?: string;
}

class PlaceOrderService implements PlaceOrderUseCase {
  constructor(
    private inventoryService: InventoryService,
    private pricingService: PricingService,
    private paymentFactory: PaymentMethodFactory,
    private orderRepository: OrderRepository,
    private eventDispatcher: OrderEventDispatcher
  ) {}

  execute(request: PlaceOrderRequest): PlaceOrderResult {
    // 在庫チェック
    if (!this.inventoryService.checkAvailability(request.items)) {
      return { success: false, error: 'One or more items are out of stock' };
    }

    // 価格計算
    const subtotal = this.pricingService.calculateSubtotal(request.items);
    const discount = this.pricingService.calculateDiscount(
      subtotal, request.customerType, request.couponCode
    );
    const shippingFee = this.pricingService.calculateShipping(
      subtotal, discount, request.shippingAddress
    );
    const taxableAmount = subtotal - discount + shippingFee;
    const tax = this.pricingService.calculateTax(taxableAmount);
    const total = taxableAmount + tax;

    // 決済処理
    const paymentMethod = this.paymentFactory.create(request.paymentMethodName);
    if (!paymentMethod.validate(request.paymentDetails)) {
      return { success: false, error: 'Invalid payment details' };
    }
    const paymentResult = paymentMethod.process(total, request.paymentDetails);
    if (!paymentResult.success) {
      return { success: false, error: paymentResult.error };
    }

    // 在庫確保
    this.inventoryService.reserveItems(request.items);

    // 注文保存
    const order: Order = {
      id: `ORD-${Date.now()}`,
      customerId: request.customerId,
      customerEmail: request.customerEmail,
      items: request.items,
      subtotal,
      discount,
      shippingFee,
      tax,
      total,
      status: 'confirmed',
      createdAt: new Date(),
    };
    this.orderRepository.save(order);

    // イベント発行(通知、ポイント付与など)
    this.eventDispatcher.dispatchOrderPlaced(order);

    return { success: true, orderId: order.id, total };
  }
}

達成度チェック

ミッションテーマ完了
Mission 1コードレビュー:問題の発見と言語化
Mission 2設計の改善:SOLID原則の適用
Mission 3パターンの適用:適切なパターンの選択
Mission 4総合リファクタリング:完成形を実装

まとめ

ポイント内容
コードレビュー問題を発見し、根拠を持って説明する
設計改善SOLID原則に基づいてクラス構造を設計する
パターン適用適切なパターンを選択して実装する
総合力知識を組み合わせて実践的な設計ができる

チェックリスト

  • コードスメルを発見し、名前で指摘できる
  • SOLID原則違反を特定し、改善方法を提案できる
  • 適切なデザインパターンを選択して適用できる
  • レガシーコードを段階的にリファクタリングできる

次のステップへ

最後に卒業クイズです。Month 1 で学んだすべての知識を確認しましょう。


推定読了時間: 90分