この記事は、保守性の高いコード設計シリーズの一部です。これまでの記事で、
if
のネストを解消する「ガード節」や、switch
の重複をなくす「ストラテジーパターン」を学びました。今回は、さらに複雑な条件分岐をエレガントに解決するための応用的なデザインパターンを紹介します。
こんにちは。今回は、条件分岐のリファクタリングの中でも、特に応用的な3つのトピックを扱います。
- ポリシーパターン: 複雑に絡み合ったビジネスルールを整理する。
instanceof
の回避: 型チェックによる分岐を、ポリモーフィズムで置き換える。- フラグ引数のリファクタリング: 1つのメソッドが2つ以上の責務を持つのを防ぐ。
これらのテクニックは、いずれも**「条件分岐のロジックを、責任あるオブジェクトに封じ込める」**という共通の目的を持っています。
1. ポリシーパターンで複雑なルールを整理する
ECサイトの「優良顧客」を判定するロジックのように、複数の条件が複雑に組み合わさっているケースを考えてみましょう。
【Before】条件が重複し、再利用性の低い判定メソッド
// ゴールド顧客の判定ロジック
boolean isGoldCustomer(PurchaseHistory history) {
if (history.totalAmount >= 100000) {
if (history.purchaseFrequencyPerMonth >= 10) {
if (history.returnRate <= 0.001) {
return true;
}
}
}
return false;
}
// シルバー顧客の判定ロジック(購入頻度と返品率の条件が重複している)
boolean isSilverCustomer(PurchaseHistory history) {
if (history.purchaseFrequencyPerMonth >= 10) {
if (history.returnRate <= 0.001) {
return true;
}
}
return false;
}
このコードは、判定条件がハードコーディングされており、ルールの組み合わせが重複しています。「プラチナ顧客」のような新しいランクを追加するのも一苦労です。
ポリシーパターンは、このような**「ビジネスルール」そのものをオブジェクトとして表現**するデザインパターンです。
【After】ポリシーパターンで、ルールを組み合わせ可能な部品にする
ステップ1: ルールのインターフェースを定義
interface CustomerRule {
boolean ok(PurchaseHistory history);
}
ステップ2: 各ルールを具体的なクラスとして実装
Java
class TotalAmountRule implements CustomerRule {
public boolean ok(PurchaseHistory history) { return history.totalAmount >= 100000; }
}
class PurchaseFrequencyRule implements CustomerRule {
public boolean ok(PurchaseHistory history) { return history.purchaseFrequencyPerMonth >= 10; }
}
class ReturnRateRule implements CustomerRule {
public boolean ok(PurchaseHistory history) { return history.returnRate <= 0.001; }
}
ステップ3: ポリシー(方針)クラスで、ルールを組み合わせる
class CustomerPolicy {
private final Set<CustomerRule> rules = new HashSet<>();
void add(CustomerRule rule) { rules.add(rule); }
boolean complyWithAll(PurchaseHistory history) {
for (CustomerRule rule : rules) {
if (!rule.ok(history)) return false;
}
return true;
}
}
ステップ4: ポリシーを使って顧客ランクを定義
// ゴールド顧客の方針を定義
CustomerPolicy goldCustomerPolicy = new CustomerPolicy();
goldCustomerPolicy.add(new TotalAmountRule());
goldCustomerPolicy.add(new PurchaseFrequencyRule());
goldCustomerPolicy.add(new ReturnRateRule());
// シルバー顧客の方針を定義
CustomerPolicy silverCustomerPolicy = new CustomerPolicy();
silverCustomerPolicy.add(new PurchaseFrequencyRule());
silverCustomerPolicy.add(new ReturnRateRule());
// 判定の実行
boolean isGold = goldCustomerPolicy.complyWithAll(customer.history);
boolean isSilver = silverCustomerPolicy.complyWithAll(customer.history);
ルールが再利用可能なオブジェクトになったことで、ネストしたif
文は消え、各顧客ランクの条件が明確になりました。新しいランクの追加も、ルールの組み合わせを変えるだけで簡単に行えます。
2. instanceof
による型チェックを回避する
ポリモーフィズムの利点を損なうのが、instanceof
を使った型チェックです。
【Before】型を判定して、処理を分岐させている
Money calculateBusySeasonFee(SubscriptionPlan plan) {
if (plan instanceof RegularPlan) {
return plan.fee().add(new Money(3000));
} else if (plan instanceof PremiumPlan) {
return plan.fee().add(new Money(5000));
}
return plan.fee();
}
このコードは、新しいプラン(例: FamilyPlan
)が追加されるたびに、このif
文を修正しなければなりません。
これは、「繁忙期の料金を計算する」という責務が、プラン自身ではなく、外部のメソッドにあることが原因です。この責務を、本来あるべきSubscriptionPlan
インターフェースとその実装クラスに移しましょう。
【After】各クラスに自分の責務を果たさせる
// インターフェースに、責務を定義
interface SubscriptionPlan {
Money fee();
Money busySeasonFee(); // 繁忙期料金を計算する責務を追加
}
// 各クラスが、自分自身の方法で責務を実装
class RegularPlan implements SubscriptionPlan {
public Money fee() { return new Money(7000); }
public Money busySeasonFee() { return fee().add(new Money(3000)); }
}
class PremiumPlan implements SubscriptionPlan {
public Money fee() { return new Money(12000); }
public Money busySeasonFee() { return fee().add(new Money(5000)); }
}
// 利用側は、型を気にせずメソッドを呼び出すだけ
Money busyFee = plan.busySeasonFee();
if
文は完全に消え、各プランが自身の繁忙期料金を計算する方法を知っている、疎結合で拡張性の高い設計になりました。
3. 「フラグ引数」をリファクタリングする
boolean
型の引数(フラグ引数)を使って、メソッドの挙動を切り替えるのは悪い設計です。それはメソッドが2つ以上の責任を持っているサインです。
【Before】isUrgent
フラグで、処理が全く変わるメソッド
void sendNotification(String message, boolean isUrgent) {
if (isUrgent) {
// 緊急:SMSで送信するロジック
smsSender.send(message);
} else {
// 通常:メールで送信するロジック
emailSender.send(message);
}
}
// 呼び出し側
sendNotification("サーバがダウンしました", true);
メソッド名sendNotification
は、通常と緊急の2つの異なる処理を隠蔽してしまっています。
解決策1: メソッドを分離する 最もシンプルな解決策は、責務ごとにメソッドを分けることです。
void sendUrgentNotification(String message) { smsSender.send(message); }
void sendStandardNotification(String message) { emailSender.send(message); }
// 呼び出し側も意図が明確になる
sendUrgentNotification("サーバがダウンしました");
解決策2: ストラテジーパターンを適用する もし処理の切り替えがより複雑なら、ストラテジーパターンが有効です。
interface NotificationChannel { void send(String message); }
class SmsChannel implements NotificationChannel { /* ... */ }
class EmailChannel implements NotificationChannel { /* ... */ }
// チャンネル(戦略)を渡すことで、振る舞いを決定する
void sendNotification(NotificationChannel channel, String message) {
channel.send(message);
}
まとめ
複雑な条件分岐は、プログラマを悩ませる大きな要因です。しかし、その多くはオブジェクト指向の原則に従うことで、よりシンプルで堅牢な形にリファクタリングできます。
- ルールや方針は、それ自身をオブジェクト(ポリシー)として表現する。
- 型での分岐はせず、振る舞いをインターフェースに追加し、各クラスに実装させる(ポリモーフィズム)。
- 一つのメソッドには、一つの責任だけを持たせる。
if
やswitch
を書きそうになったら、一度立ち止まって「この判断ロジックをカプセル化できるクラスはないか?」と考えてみることが、中級者への扉を開く鍵となるでしょう。