Template Methodパターンの実装あれこれ
Template Methodは、GoF (Gang of Four) のデザインパターンの1つで、処理の共通部分をスーパークラスに抽出し、固有の処理をサブクラスで実装することで、最適なアプリケーション構造にするパターンである。
今回は、Java7およびJava8におけるTemplate Methodパターンの実装について検討する。
Template Methodの実装(Java7)
まずはJava7におけるTemplate Methodパターンの実装例を示す。
ここでは、簡単なファイルの読み込み処理を例に取り上げる。
一般的なコードは次のようになる。
try (BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream(fileName)))) { String line; while ((line = reader.readLine()) != null) { // 省略(読み込んだ1行ごとに処理を行う) } } catch (IOException e){ throw new RuntimeException("error", e); }
この処理に、Template Methodパターンを適用すると次のようになる。
// 抽象クラス abstract class FileProcessor { // テンプレートメソッド public void readFile(String fileName){ try (BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream(fileName)))) { String line; while ((line = reader.readLine()) != null) { doProcessLine(line); // フックメソッドの呼び出し } } catch (IOException e){ throw new RuntimeException("error", e); } } // フックメソッド(具象クラスに実装を任せる) protected abstract void doProcessLine(String line); } // 具象クラス class SimpleFileProcessor extends FileProcessor { //フックメソッドの実装 @Override protected void doProcessLine(String line) { System.out.println(line); } } class Example { public static void main(String[] args) { new SimpleFileProcessor().readFile("sample.txt"); } }
インタフェースを使ったTemplate Methodの実装(Java7)
上記の方法だと、具象クラスは抽象クラスを継承する必要があるため、すでに別のスーパークラスを継承している具象クラスには適用できない。
そこで少し工夫して、抽象クラスを継承する代わりに、インタフェースを実装するように変更してみよう。
// フックメソッドを定義したインタフェース interface LineProcessor { void doProcessLine(String line); } // テンプレートクラス class FileProcessor { // テンプレートメソッド public void readFile(String fileName, LineProcessor processor){ try (BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream(fileName)))) { String line; while ((line = reader.readLine()) != null) { processor.doProcessLine(line); // フックメソッドの呼び出し } } catch (IOException e){ throw new RuntimeException("error", e); } } } // 具象クラス class SimpleLineProcessor implements LineProcessor { // フックメソッドの実装 @Override public void doProcessLine(String line) { System.out.println(line); } } class Example { public static void main(String[] args) { new FileProcessor().readFile("sample.txt", new SimpleLineProcessor()); } }
ここではFileProcessor
の代わりにLineProcessor
を実装するようになったため、具象クラスの名前をSimpleLineProcessor
に変更している。
このコードにおけるSimpleLineProcessor
の責務は、ファイル処理全体ではなく、1行分の処理だけになっている。このため、このクラス構造は、Template MethodパターンよりもStrategyパターンと考えるほうが適切かもしれない。
テンプレートをstaticメソッドにする
テンプレートクラスをインタフェースにすると、readFile
メソッドはstatic
指定のユーティリティメソッドにできる。
// フックメソッドを定義したインタフェース interface LineProcessor { void doProcessLine(String line); } // テンプレートクラス class FileProcessor { // テンプレートメソッド public static void readFile(String fileName, LineProcessor processor) { try (BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream(fileName)))) { String line; while ((line = reader.readLine()) != null) { processor.doProcessLine(line); // フックメソッドの呼び出し } } catch (IOException e){ throw new RuntimeException("error", e); } } } // 具象クラス class SimpleLineProcessor implements LineProcessor { // フックメソッドの実装 @Override public void doProcessLine(String line) { System.out.println(line); } } class Example { public static void main(String[] args) { FileProcessor.readFile("sample.txt", new SimpleLineProcessor()); } }
匿名クラスの利用
さらに、次に示すように、LineProcessor
の具象クラスを作成せず、匿名クラスを生成することも可能である。
class Example { public static void main(String[] args) { FileProcessor.readFile("sample.txt", new LineProcessor() { @Override public void doProcessLine(String line) { System.out.println(line); } }); } }
Template Methodの実装(Java8)
さてここからは、Java8におけるTemplate Methodの実装を見ていこう。
Java8では、インタフェースにデフォルトメソッドを定義できるようになった。
この仕組みを利用することで、インタフェースだけを使ってTemplate Methodパターンを実装できる。
// テンプレートインタフェース interface FileProcessor { // テンプレートメソッド(default指定) public default void readFile(String fileName){ try (BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream(fileName)))) { String line; while ((line = reader.readLine()) != null) { doProcessLine(line); // フックメソッドの呼び出し } } catch (IOException e) { throw new RuntimeException("error", e); } } // フックメソッド(具象クラスに実装を任せる) public void doProcessLine(String line); } //具象クラス class SimpleFileProcessor implements FileProcessor { //フックメソッドの実装 @Override public void doProcessLine(String line) { System.out.println(line); } } class Example { public static void main(String[] args) { new SimpleFileProcessor().readFile("sample.txt"); } }
Java7のコードでは、doProcessLine
メソッドの可視性はprotected
だったが、インタフェースにしたためにpublic
に変更する必要が出ている。
なお、Java8のデフォルトメソッドは状態を持てないため、テンプレートクラスが変数を持つ場合にはこの仕組みを使えない。
関数型インタフェースの利用
最後に、上記のFileProcessor
が持つフックメソッドは1つだけなので、関数型インタフェースにしてみよう。
@FunctionalInterface interface FileProcessor { // テンプレートメソッド(default指定) public default void readFile(String fileName){ try (BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream(fileName)))) { String line; while ((line = reader.readLine()) != null) { doProcessLine(line); // フックメソッドの呼び出し } } catch (IOException e) { throw new RuntimeException("error", e); } } // フックメソッド(具象クラスに実装を任せる) public void doProcessLine(String line); }
利用する側のコードは次のようになる。
class Example { public static void main(String[] args) { // 右辺のラムダ式は System.err::println; と書くことも可能 FileProcessor processor = line -> System.err.println(line); processor.readFile("sample.txt"); } }
なんだか不思議なコードである。
そう感じる理由の1つは、関数型インタフェースのFileProcessor
オブジェクトに与えるラムダ式が、ファイル読み込み処理の全体ではなく、1行分だけを解析する内部ロジックだからだろう。FileProcessor
オブジェクトに対して呼び出すreadFile
メソッドが、裏に隠れた存在とも言えるデフォルトメソッドであることもなんだか不自然に思える。
そもそも、Template Methodパターンにおける主役は、テンプレートとなるメソッドであり、差し替え可能なフックメソッドは裏方的な存在だ。
このため、Template Methodのテンプレートにしたインタフェースと、@FunctionalInterface
アノテーションの相性はあまり良くないのだろう。
[高橋 友樹] [前多 賢太郎]