ここでは、ポリモフィズムについて初心者が理解できるようにわかりやすく説明します。
使用する言語はJavaです。
実現したいこと
ポリモフィズムが実現したいことは、機能を交換可能にすることです。
例えば、あなたが次のようなセンスあふれるかっこいいロボットを作ったとしましょう。
現在、手には丸いものがついていますが、これを「ドリルにしてください」という依頼があったとします。
しかし、この手は腕に完全に接着されており、手を変えようと思ったら腕ごと胴体から切り離さないといけません。
これでは困るわけです。
手の機能は、状況に合わせて交換できるようにしておいたほうが良さそうです。
どうしたらいいでしょうか。
ここでは次のように、腕にガションッとはめられる穴を作ったとしましょう。
そして、手の方にも腕の穴の形に合わせた結合部をつけます。
こうしておけば、手の機能を簡単に交換できます。
新しい手を用意したら、同じ規格の結合部を持たせれば付け替えることができるのです。
このように規格を揃えて交換可能にしておくことで、本体を壊すことなく機能を変更できます。
ひっきりなしに機能変更が行われるソフトウェアでもこれができたら、本体のビジネスロジックに影響を与えずに機能を交換できそうです。
ポリモフィズムはソフトウェアでの機能の交換可能にします。
プログラムでポリモフィズムを実現する
ここではString配列を標準出力するPrinterクラスを例に考えてみます。
public class PlainPrinter {
private String[] values;
public PlainPrinter(String[] values) {
// コンストラクタでString配列を受け取る
this.values = values;
}
public void print() {
// 配列をスペースで結合して出力する
System.out.println(String.join(" ", values));
}
}
ポリモフィズムを利用しない例
まずポリモフィズムを利用しない例を見てみましょう。
先ほどのPlainPrinterクラスを使って、次のように出力したいという要件だったとします。
------------------
Java PHP Ruby
------------------
そこで、PlainPrinterクラスを利用するPrinterServiceクラスを作成します。
public class PrinterService {
private PlainPrinter plainPrinter;
public PrinterService(PlainPrinter plainPrinter) {
// コンストラクタでPlainPrinterを受け取る
this.plainPrinter = plainPrinter;
}
public void printInfo() {
// 出力の作成
System.out.println("------------------");
plainPrinter.print();
System.out.println("------------------");
}
}
PrinterServiceクラスはビジネスロジックを担当するクラスです。
最後にmainメソッドからPrinterServiceを呼び出します。
public class Main {
public static void main(String[] args) {
// PlainPrinterインスタンスを作成
String[] values = {"Java", "PHP", "Ruby"};
PlainPrinter printer = new PlainPrinter(values);
// PrinterService呼び出し
PrinterService printerService = new PrinterService(printer);
printerService.printInfo();
}
}
次のように出力されます。
------------------
Java PHP Ruby
------------------
この時点で特に問題はありません。
さてここで、次のような仕様変更が入ったとしましょう。
えらい人「やっぱスペース区切りじゃなくて、カンマ区切りがいいなぁ。」
しんどい仕様変更です。
PlainPrinterクラスの「" "」を「","」に変更するのも一つの手ですが、PlainPrinterクラス自体は他のクラスからも呼び出されている可能性があります。
影響範囲の調査、テスト、したくないですよね。
PlainPrinterを直接変更するのは得策ではないでしょう。
ここでは新しくCsvPrinterを作って差し替えたほうが良さそうです。
public class CsvPrinter {
private String[] values;
public CsvPrinter(String[] values) {
this.values = values;
}
public void print() {
// 配列をカンマで結合して出力する
System.out.println(String.join(",", values));
}
}
さて、CsvPrinterを使うためには、PrinterServiceを書き換える必要があります。
PlainPrinterをCsvPrinterに書き換えます。
public class PrinterService {
- private PlainPrinter plainPrinter;
+ private CsvPrinter csvPrinter;
- public PrinterService(PlainPrinter plainPrinter) {
+ public PrinterService(CsvPrinter csvPrinter) {
- this.plainPrinter = plainPrinter;
+ this.csvPrinter = csvPrinter;
}
public void printInfo() {
System.out.println("------------------");
- plainPrinter.print();
+ csvPrinter.print();
System.out.println("------------------");
}
}
最後にmainメソッドで、PrinterServiceにCsvPrinterのインスタンスを注入します。
public class Main {
public static void main(String[] args) {
// CsvPrinterインスタンスを作成
String[] values = {"Java", "PHP", "Ruby"};
- PlainPrinter printer = new PlainPrinter(values);
+ CsvPrinter printer = new CsvPrinter(values);
// PrinterService呼び出し
PrinterService printerService = new PrinterService(printer);
printerService.printInfo();
}
}
出力結果は次のようになります。
------------------
Java,PHP,Ruby
------------------
一見、問題なさそうに見えます。
しかし、実は大きな問題がありました。
それは、Printerの機能を変えるためにPrinterServiceの内容を書き換えなくてはならなかったことです。
これはロボットで言えば、手の機能を変えるために腕もしくは胴を変更していることになります。
大本を変更すれば、それだけ影響範囲が広がります。
影響範囲を調べ、テストもやり直しです。
PrinterServiceを変更せずに、機能を変更できれば理想的です。
ポリモフィズムを利用した例
ロボットの例では規格を揃えることで、手の機能を交換可能にしました。
Javaプログラムで規格を揃えるには、インターフェースを利用します。
インターフェースの役割は、クラスの規格を揃えることなのです。
それではPlainPrinterとCsvPrinterの規格となるPrinterインターフェースを定義しましょう。
public interface Printer {
public void print();
}
それぞれのクラスに実装します。
- public class PlainPrinter {
+ public class PlainPrinter implements Printer {
private String[] values;
public PlainPrinter(String[] values) {
this.values = values;
}
public void print() {
System.out.println(String.join(" ", values));
}
}
- public class CsvPrinter {
+ public class CsvPrinter implements Printer {
private String[] values;
public CsvPrinter(String[] values) {
this.values = values;
}
public void print() {
System.out.println(String.join(",", values));
}
}
元々同じ形だったので、ほとんど変更は不要です。
次にPrinterServiceを次のように書き換えます。
public class PrinterService {
- private CsvPrinter csvPrinter;
+ private Printer printer;
- public PrinterService(CsvPrinter csvPrinter) {
+ public PrinterService(Printer printer) {
- this.csvPrinter = csvPrinter;
+ this.printer = printer;
}
public void printInfo() {
System.out.println("------------------");
- csvPrinter.print();
+ printer.print();
System.out.println("------------------");
}
}
さて、インターフェースを挟んだだけなので実際の処理に変更はありません。
出力結果は次のようになります。
------------------
Java,PHP,Ruby
------------------
これで完成です。
交換によって機能変更できるようになりました。
えらいひと「やっぱりスペース区切りに戻したいなぁ。」
先ほどは仕様変更にイライラしていたあなたも今度は笑顔で仕様変更を受け入れることができます。
mainメソッドを次のように書き換えてください。
public class Main {
public static void main(String[] args) {
// Printerインスタンスを作成
String[] values = {"Java", "PHP", "Ruby"};
- CsvPrinter printer = new CsvPrinter(values);
+ Printer printer = new PlainPrinter(values);
// PrinterService呼び出し
PrinterService printerService = new PrinterService(printer);
printerService.printInfo();
}
}
PrinterServiceに注入するインスタンスをPlainPrinterに書き換えました。
出力結果は次のようになります。
------------------
Java PHP Ruby
------------------
どうですか?PrinterServiceもPlainPrinter、CsvPrinterも修正することなく、機能を交換することができました。
PlainPrinterとCsvPrinterはなぜ交換できるのでしょうか。
それは同じ規格(インターフェース)だからです。
せっかくなのでもう一回やってみましょう。
えらいひと「やっぱスペースじゃなくて改行で」
大丈夫です。新しいPrinterを作って交換するだけです。
public class BreakPrinter implements Printer {
private String[] values;
public BreakPrinter(String[] values) {
this.values = values;
}
public void print() {
// 配列を改行コードで結合して出力する
System.out.println(String.join("\r\n", values));
}
}
BreakPrinterもPrinterインターフェースに沿って作られているので交換可能です。
public class Main {
public static void main(String[] args) {
// Printerインスタンスを作成
String[] values = {"Java", "PHP", "Ruby"};
- Printer printer = new PlainPrinter(values);
+ Printer printer = new BreakPrinter(values);
// PrinterService呼び出し
PrinterService printerService = new PrinterService(printer);
printerService.printInfo();
}
}
出力結果は・・・
------------------
Java
PHP
Ruby
------------------
新しいインスタンスを注入するだけで、既存のコードを変更せずに機能変更できました。
Spring FrameworkなどにみられるDIコンテナを利用するとインスタンスの注入すらも自動化できます。
ポリモフィズムとは
ポリモフィズムは多態性と訳されます。
同じプログラムなのに、注入されるインスタンスの種類によって異なる動作をする様を多態(様々な態度を取る)と呼んでるんですね。
これは、本体となるプログラムを変更せずに機能を交換できることを意味しています。
ポリモフィズムを実現して、保守性、拡張性の高いシステムを目指しましょう。