入社2年目の夏、私は絶望的なコードと向き合っていました。
ECサイトの税金計算機能。最初は消費税10%だけのシンプルな仕様だったのに、軽減税率、法人税率、免税事業者対応…と要求が次々に追加されて。
気づいたときには、if文が7層も重なった魔界のようなコードが出来上がっていました。
if (customerType == "一般") {
if (productType == "食品") {
if (region == "国内") {
if (isDiscount) {
if (discountType == "学生") {
// もうここで心が折れる...
}
}
}
}
}
// この地獄があと100行続く新しい条件を1つ追加するだけで半日かかる。テストは書けたものじゃない。先輩に「なんだこのコード」と言われる始末。
そんな時、救世主のように現れたのがStrategyパターンでした。
私を救ったシンプルな考え方
Strategyパターンって、要するに「やりたいことは同じ。でもやり方がいくつかある」時に使う設計パターンです。
税金を計算したい ← やりたいこと 一般客・法人・免税事業者で計算方法が違う ← やり方がいくつか
これを別々のクラスに分けて、状況に応じて使い分ける。ただそれだけ。
最初に聞いた時は「は?それだけ?」って思いました。でも実際にやってみたら…
劇的ビフォーアフター
改善前の地獄コード(120行)
- 条件分岐が7層の入れ子
- 新機能追加に半日かかる
- テストが書けない
- 誰も触りたがらない
改善後の天国コード(35行)
- 各顧客タイプが独立したクラス
- 新機能追加が15分で完了
- 各クラスを個別にテスト可能
- 後輩も「分かりやすいですね!」
実際のコードはこんな感じになりました。
Step1: 共通インターフェース
// 全ての税率計算戦略が実装するインターフェース
public interface ITaxStrategy
{
decimal CalculateTaxRate(string productType, string region);
}「なんだ、メソッド1つだけか」と拍子抜けしました。でもこれが肝なんです。
Step2: 各Strategyクラス
// 一般顧客用
public class StandardCustomerTaxStrategy : ITaxStrategy
{
public decimal CalculateTaxRate(string productType, string region)
{
if (region == "海外") return 0.0m;
return productType switch
{
"食品" or "書籍" => 0.08m,
_ => 0.10m
};
}
}
// 法人顧客用(シンプル!)
public class CorporateCustomerTaxStrategy : ITaxStrategy
{
public decimal CalculateTaxRate(string productType, string region)
{
return region == "海外" ? 0.0m : 0.10m;
}
}
各クラスが独立してるから、一般顧客の計算ロジックをいじっても法人側には影響なし。当たり前のことなのに、これまでのコードではできてなかった。
Step3: Strategyクラスを使う側
public class TaxCalculator
{
private readonly Dictionary<string, ITaxStrategy> strategies;
public TaxCalculator()
{
strategies = new Dictionary<string, ITaxStrategy>
{
["一般"] = new StandardCustomerTaxStrategy(),
["法人"] = new CorporateCustomerTaxStrategy(),
["免税事業者"] = new TaxExemptCustomerTaxStrategy()
};
}
public decimal CalculateTax(decimal price, string customerType, string productType, string region)
{
var strategy = strategies[customerType];
return price * strategy.CalculateTaxRate(productType, region);
}
}
メインの計算クラスがこんなにスッキリ。もう条件分岐の海で溺れる心配はありません。
新機能追加が楽しくなった話
学生割引機能の追加依頼が来たときのことです。
以前なら「うわぁ、またif文追加か…」と憂鬱になっていたところ、今回は15分で完了。
// 新しいStrategyクラスを追加するだけ
public class StudentCustomerTaxStrategy : ITaxStrategy
{
public decimal CalculateTaxRate(string productType, string region)
{
var baseRate = productType switch
{
"食品" or "書籍" => 0.08m,
_ => 0.10m
};
return baseRate * 0.8m; // 20%割引
}
}
// 戦略マップに1行追加
strategies["学生"] = new StudentCustomerTaxStrategy();
既存のコードに一切手を加えることなく新機能完了。
PM「え、もう終わったの?」 私「はい、テストも通ってます」 PM「…天才か?」
いえいえ、Strategyパターンが天才なんです。
実際に使ってみて感じたこと
良かった点
開発効率が10倍になった 新機能追加が半日→15分になったのは大きい。
テストが書きやすい 各戦略クラスを個別にテストできるから、バグの特定が楽。
[Test]
public void 学生_一般商品_20パーセント割引()
{
var strategy = new StudentCustomerTaxStrategy();
var rate = strategy.CalculateTaxRate("一般商品", "国内");
// 0.10 * 0.8 = 0.08
Assert.AreEqual(0.08m, rate);
}
後輩に説明しやすい 「この顧客タイプはこのクラス見て」で済む。
注意点もある
クラスファイルが増える Strategyの数だけファイルが増えるので、小規模なら逆に複雑になるかも。
設計パターンの学習コスト チーム全員が理解していないと、「なんでこんな面倒なことしてるの?」となりがち。
やりすぎ注意 2択の簡単な分岐までStrategyパターンにする必要はない。if文で十分。
いつ使うか、いつ使わないか
私の経験則ですが…
使うべき時
- 3つ以上の似たような処理パターンがある
- 将来的に処理パターンが増えそう
- 各処理が複雑で、個別にテストしたい
使わない方がいい時
- 処理パターンが2つだけ
- 各処理が1〜2行で済む
- チームが設計パターンに慣れていない
私も最初は何でもかんでもパターン適用しようとして、2択の判定までStrategyパターンにして先輩に怒られました。「素直にif文使え」って。
これまでの学習との繋がり
振り返ってみると、これまでのリファクタリング学習が全部繋がってたんです。
- 変数命名 → 戦略クラス名も分かりやすく
- 定数化 → 戦略名を定数で管理
- 処理の共通化 → インターフェースで共通化
- ファイル分割 → 各戦略を別ファイルに
- DRY原則 → 重複ロジックを戦略に集約
基礎の積み重ねがあったからこそ、設計パターンも自然に理解できました。
次に何を学ぶか
Strategyパターンをマスターしたら、次はFactoryパターンがおすすめ。
戦略クラスの生成処理をもっとスマートに管理できるようになります。今回作ったTaxCalculatorのコンストラクタ部分がさらに綺麗になりそう。
設定ファイルから動的に戦略を切り替える、なんてこともできるようになります。
最後に
正直、最初は「設計パターンなんて難しそう」と敬遠してました。でも実際に使ってみると、こんなにコードが書きやすくなるとは。
特に新米プログラマーの私にとって、「拡張に強いコード」を書けるようになったのは大きな自信になりました。
もし今、条件分岐の海で溺れている人がいたら、ぜひStrategyパターンを試してみてください。きっと救われます。
私みたいに「天才か?」と言われるかもしれませんよ(実際は天才じゃないですが)。
次回予告 Factoryパターンで、オブジェクト生成をもっとスマートに管理する方法を学びます。Strategyパターンとの組み合わせで、さらに強力な設計が可能に!
参考になったら嬉しいです。一緒に設計パターンマスターを目指しましょう!
