エンタープライズギークス (Enterprise Geeks)

企業システムの企画・開発に携わる技術者集団のブログです。開発言語やフレームワークなどアプリケーション開発に関する各種情報を発信しています。ウルシステムズのエンジニア有志が運営しています。

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アノテーションの相性はあまり良くないのだろう。

[高橋 友樹] [前多 賢太郎]