ストーリー
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分