本記事では「値引き処理」の実装を例に、
昔ながらの条件分岐・ポリモーフィズム・デリゲート注入の違いと使いどころを整理してみました。
0.実装したい内容
- 定価がある
- 顧客によってランクがある
- 顧客ランクによって値引きされる
1.昔ながらの書き方(条件分岐による値引き)
decimal 値引き後価格(decimal 定価, string ランク)
{
switch (ランク)
{
case "A":
return 定価 * 0.60m;
case "B":
return 定価 * 0.70m;
default:
return 定価;
}
}
✅ 特徴
- シンプルで一目で処理が分かる
- 条件が複雑になると、保守性が下がる
(実務だとここで IF文 が複雑にネストしてたりしますね)
2.ポリモーフィズム(クラス継承による実装)
abstract class 価格計算
{
abstract decimal 値引き後価格(decimal 定価);
}
class ランクA購入者 : 価格計算
{
override decimal 値引き後価格(decimal 定価) => 定価 * 0.60m;
}
class 一般購入者 : 価格計算
{
override decimal 値引き後価格(decimal 定価) => 定価;
}
✅ 使用例
var ランクA = new ランクA購入者();
decimal 金額 = ランクA.値引き後価格(定価);
✅ 特徴
- 値引きルールが複雑でもクラス単位で明確に分けて実装可能
- 呼び出し側は「値引きという共通の型」だけを意識すればOK
- 新ルール追加も既存コード変更なし(OCPの実現)
処理の責任が各クラスに分かれていて拡張しやすい
3.デリゲート注入
class 値引き処理
{
Func<decimal, decimal> 計算ロジック;
値引き処理(Func<decimal, decimal> 計算ロジック)
{
this.計算ロジック = 計算ロジック;
}
decimal 値引き後価格(decimal 定価)
{
return 計算ロジック(定価);
}
}
✅ 使用例
var ランクA = new 値引き処理(x => x * 0.60m);
var ランクB = new 値引き処理(x => x * 0.70m);
var 一般 = new 値引き処理(x => x);
decimal 金額 = ランクA.値引き後価格(定価);
✅ 特徴
- 実行時にロジックを差し替え可能(柔軟)
- 匿名関数やラムダで記述できる
金額を求める直前で、値引き処理の中身を差し替えられる柔軟性
🧱 主なリスク
- どの処理が実行されたかが、コード上で一見して分かりづらい
- 注入される関数がどこでどう決まるのか把握しづらい
- ラムダや匿名関数は名前がないため、ログに処理名を残せない
柔軟な反面、追跡やデバッグが難しい
4.比較:それぞれのメリット・デメリット
実装方法 | メリット | デメリット |
---|---|---|
条件分岐(if) | シンプル、動作が見える | 変更や拡張に弱い、条件が増えると煩雑 |
クラス継承 | 責務の分離、OCPに沿った拡張がしやすい | クラスが増えすぎると煩雑、継承構造の把握が必要 |
デリゲート注入 | 柔軟・動的に処理差し替えができる | どこで何が設定されたか分かりづらい |
5.使い分けの判断基準
✅ 条件分岐(if)で十分なケース
- ランクやルールが数パターンしかなく、今後も増える予定がない
- 小さな関数内で完結し、複雑なロジックが不要
🚩 例:「社内ツールの簡易バッチ」「試作コード」
✅ クラス継承での実装が適しているケース
- ルールごとに処理が大きく異なる
- 各ランクに状態や他のメソッドも持たせたい
- ルール追加が予想され、保守性を重視したいとき
🚩 例:「出力帳票形式の切り替え」「会員種別による処理分岐」
✅ デリゲート注入が有効なケース
- 処理ロジックを動的に差し替えたい(実行時に変わる)
- 単一の処理だけ切り替えたい、柔軟性を重視
- テストや一時的な差し替えに便利(Mockにも使える)
🚩 例:「テスト中だけ特別な値引き」「UIからユーザーがルール選択」
6.業務での具体的な適用例
シーン | 適用方法 | 理由 |
---|---|---|
小規模ツールでの単純な割引 | 条件分岐 | シンプルで実装コストが低い |
社員ランクに応じた手当計算 | クラス継承 | ランクに応じて他の処理も絡む可能性があるため |
特売期間中の一時的な割引 | デリゲート注入 | 実行時に設定で切り替えたい |
7.まとめ:道具は使い分けてこそ
「デリゲート注入」も、目的に応じて正しく選べば強力な武器になります。
ただ、個人的には「デリゲート注入」は柔軟過ぎて、多用すると後が怖いです。
大切なのは「変更されるものを外に出す」=責務の分離と、
将来の変化に耐えられる設計にする事だと思います。
X. おまけ(ラムダ式が苦手な方へ:3つの書き方比較)
以下の3種類のコードは全て同じ意味です
- 文字列を受け取り、文字列数を返す
✅ 関数を割り当て
int GetLength(string s)
{
return s.Length;
}
Func<string, int> GetLengthByFunction = GetLength;
// 実行例
int len = GetLengthByFunction("ねこ"); // len = 2
✅ 匿名デリゲート
Func<string, int> GetLengthByDelegate = delegate(string s) { return s.Length; };
// 実行例
int len = GetLengthByDelegate("いぬ"); // len = 2
✅ ラムダ式
Func<string, int> GetLengthByLambda = s => s.Length;
// 実行例
int len = GetLengthByLambda("とり"); // len = 2