LESSON 30分

ストーリー

高橋アーキテクト
条件分岐は必要悪だ。でも、if-else の連鎖が10段も重なると、もはや読み解けるコードではない
高橋アーキテクト
今日は、複雑な条件分岐をスッキリ整理するリファクタリング技法を学ぼう。特に”Replace Conditional with Polymorphism”は強力だ

Guard Clause(ガード節)

ネストした条件分岐を早期リターンでフラットにします。

Before

function getPayAmount(employee: Employee): number {
  let result: number;
  if (employee.isSeparated) {
    result = separatedAmount(employee);
  } else {
    if (employee.isRetired) {
      result = retiredAmount(employee);
    } else {
      result = normalPayAmount(employee);
    }
  }
  return result;
}

After

function getPayAmount(employee: Employee): number {
  if (employee.isSeparated) return separatedAmount(employee);
  if (employee.isRetired) return retiredAmount(employee);
  return normalPayAmount(employee);
}

ガード節のルール:

  • 異常系・特殊ケースを先に処理して早期リターン
  • 正常系(メインロジック)がネストされずにフラットに書ける
  • 各条件が独立しているので理解しやすい

Decompose Conditional(条件の分解)

複雑な条件式を、意味のある名前を持つメソッドに分解します。

Before

function calculateCharge(date: Date, quantity: number, baseRate: number): number {
  if (date.getMonth() >= 5 && date.getMonth() <= 9
      && date.getDay() !== 0 && date.getDay() !== 6
      && quantity > 100) {
    return quantity * baseRate * 0.8;
  } else if (date.getMonth() >= 11 || date.getMonth() <= 1) {
    return quantity * baseRate * 1.2;
  } else {
    return quantity * baseRate;
  }
}

After

function calculateCharge(date: Date, quantity: number, baseRate: number): number {
  if (isSummerWeekdayBulk(date, quantity)) {
    return applySummerDiscount(quantity, baseRate);
  }
  if (isWinterPeriod(date)) {
    return applyWinterSurcharge(quantity, baseRate);
  }
  return quantity * baseRate;
}

function isSummerWeekdayBulk(date: Date, quantity: number): boolean {
  const month = date.getMonth();
  const day = date.getDay();
  return month >= 5 && month <= 9 && day !== 0 && day !== 6 && quantity > 100;
}

function isWinterPeriod(date: Date): boolean {
  const month = date.getMonth();
  return month >= 11 || month <= 1;
}

function applySummerDiscount(quantity: number, baseRate: number): number {
  return quantity * baseRate * 0.8;
}

function applyWinterSurcharge(quantity: number, baseRate: number): number {
  return quantity * baseRate * 1.2;
}

Replace Conditional with Polymorphism(条件分岐をポリモーフィズムに置換)

型による分岐を、ポリモーフィズム(多態性)に置き換えます。最も強力なリファクタリング技法の1つです。

Before

class Bird {
  type: 'european' | 'african' | 'norwegian_blue';
  numberOfCoconuts: number;
  voltage: number;
  isNailed: boolean;

  getSpeed(): number {
    switch (this.type) {
      case 'european':
        return 35;
      case 'african':
        return 40 - 2 * this.numberOfCoconuts;
      case 'norwegian_blue':
        return this.isNailed ? 0 : 10 + this.voltage / 10;
      default:
        throw new Error(`Unknown bird type: ${this.type}`);
    }
  }

  getPlumage(): string {
    switch (this.type) {
      case 'european':
        return 'average';
      case 'african':
        return this.numberOfCoconuts > 2 ? 'tired' : 'average';
      case 'norwegian_blue':
        return this.voltage > 100 ? 'scorched' : 'beautiful';
      default:
        return 'unknown';
    }
  }
}

同じ switch が複数メソッドに散在しています。新しい鳥の種類を追加するたびに、すべての switch を修正する必要があります。

After

// 基底クラス
abstract class Bird {
  abstract getSpeed(): number;
  abstract getPlumage(): string;
}

class EuropeanBird extends Bird {
  getSpeed(): number {
    return 35;
  }

  getPlumage(): string {
    return 'average';
  }
}

class AfricanBird extends Bird {
  constructor(private numberOfCoconuts: number) {
    super();
  }

  getSpeed(): number {
    return 40 - 2 * this.numberOfCoconuts;
  }

  getPlumage(): string {
    return this.numberOfCoconuts > 2 ? 'tired' : 'average';
  }
}

class NorwegianBlueBird extends Bird {
  constructor(
    private voltage: number,
    private isNailed: boolean
  ) {
    super();
  }

  getSpeed(): number {
    return this.isNailed ? 0 : 10 + this.voltage / 10;
  }

  getPlumage(): string {
    return this.voltage > 100 ? 'scorched' : 'beautiful';
  }
}

// Factory で生成
function createBird(data: BirdData): Bird {
  switch (data.type) {
    case 'european': return new EuropeanBird();
    case 'african': return new AfricanBird(data.numberOfCoconuts);
    case 'norwegian_blue': return new NorwegianBlueBird(data.voltage, data.isNailed);
    default: throw new Error(`Unknown bird type: ${data.type}`);
  }
}

// 使う側は型を気にしない
function printBirdInfo(bird: Bird): void {
  console.log(`Speed: ${bird.getSpeed()}, Plumage: ${bird.getPlumage()}`);
}

switch は Factory に1箇所だけ残り、各メソッド内の条件分岐は完全に消えました。


Consolidate Conditional Expression(条件式の統合)

同じ結果を返す複数の条件をまとめます。

Before

function disabilityAmount(employee: Employee): number {
  if (employee.seniority < 2) return 0;
  if (employee.monthsDisabled > 12) return 0;
  if (employee.isPartTime) return 0;
  // 実際の計算
  return employee.salary * 0.6;
}

After

function disabilityAmount(employee: Employee): number {
  if (isNotEligibleForDisability(employee)) return 0;
  return employee.salary * 0.6;
}

function isNotEligibleForDisability(employee: Employee): boolean {
  return employee.seniority < 2
    || employee.monthsDisabled > 12
    || employee.isPartTime;
}

Replace Nested Conditional with Lookup(ネスト条件をルックアップに置換)

// Before
function getShippingCost(region: string, weight: number): number {
  if (region === 'domestic') {
    if (weight <= 1) return 500;
    if (weight <= 5) return 800;
    return 1200;
  } else if (region === 'asia') {
    if (weight <= 1) return 1500;
    if (weight <= 5) return 2500;
    return 4000;
  } else {
    if (weight <= 1) return 3000;
    if (weight <= 5) return 5000;
    return 8000;
  }
}

// After
const SHIPPING_RATES: Record<string, { light: number; medium: number; heavy: number }> = {
  domestic: { light: 500, medium: 800, heavy: 1200 },
  asia:     { light: 1500, medium: 2500, heavy: 4000 },
  other:    { light: 3000, medium: 5000, heavy: 8000 },
};

function getShippingCost(region: string, weight: number): number {
  const rates = SHIPPING_RATES[region] ?? SHIPPING_RATES['other'];
  if (weight <= 1) return rates.light;
  if (weight <= 5) return rates.medium;
  return rates.heavy;
}

高橋アーキテクトのアドバイス:

「条件分岐の整理は、リファクタリングで最も効果が大きい作業の1つだ。特に Replace Conditional with Polymorphism は、Step 2 で学んだ OCP を実現する最も実践的な方法だ」


まとめ

ポイント内容
Guard Clause早期リターンでネストを解消
Decompose Conditional条件式に名前をつけて分解
Replace with Polymorphism型による分岐をクラス階層に置換
Lookup Tableネスト条件をデータ構造に置換

チェックリスト

  • Guard Clause でネストを減らせる
  • 複雑な条件式を分解できる
  • switch文をポリモーフィズムに置き換えられる

次のステップへ

次は「データの再編成」です。クラスの抽出や値オブジェクトの導入によって、データ構造を改善する方法を学びます。


推定読了時間: 30分