Loading
BLOG 開発者ブログ

2026年1月5日

if-else地獄からの脱却:StrategyとFactoryパターンの合わせ技

「ちょっとした機能追加のはずが、気づけば if 文が10個以上のネスト地獄に…」
「このコード、3ヶ月前の自分が書いたはずなのに、もう理解できない…」

こんな経験、ありませんか?

最初は綺麗に書けていたコードも、仕様変更や機能追加を重ねるうちに、条件分岐だらけの読めないコードになってしまうことがあります。

本記事では、StrategyパターンFactoryパターンを組み合わせて、条件分岐地獄から脱却する方法を段階的に解説します。

目次

はじめに

こんにちは。クラウドソリューション第1グループのkido.m です。この記事は アイソルート Advent Calendar 2025 25日目の記事です。
まずは、よくありがちなコードを見てみましょう。
こちらはユーザーへの通知を送るシステムで、メール・SMS・Slackなど、異なる方法で通知を送る処理を行っています。

class NotificationSender
{
    public function send(string $channel, string $message, User $user): void
    {
        if ($channel === 'email') {
            // メールアドレスの形式チェック
            $this->validateEmail($user->getEmail());
            // SMTPサーバーに接続
            $this->connectToSmtpServer();
            // メール送信
            $this->sendEmail($user->getEmail(), $message);
            // 送信履歴を記録
            $this->logEmailSent($user->getId());
            
        } elseif ($channel === 'sms') {
            // 電話番号の形式チェック
            $this->validatePhoneNumber($user->getPhone());
            // SMS送信サービスに接続
            $this->connectToSmsGateway();
            // SMS送信
            $this->sendSms($user->getPhone(), $message);
            // 送信履歴を記録
            $this->logSmsSent($user->getId());
        } elseif ($channel === 'slack') {
            // Slackトークンの確認
            $this->validateSlackToken($user->getSlackId());
            // Slack APIに接続
            $this->connectToSlackApi();
            // Slackに投稿
            $this->postToSlack($user->getSlackId(), $message);
            // 送信履歴を記録
            $this->logSlackSent($user->getId());
        } else {
            throw new InvalidArgumentException("未対応の通知チャネル: {$channel}");
        }
    }
}
このコードは何をしている?
このクラスは、通知を送る「チャネル(方法)」に応じて、異なる処理を実行しています。
例えば:

  • email の場合 → メールサーバーに接続してメール送信
  • sms の場合 → SMS送信サービスに接続してSMS送信
  • slack の場合 → Slack APIに接続してメッセージ投稿

各チャネルで「バリデーション → 接続 → 送信 → ログ記録」という流れは同じですが、使うサービスや処理の詳細が異なります。

「このようなコード、どこかで見たことある…」

そう思った方も多いのではないでしょうか?

このコードは動作に問題ありませんが、新しい通知方法(例:Discord、LINE、Teamsなど)を追加するたびに、elseif が増え続け、メソッドがどんどん長くなっていきます。

では、なぜこのような if-else 地獄が問題なのか、もう少し詳しく見ていきましょう。

if-else地獄の何が問題なのか

先ほどのコードは一見動作に問題なさそうですが、実は様々な問題を抱えています。
ここでは、保守性・可読性・テスタビリティの3つの視点から見ていきましょう。

1. 保守性:どこに何を書けば良いのか分からない

追加修正依頼があり、新しい通知チャネル「discord(Discord通知)」を追加することになったとします。
実際に既存コードに追加してみましょう。

public function send(string $channel, string $message, User $user): void
{
    if ($channel === 'email') {
        // メールアドレスの形式チェック
        $this->validateEmail($user->getEmail());
        // SMTPサーバーに接続
        $this->connectToSmtpServer();
        // メール送信
        $this->sendEmail($user->getEmail(), $message);
        // 送信履歴を記録
        $this->logEmailSent($user->getId());
    } elseif ($channel === 'sms') {
        // 電話番号の形式チェック
        $this->validatePhoneNumber($user->getPhone());
        // SMS送信サービスに接続
        $this->connectToSmsGateway();
        // SMS送信
        $this->sendSms($user->getPhone(), $message);
        // 送信履歴を記録
        $this->logSmsSent($user->getId());       
    } elseif ($channel === 'slack') {
        // Slackトークンの確認
        $this->validateSlackToken($user->getSlackId());
        // Slack APIに接続
        $this->connectToSlackApi();
        // Slackに投稿
        $this->postToSlack($user->getSlackId(), $message);
        // 送信履歴を記録
        $this->logSlackSent($user->getId());        
    } elseif ($channel === 'discord') { // ← ここに追加?
        // Discordトークンの確認
        $this->validateDiscordToken($user->getDiscordId());
        // Discord APIに接続
        $this->connectToDiscordApi();
        // Discordに投稿
        $this->postToDiscord($user->getDiscordId(), $message);
        // 送信履歴を記録
        $this->logDiscordSent($user->getId());       
    } else {
        throw new InvalidArgumentException("未対応の通知チャネル: {$channel}");
    }
}

このコードには以下のような問題があります:

  • メソッドが肥大化:通知チャネルが増えるたびに、1つのメソッドがどんどん長くなる
  • 既存コードの修正が必要:新機能追加のたびに既存のメソッドに手を入れなければならない
  • どこに追加すべきか迷う:「slack の後?それとも最後?」と毎回考える必要がある

これはOpen/Closed原則(拡張に対して開いており、修正に対して閉じている)に違反しています。
理想的には、既存コードを変更せずに新しい機能を追加できるべきです。

2. 可読性:コードの全体像が把握しづらい

通知チャネルが10個、20個と増えていくとどうなるでしょうか?

public function send(string $channel, string $message, User $user): void
{
    if ($channel === 'email') {
        // メール送信処理(4〜5行)
    } elseif ($channel === 'sms') {
        // SMS送信処理(4〜5行)
    } elseif ($channel === 'slack') {
        // Slack送信処理(4〜5行)
    } elseif ($channel === 'discord') {
        // Discord送信処理(4〜5行)
    } elseif ($channel === 'line') {
        // LINE送信処理(4〜5行)
    } elseif ($channel === 'teams') {
        // Teams送信処理(4〜5行)
    } elseif ($channel === 'telegram') {
        // Telegram送信処理(4〜5行)
    } elseif ($channel === 'push') {
        // プッシュ通知処理(4〜5行)
    } // ... さらに続く
    else {
        throw new InvalidArgumentException("未対応の通知チャネル");
    }
}

このコードには以下のような問題があります:

  • どんな通知チャネルがあるか分かりにくい:メソッド内を全部読まないと把握できない
  • 関連する処理が分散:1つの通知チャネルに関する処理が条件分岐の中に埋もれている

3. テスタビリティ:特定の処理だけをテストしにくい

「Slack通知の処理だけをテストしたい」と思ったとき、どうすればいいでしょうか?

public function testSlackNotification()
{
    $sender = new NotificationSender();
    $user = new User(['slack_id' => 'U12345']);
    
    $sender->send('slack', 'テストメッセージ', $user);
    
    // Slackに正しく送信されたか確認
    $this->assertTrue($this->slackWasCalled());
}

一見問題なさそうですが、実際には:

  • すべての分岐を通る必要がある:内部的には他の条件分岐も評価される
  • 独立したテストができない:特定の通知チャネルのロジックだけを切り出してテストできない
  • モックが作りにくい:各通知チャネルの処理を個別にモック化できない

理想的には、各通知チャネルの処理を独立してテストできるべきです。

段階的リファクタリング

ここまで、if-else地獄のコードがなぜ問題なのかを、保守性・可読性・テスタビリティの3つの観点から見てきました。
では、いよいよ本題です。このコードを段階的にリファクタリングして、綺麗なコードに変えていきましょう!

1. Strategyパターンで処理を分離

STEP1. インターフェース定義

interface NotificationStrategy
{
    public function send(string $message, User $user): void;
}

STEP2. 各通知チャネルの具象クラス作成

// メール通知
class EmailNotification implements NotificationStrategy
{
    public function send(string $message, User $user): void
    {
        // メールアドレスの形式チェック
        $this->validateEmail($user->getEmail());
        // SMTPサーバーに接続
        $this->connectToSmtpServer();
        // メール送信
        $this->sendEmail($user->getEmail(), $message);
        // 送信履歴を記録
        $this->logEmailSent($user->getId());
    }
    
    private function validateEmail(string $email): void { /* ... */ }
    private function connectToSmtpServer(): void { /* ... */ }
    private function sendEmail(string $email, string $message): void { /* ... */ }
    private function logEmailSent(int $userId): void { /* ... */ }
}
// SMS通知
class SmsNotification implements NotificationStrategy
{
    public function send(string $message, User $user): void
    {
        // 電話番号の形式チェック
        $this->validatePhoneNumber($user->getPhone());
        // SMS送信サービスに接続
        $this->connectToSmsGateway();
        // SMS送信
        $this->sendSms($user->getPhone(), $message);
        // 送信履歴を記録
        $this->logSmsSent($user->getId());
    }
    
    private function validatePhoneNumber(string $phone): void { /* ... */ }
    private function connectToSmsGateway(): void { /* ... */ }
    private function sendSms(string $phone, string $message): void { /* ... */ }
    private function logSmsSent(int $userId): void { /* ... */ }
}
// Slack通知
class SlackNotification implements NotificationStrategy
{
    public function send(string $message, User $user): void
    {
        // Slackトークンの確認
        $this->validateSlackToken($user->getSlackId());
        // Slack APIに接続
        $this->connectToSlackApi();
        // Slackに投稿
        $this->postToSlack($user->getSlackId(), $message);
        // 送信履歴を記録
        $this->logSlackSent($user->getId());
    }
    
    private function validateSlackToken(string $slackId): void { /* ... */ }
    private function connectToSlackApi(): void { /* ... */ }
    private function postToSlack(string $slackId, string $message): void { /* ... */ }
    private function logSlackSent(int $userId): void { /* ... */ }
}

STEP3. コンテキストクラス(NotificationSender)作成

class NotificationSender
{
    private NotificationStrategy $strategy;
    
    public function __construct(NotificationStrategy $strategy)
    {
        $this->strategy = $strategy;
    }
    
    public function send(string $message, User $user): void
    {
        $this->strategy->send($message, $user);
    }
    
}

STEP4. 使用方法

// ユーザーの設定から通知チャネルを取得

$channel = $user->getPreferredChannel(); // 'email', 'sms', 'slack' など


if ($channel === 'email') {
    $sender = new NotificationSender(new EmailNotification());
} elseif ($channel === 'sms') {
    $sender = new NotificationSender(new SmsNotification());
} elseif ($channel === 'slack') {
    $sender = new NotificationSender(new SlackNotification());
} else {
    throw new InvalidArgumentException("未対応の通知チャネル: {$channel}");
}

$sender->send('重要なお知らせ', $user);

Strategyパターンを使用してリファクタリングを行った結果、各通知方法が独立したクラスになり、責任が明確に分離されました。
また、もし追加修正で新たな通知方法を追加しなくてはいけない場合も、既存のコードに影響せずに修正ができます。

しかし!!!!

if ($channel === 'email') {
    $sender = new NotificationSender(new EmailNotification());
} elseif ($channel === 'sms') {
    $sender = new NotificationSender(new SmsNotification());
} elseif ($channel === 'slack') {
    $sender = new NotificationSender(new SlackNotification());
} else {
    throw new InvalidArgumentException("未対応の通知チャネル: {$channel}");
}

まだif-elseが残っている!
ここでFactoryパターンの出番です。次のステップでこれを解決しましょう。

2. Factoryパターンで生成を統一

STEP1. Factoryクラスの作成

class NotificationFactory
{
    public static function create(string $channel): NotificationStrategy
    {
        return match ($channel) {
            'email' => new EmailNotification(),
            'sms' => new SmsNotification(),
            'slack' => new SlackNotification(),
            default => throw new InvalidArgumentException("未対応の通知チャネル: {$channel}")
        };
    }
}

STEP2. 使用方法

// ユーザーの設定から通知チャネルを取得
$channel = $user->getPreferredChannel();


// Factoryで適切なStrategyを生成
$strategy = NotificationFactory::create($channel);
$sender = new NotificationSender($strategy);

$sender->send('重要なお知らせ', $user);

Factoryパターンを使用することで、条件分岐がクライアントコードから完全に消え、とても見やすいコードになりました。
さらに、新しい通知方法を追加する場合も、Factoryクラスに1行追加するだけで完了します。

class NotificationFactory
{
    public static function create(string $channel): NotificationStrategy
    {
        return match ($channel) {
            'email' => new EmailNotification(),
            'sms' => new SmsNotification(),
            'slack' => new SlackNotification(),
            'discord' => new DiscordNotification(), // ← ここだけ追加
            default => throw new InvalidArgumentException("未対応: {$channel}")
        };
    }
}

注意点とアンチパターン

1. やりすぎ注意:過度な抽象化

選択肢が2〜3個しかないのにパターン適用は避けたほうがいいです。

// たった2つの選択肢のためにパターンを使う
class TaxCalculator
{
    public function calculate(string $type, float $amount): float
    {
        if ($type === 'standard') {
            return $amount * 0.10;
        } else {
            return $amount * 0.08; 
        }
    }
}

この程度なら、シンプルなif-elseで十分です。パターンを適用すると逆に複雑になります。
ファイルが4つも増えて、かえって分かりにくくなってしまいます。

// 過度な抽象化の例
interface TaxStrategy { /* ... */ }
class StandardTaxStrategy implements TaxStrategy { /* ... */ }
class ReducedTaxStrategy implements TaxStrategy { /* ... */ }
class TaxStrategyFactory { /* ... */ }

2. 処理が単純すぎる場合は使わない

// 単純な値の返却だけ
class ShippingCalculator
{
    public function calculate(string $method): int
    {
        return match ($method) {
            'standard' => 500,
            'express' => 1000,
            'overnight' => 2000,
            default => 500
        };
    }
}

上記のように単純に値を返すだけの処理である場合は、配列や定数で十分です。

// シンプルな方法
class ShippingFee
{
    private const FEES = [
        'standard' => 500,
        'express' => 1000,
        'overnight' => 2000,
    ];
    
    public static function get(string $method): int
    {
        return self::FEES[$method] ?? 500;
    }
}

3. パターンを使うべき場合

条件 具体例
選択肢が4つ以上 通知チャネルが email, sms, slack, line, discord…
各処理が複雑(5行以上) バリデーション → 接続 → 送信 → ログ記録
今後も増える可能性が高い 決済方法、認証方式、エクスポート形式など
各処理を独立してテストしたい 特定の通知方法だけをモックしてテスト
各処理で依存関係が異なる メールはSMTPサーバー、SMSはゲートウェイ…

4. パターンを使わない方がいい場合

条件 代替案
選択肢が2〜3個だけ シンプルな if-else や match 式
処理が単純(1〜2行) 配列、定数、match式
今後増える予定がない 必要になってから実装
一度しか使わない処理 インライン実装で十分

まとめ

今回は、StrategyパターンFactoryパターンを組み合わせて、条件分岐地獄から脱却する方法をご紹介しました。

本記事のポイント

  • Strategyパターン:各処理を独立したクラスに分離
  • Factoryパターン:オブジェクト生成を一元管理
  • 2つの組み合わせ:保守性・拡張性・テスタビリティが劇的に向上

いつ使うべき?

  • 選択肢が4つ以上ある
  • 各処理が5行以上で複雑
  • 今後も増える可能性が高い

迷ったら、まずシンプルに実装して、必要になったらリファクタリングしましょう。

if-else地獄を脱却する方法は、早期リターンやガード節などもありますが、
大規模なプロジェクトや複雑な処理が絡む場合は、
今回ご紹介したデザインパターンの組み合わせが特に有効です。

ぜひ、明日からのコーディングに活かしていただければ幸いです!
最後まで読んでいただき、ありがとうございました。

kidomのブログ