ポリモーフィズムで分岐を減らすとは何か
「ポリモーフィズムによる分岐削減」は、if や switch でゴチャゴチャ分岐している処理を、
「オブジェクトの“型ごとの振る舞いの違い”に任せてスッキリさせる」考え方です。
リファクタリングの世界では「条件分岐のポリモーフィズムへの置き換え(Replace Conditional with Polymorphism)」という名前で知られています。
複雑な条件分岐は種類が増えるたびにコードが肥大化して修正が辛くなりますが、ポリモーフィズムを使うと新しいケース追加時に既存コードをほとんど触らずに拡張できます。
ざっくり言うと、
「種類ごとの違いを if/switch に閉じ込める」のではなく
「種類ごとにクラスを分けて、それぞれに自分の仕事を書いてもらう」
という方向に発想を切り替える、ということです。
典型的な悪い例:種別コード+switch だらけのクラス
switch で種類ごとのロジックを書いてしまう
まずは、よくある「悪い例」から。
final class Employee {
static final int ENGINEER = 1;
static final int MANAGER = 2;
static final int SALES = 3;
private final int type;
private final int baseSalary;
private final int bonus;
private final int commission;
Employee(int type, int baseSalary, int bonus, int commission) {
this.type = type;
this.baseSalary = baseSalary;
this.bonus = bonus;
this.commission = commission;
}
int calcPay() {
switch (type) {
case ENGINEER:
return baseSalary;
case MANAGER:
return baseSalary + bonus;
case SALES:
return baseSalary + commission;
default:
throw new IllegalArgumentException("unknown type");
}
}
}
Java一見シンプルですが、問題はここからです。
種類が増えるたびに switch に case を足していかなければならない。calcPay 以外にも「勤務時間の計算」「役職別メッセージ」などが増えると、同じ switch(type) があちこちにコピペされる。
新しい type を追加したいとき、全ての switch を探して修正しないといけない。
これがまさに「条件分岐の乱用」「Switch Statements の悪臭」と呼ばれる状態です。
ポリモーフィズムで書き直す基本パターン(重要)
共通のインターフェース(または抽象クラス)を用意する
まず「従業員は給料を計算できる」という共通の契約をインターフェースで表現します。
interface Employee {
int calcPay();
}
Java種類ごとにクラスを分けて、振る舞いをオーバーライドする
次に、種類ごとに実装クラスを用意します。
final class Engineer implements Employee {
private final int baseSalary;
Engineer(int baseSalary) {
this.baseSalary = baseSalary;
}
@Override
public int calcPay() {
return baseSalary;
}
}
final class Manager implements Employee {
private final int baseSalary;
private final int bonus;
Manager(int baseSalary, int bonus) {
this.baseSalary = baseSalary;
this.bonus = bonus;
}
@Override
public int calcPay() {
return baseSalary + bonus;
}
}
final class Sales implements Employee {
private final int baseSalary;
private final int commission;
Sales(int baseSalary, int commission) {
this.baseSalary = baseSalary;
this.commission = commission;
}
@Override
public int calcPay() {
return baseSalary + commission;
}
}
Javaここで重要なのは、
「種類ごとの違いを switch に書く」のではなく
「種類ごとのクラスに、それぞれ自分のロジックを書いてもらう」
という設計に変えたことです。
使う側は「型を意識せず」同じメソッドを呼ぶだけ
利用側のコードはこうなります。
Employee e1 = new Engineer(300_000);
Employee e2 = new Manager(400_000, 100_000);
Employee e3 = new Sales(250_000, 150_000);
int p1 = e1.calcPay();
int p2 = e2.calcPay();
int p3 = e3.calcPay();
Javaここには if も switch もありません。
「従業員なら calcPay() ができる」という抽象にだけ依存していて、
具体的にどのクラスかは気にしていません。
新しい種類(例えば PartTimer)を追加したいときは、
PartTimer implements Employee クラスを新しく作るだけでよく、
既存の calcPay() 呼び出し側は一切変更せずに済みます。
これが「分岐をポリモーフィズムに置き換える」ことの大きな価値です。
「分岐を減らす」という発想の切り替え方(深掘り)
1. 「何で分岐しているか」を見つける
最初のステップは、
どの if / switch が「種類ごとの振る舞いの違い」を扱っているかを見抜くことです。
典型的には、次のようなパターンです。
種別コード(type や kind)に応じて処理を切り替えている。
クラス名や enum の name() を見て if している。instanceof で具体クラスを見て分岐している。
こういう分岐が「種類ごとの振る舞い」を意味していることが多いです。
2. 抽象メソッドにしたときに「しっくり来るか」を考える
例えば、さっきの例なら
「従業員なら calcPay できるよね」
というのは、抽象メソッドとしてとても自然です。
逆に「従業員には getType がある」という抽象は、
「種類コードを教えてもらって、外で switch します」という設計を意味します。
これはポリモーフィズム的には逆方向です。
ポリモーフィズムに向いているのは、
「〜できる」という動詞(振る舞い)で表現できるものです。
double calcPay()String format()boolean matches(String input)
など、「要するにこれがしたいんだよね」という行為に名前をつけて、
それを抽象メソッドにしてしまうイメージです。
戦略パターンとしての分岐削減(アルゴリズムの差し替え)
「割引方法ごとの if/else」をポリモーフィックにする
もう少し身近な例として、「割引のやり方で分岐している」コードを見てみます。
final class DiscountService {
int apply(int price, String type) {
if ("none".equals(type)) {
return price;
} else if ("rate".equals(type)) {
return price - price * 10 / 100;
} else if ("fixed".equals(type)) {
return price - 100;
}
throw new IllegalArgumentException("unknown type");
}
}
Javatype で if している典型例です。
これをポリモーフィズムで書き直すと、戦略パターン(Strategy)になります。
interface Discount {
int apply(int price);
}
final class NoDiscount implements Discount {
@Override
public int apply(int price) {
return price;
}
}
final class RateDiscount implements Discount {
private final int percent;
RateDiscount(int percent) {
this.percent = percent;
}
@Override
public int apply(int price) {
return price - price * percent / 100;
}
}
final class FixedDiscount implements Discount {
private final int amount;
FixedDiscount(int amount) {
this.amount = amount;
}
@Override
public int apply(int price) {
return price - amount;
}
}
Javaサービス側は、Discount にだけ依存します。
final class DiscountService {
private final Discount discount;
DiscountService(Discount discount) {
this.discount = discount;
}
int apply(int price) {
return discount.apply(price); // ここにはもう if も switch もない
}
}
Java呼び出し側はこうなります。
DiscountService s1 = new DiscountService(new NoDiscount());
DiscountService s2 = new DiscountService(new RateDiscount(10));
DiscountService s3 = new DiscountService(new FixedDiscount(100));
Java新しい割引ロジックを追加したいときは、新しい Discount 実装クラスを作るだけで済みます。DiscountService とその呼び出し側には手を入れずに拡張できます。
これは OCP(拡張に開き、修正に閉じる)の実現パターンのひとつです。
分岐削減のメリットと「やりすぎ注意」ポイント
メリット:変更に強く、読みやすく、テストしやすい
ポリモーフィズムで分岐を削ると、次のような良さが出ます。
新しい種類追加のとき、既存の switch / if を探して回収しなくてよい。
コードの見通しが良くなり、各クラスが「自分の責務」に集中できる。
テストがしやすくなる(種類ごとにクラス単体でテストできる)。
特に「同じような switch が複数箇所にある」場合は、
ポリモーフィズムに置き換えることで「1箇所に責務を集約」できます。
やりすぎ注意:何でもクラス分けすればいいわけではない
一方で、「全部をクラスに分ければいい」というわけでもありません。
種類が 2つだけで、増える見込みもなく、ロジックも激シンプル。
一時的な分岐で、ビジネスとして種類の概念が重要なわけではない。
こういった場合は、素直な if のほうが読みやすいこともあります。
ポリモーフィズムへ置き換えるべきなのは特に、
条件が増えそう
同じ分岐があちこちにコピペされている
種類ごとに意味のある「役割」がある
といったケースです。
まとめ:分岐を見たら「クラスにしてもらえないか?」と考える
ポリモーフィズムによる分岐削減は、
「if/switch が悪い」のではなく「if/switch に詰め込まれた“種類ごとの責務”を、それぞれのクラスに移してあげる」
という発想の転換です。
type や kind で分岐しているコードを見たら、心の中でこう問いかけてみてください。
「これ、種類ごとにクラスを分けたほうが自然じゃないか?」
「“〜できる”というメソッド名で抽象化できないか?」
「新しい種類が増えたとき、今のままで安全に拡張できるか?」
