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地獄を脱却する方法は、早期リターンやガード節などもありますが、
大規模なプロジェクトや複雑な処理が絡む場合は、
今回ご紹介したデザインパターンの組み合わせが特に有効です。
ぜひ、明日からのコーディングに活かしていただければ幸いです!
最後まで読んでいただき、ありがとうございました。








