オブジェクト指向の悟りを開く!初心者でも分かるOOPの最重要技術ポリモーフィズムの使い方!
オブジェクト指向の三大要素は
- 継承
- カプセル化
- ポリモーフィズム
です。中でもポリモーフィズムが最も強力かつ難解な技術と言われています。
Rubyの開発者まつもとゆきひろさんも
「オブジェクト指向プログラミングを構成するテクニックのうち,ポリモーフィズムは最も重要なものと言えます。」
と仰っています(まつもと直伝 プログラミングのオキテ 第1回(3))。
MSDNでも
「ポリモーフィズムは、オブジェクト指向プログラミングにおいて最も重要な概念であるともいわれています。」
とあります(インターフェイス ベース プログラミングについて)。
私も同意見です。
ポリモーフィズムを理解した時、悟りを開いたかのように「オブジェクト指向ってそういうことかぁ!!」と心から腑に落ちたのを覚えています。
ポリモーフィズムの説明として、
Animalクラスを継承したCatクラスの鳴くメソッドを呼び出すと「ニャ~」と鳴く、Dogクラスの場合、「ワン」と吠える
みたいなものをよく目にします。だから何なの?って感じですよね。
このような説明では実際のプログラミングにどう使えばいいのかがわかりません。
この記事ではもっと実践的な実際に使える例を紹介します。
ポリモーフィズムの本質を一言で言い表すならば、
「ポリモーフィズムとは、呼び出す側のコードを共通化する技術である」
です。
「なんじゃそりゃ?」と思われた方も安心してください。この記事を読み終わるころには「なるほど、そういうことか!」と納得されるはずです。
重複のあるサンプルコードを手続き的なやり方とポリモーフィズムを使ったやり方で共通化していきます。
そのアプローチの違いを見ることでポリモーフィズムを理解できます。
これを読めば、あなたもオブジェクト指向の「悟り」を開けます!
Contents
デザインパターンはポリモーフィズムのパターン
23個のデザインパターンの内Singleton、Facadeなどを除くほとんどのパターンはポリモーフィズムをどう使うかのパターンです。
ポリモーフィズムのパターンも大きく分けて以下の2つに分類できます。
- Strategyパターン型
- Template Methodパターン型
ですから、StrategyパターンとTemplate Methodパターンを覚えれば、ポリモーフィズムを理解できるんです。
おすすめ本は結城浩さんのJava言語で学ぶデザインパターン入門です。
私はこの本を読んでポリモーフィズムを悟りました。
なので、この本を読めばいいんですけど、けっこう分厚い本なので読むのも大変です。この記事ではコンパクトにエッセンスを凝縮して解説します。
※ この本は分厚いけど、文章が読みやすいので、読むのはそれほど大変ではありません。
重複したコードを共通化する方法は2種類
重複したコードを共通化する方法は多数ありますが、本質的には以下の2つに絞られます。
- 重複したコードをメソッド/関数に切り出して共通化する
- ポリモーフィズムで処理の流れを共通化する
あと、もう一つ入れるとすればAOPで前後処理を追加するっていう方法がありますが、今回は省きます。
では次の2つのプログラムから重複ロジックを抽出して共通化を施してみましょう。
↓1つ目のプログラム
import java.io.BufferedReader; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.PrintStream; import java.io.Reader; public class AppLogAnalyzer { public static void main(String[] args) { // 解析対象のファイルを読み込む InputStream inputStream = null; Reader inputStreamReader = null; BufferedReader in = null; int warningCount = 0; int errorCount = 0; try { inputStream = new FileInputStream("applog.txt"); inputStreamReader = new InputStreamReader(inputStream); in = new BufferedReader(inputStreamReader); String line = null; while ((line = in.readLine()) != null) { // エラーと警告の件数をカウントする if (line.indexOf("[ERROR]") >= 0) { errorCount++; } else if (line.indexOf("[WARN]") >= 0) { warningCount++; } } } catch (FileNotFoundException e) { e.printStackTrace(); return; } catch (IOException e) { e.printStackTrace(); return; } finally { if (in != null) { try { in.close(); } catch (IOException e) { } } } // 集計結果をファイルに保存する FileOutputStream fileOutputStream = null; PrintStream out = null; try { fileOutputStream = new FileOutputStream("applogAnalyze.txt"); out = new PrintStream(fileOutputStream); out.println("error=" + errorCount + "件"); out.println("warning=" + warningCount + "件"); } catch (FileNotFoundException e) { e.printStackTrace(); return; } finally { if (out != null) { out.close(); } } } }
↓2つ目がこちら。とても似ているコードですが、少しだけ違います。つまりコードが重複しています。
import java.io.BufferedReader; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.PrintStream; import java.io.Reader; public class WebLogAnalyzer { public static void main(String[] args) { // 解析対象のファイルを読み込む InputStream inputStream = null; Reader inputStreamReader = null; BufferedReader in = null; int count = 0; try { inputStream = new FileInputStream("weblog.txt"); inputStreamReader = new InputStreamReader(inputStream); in = new BufferedReader(inputStreamReader); String line = null; while ((line = in.readLine()) != null) { // 会社情報ページのアクセス数をカウントする String[] tokens = line.split("\t"); if (tokens[5].equals("company_info.html")) { count++; } } } catch (FileNotFoundException e) { e.printStackTrace(); return; } catch (IOException e) { e.printStackTrace(); return; } finally { if (in != null) { try { in.close(); } catch (IOException e) { } } } // 集計結果をファイルに保存する FileOutputStream fileOutputStream = null; PrintStream out = null; try { fileOutputStream = new FileOutputStream("weblogAnalyze.txt"); out = new PrintStream(fileOutputStream); out.println(count); } catch (FileNotFoundException e) { e.printStackTrace(); return; } finally { if (out != null) { out.close(); } } } }
どちらも
- ファイルを読む
- ファイルの内容を解析する
- 解析結果をファイルに出力する
というプログラムです。ファイルオープン/クローズ、例外処理などが重複しています。
共通化を施すためのサンプルコードなのでわざと冗長な書き方をしている部分もありますが効果を分かりやすくするための演出なのでJavaをディスる主旨はありません。
1. 重複したコードをメソッド/関数に切り出して共通化する
まずは多くの人にとってなじみのある手続き型言語的なアプローチで共通化してみましょう。
重複したコードをメソッドに切り出します。ファイルをオープン・クローズするコードをメソッドに切り出してみましょう。
import java.io.BufferedReader; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.InputStream; import java.io.InputStreamReader; import java.io.PrintStream; import java.io.Reader; public class FileUtil { public static BufferedReader openInputFile(String fileName) { InputStream inputStream = null; Reader inputStreamReader = null; BufferedReader in = null; try { inputStream = new FileInputStream(fileName); inputStreamReader = new InputStreamReader(inputStream); in = new BufferedReader(inputStreamReader); } catch (FileNotFoundException e) { e.printStackTrace(); return null; } return in; } public static PrintStream openOutputFile(String fileName) { FileOutputStream fileOutputStream = null; PrintStream out = null; try { fileOutputStream = new FileOutputStream(fileName); out = new PrintStream(fileOutputStream); } catch (FileNotFoundException e) { e.printStackTrace(); return null; } return out; } }
FileUtilという共通クラスにメソッドを切り出しました。このクラスを使うようにAppLogAnalyzerとWebLogAnalyzerを書き換えましょう。
import java.io.BufferedReader; import java.io.IOException; import java.io.PrintStream; //少しコードは短くなったがまだまだ長い public class AppLogAnalyzer { public static void main(String[] args) { // 解析対象のファイルを読み込む BufferedReader in = null; int warningCount = 0; int errorCount = 0; try { in = FileUtil.openInputFile("applog.txt"); if (in == null) { return; } String line = null; while ((line = in.readLine()) != null) { // エラーと警告の件数をカウントする if (line.indexOf("[ERROR]") >= 0) { errorCount++; } else if (line.indexOf("[WARN]") >= 0) { warningCount++; } } } catch (IOException e) { e.printStackTrace(); return; } finally { if (in != null) { try { in.close(); } catch (IOException e) { } } } // 集計結果をファイルに保存する PrintStream out = null; try { out = FileUtil.openOutputFile("applogAnalyze.txt"); if (out == null) { return; } out.println("error=" + errorCount + "件"); out.println("warning=" + warningCount + "件"); } finally { if (out != null) { out.close(); } } } }
import java.io.BufferedReader; import java.io.IOException; import java.io.PrintStream; //少しコードは短くなったがまだまだ長い public class WebLogAnalyzer { public static void main(String[] args) { // 解析対象のファイルを読み込む BufferedReader in = null; int count = 0; try { in = FileUtil.openInputFile("weblog.txt"); if (in == null) { return; } String line = null; while ((line = in.readLine()) != null) { // 会社情報ページのアクセス数をカウントする String[] tokens = line.split("\t"); if (tokens[5].equals("company_info.html")) { count++; } } } catch (IOException e) { e.printStackTrace(); return; } finally { if (in != null) { try { in.close(); } catch (IOException e) { } } } // 集計結果をファイルに保存する PrintStream out = null; try { out = FileUtil.openOutputFile("weblogAnalyze.txt"); if (in == null) { return; } out.println(count); } finally { if (out != null) { out.close(); } } } }
重複コードが減って多少すっきりしましたが、try-catchのコードが重複しています。
手続き型言語的アプローチだとここまでが限界なのですが、オブジェクト指向言語の機能を使って、これらを共通化できないでしょうか?
やり方が思いつきませんか?
ここでポリモーフィズムの出番なのです。
2. ポリモーフィズムで処理の流れを共通化する
Javaでポリモーフィズムを使うためには抽象クラスかインターフェースを使います。このサンプルプログラムの場合、処理の流れに2つのパターンがあります。
パターン1
1. 読み込み用ファイルをオープンする
2. ファイルを読んでなんらかの集計処理をする
3. ファイルを閉じる
4. 例外発生時にはスタックトレースを出力する
パターン2
1. 出力用ファイルをオープンする
2. ファイルになんらかの集計結果を出力する
3. ファイルを閉じる
なんらかの~という部分以外は同じ流れになっていることが分かります。
そこでなんらかの処理をする部分を抽象メソッドとして抽出して、共通の処理の流れの中から、抽象メソッドを呼び出すつくりにします。
抽象メソッドはサブクラスで実装するのでサブクラスが何かによって、「なんらかの処理」が具体的な処理に決まります。
このような設計をTemplate Methodパターンと言います。Template Methodパターンを使って共通化を施してみましょう!
import java.io.BufferedReader; import java.io.FileNotFoundException; import java.io.IOException; import java.io.PrintStream; public abstract class FileAnalyzer { /** * このメソッドは処理の全体フローを記述するTemplate Methodです。 * ファイル読み込み & 集計処理 * 1. 読み込み用ファイルをオープンする * 2. ファイルを読んでなんらかの集計処理をする(抽象メソッド:readFileを呼び出す) * 3. ファイルを閉じる * 4. 例外発生時にはスタックトレースを出力する * * 集計結果ファイル出力処理 * 1. 出力用ファイルをオープンする * 2. ファイルになんらかの集計結果を出力する(抽象メソッド:writeFileを呼び出す) * 3. ファイルを閉じる * */ public void analyzeFile(String inputFile, String outputFile) { // 解析対象のファイルを読み込む BufferedReader in = FileUtil.openInputFile(inputFile); if (in == null) { return; } try { readFile(in); } catch (FileNotFoundException e) { e.printStackTrace(); return; } catch (IOException e) { e.printStackTrace(); return; } finally { if (in != null) { try { in.close(); } catch (IOException e) { } } } // 集計結果をファイルに保存する PrintStream out = FileUtil.openOutputFile(outputFile); if (out == null) { return; } try { writeFile(out); } finally { if (out != null) { out.close(); } } } /** サブクラスでファイルを読んでなんらかの集計処理を実装するための抽象メソッド */ protected abstract void readFile(BufferedReader in) throws FileNotFoundException, IOException; /** サブクラスでファイルになんらかの集計結果を出力する処理を実装するための抽象メソッド */ protected abstract void writeFile(PrintStream out); }
このFileAnalyzerクラスをAppLogAnalyzerとWebLogAnalyzerに継承させて、抽象メソッド(何らかの処理だったもの)を具体化するために実装します。
import java.io.BufferedReader; import java.io.FileNotFoundException; import java.io.IOException; import java.io.PrintStream; //最初のバージョンに比べてかなりコードが短くなっている public class AppLogAnalyzer extends FileAnalyzer{ private int warningCount = 0; private int errorCount = 0; @Override protected void readFile(BufferedReader in) throws FileNotFoundException, IOException { String line = null; while ((line = in.readLine()) != null) { // エラーと警告の件数をカウントする if (line.indexOf("[ERROR]") >= 0) { errorCount++; } else if (line.indexOf("[WARN]") >= 0) { warningCount++; } } } @Override protected void writeFile(PrintStream out) { // 集計結果をファイルに保存する out.println("error=" + errorCount + "件"); out.println("warning=" + warningCount + "件"); } public static void main(String[] args) { AppLogAnalyzer obj = new AppLogAnalyzer(); obj.analyzeFile("applog.txt", "applogAnalyze.txt"); } }
import java.io.BufferedReader; import java.io.FileNotFoundException; import java.io.IOException; import java.io.PrintStream; //最初のバージョンに比べてかなりコードが短くなっている public class WebLogAnalyzer extends FileAnalyzer { int count = 0; @Override protected void readFile(BufferedReader in) throws FileNotFoundException, IOException { String line = null; while ((line = in.readLine()) != null) { // 会社情報ページのアクセス数をカウントする String[] tokens = line.split("\t"); if (tokens[5].equals("company_info.html")) { count++; } } } @Override protected void writeFile(PrintStream out) { // 集計結果をファイルに保存する out.println(count); } public static void main(String[] args) { AppLogAnalyzer obj = new AppLogAnalyzer(); obj.analyzeFile("weblog.txt", "weblogAnalyze.txt"); } }
FileAnalyzerのanalyzeFileメソッドで処理の全体フローを記述しています。フローの中で異なる部分を抽象メソッド呼び出しすることでサブクラスの固有処理に遷移します。
このプログラムの場合、全体の流れはいっしょなんだけど、集計ロジックが異なっています。AppLogAnalyzerがエラーと警告の件数をカウントしてるのに対してWebLogAnalyzerは会社情報ページのアクセス数をカウントしています。その固有ロジックを抽象メソッドに切り出しています。
手続き型のアプローチだと共通部分をメソッドに切り出していましたが、ポリモーフィズムを使う場合は固有部分を抽象メソッドに切り出します。
真逆のアプローチです。
これらは補完関係にあるのでどちらが偉いというわけではありません。
処理の流れは似ているんだけど一部分だけ違うっていう場合はポリモーフィズムを使って処理の流れを共通化するアプローチが有効です。
単純に重複コードがある場合はそれらをメソッドに切り出して使用する手続き型のアプローチが有効です。
いわゆるフレームワークというものは必ずポリモーフィズムを使用しています。
ライブラリは固有ロジックから呼び出されますが、フレームワークの場合、フレームワークが固有ロジックを呼び出します。
- 呼び出される側のコードを共通化したのがライブラリ
- 呼び出す側のコードを共通化したのがフレームワーク
なんです。
これが、制御の反転(Inversion of Control、IoC)ってやつなんです。
ポリモーフィズムを使えば手続き型言語の数倍共通化できる!
いかがだったでしょうか?
ポリモーフィズムを使えば、重複ロジックをメソッドに切り出すよりも多くのコードを共通化できることがお分かりいただけたかと思います。
AppLogAnalyzerとWebLogAnalyzerを最初のバージョン、手続き型アプローチで共通化を施したバージョン、ポリモーフィズムで共通化を施したバージョンとで見比べてみてください。ポリモーフィズムバージョンのコードは驚くほど短くなっています。
AppLogAnalyzerは68行から36行へ、WebLogAnalyzerは64行から33行へと減っています。
- 処理の流れは似ているんだけど一部分だけ違う
- 例外をとらえる処理を共通化したい
そんな場合、ポリモーフィズムが効きます。
もちろん、手続き型のアプローチが有効な場面も多々ありますから、適材適所で使い分けてみてください!