テンプレートメソッドパターンは、オブジェクト指向における振る舞い型デザインパターンの一つです。このパターンの主な目的は、アルゴリズムの全体構造をスーパークラスで定義しつつ、特定のステップの詳細実装をサブクラスに委ねることにあります。これにより、コードの重複を排除し、アルゴリズムの構造を一箇所で管理できるようになります。
課題:コードの重複と保守性の低下
例として、異なるフォーマットのデータを処理するシナリオを考えてみましょう。CSVファイルとJSONファイルを処理するクラスがあり、それぞれの手順は以下の通りであるとします。
- データソースへの接続(共通)
- データの読み込みと解析(フォーマット依存)
- データの変換(フォーマット依存)
- 結果の保存(共通)
テンプレートメソッドパターンを使用せずに実装した場合、以下のようにクラスごとに共通のロジックが重複してしまいます。
public class CsvDataHandler {
public void executeProcess() {
connectToSource();
readAndParseCsv();
transformData();
saveToTarget();
System.out.println("CSV処理完了");
}
private void connectToSource() {
System.out.println("データソースに接続...");
}
private void readAndParseCsv() {
System.out.println("CSVデータを読み込み・解析中...");
}
private void transformData() {
System.out.println("データをフォーマット変換中...");
}
private void saveToTarget() {
System.out.println("データを保存...");
}
}
public class JsonDataHandler {
public void executeProcess() {
connectToSource();
readAndParseJson();
transformData();
saveToTarget();
System.out.println("JSON処理完了");
}
private void connectToSource() {
System.out.println("データソースに接続...");
}
private void readAndParseJson() {
System.out.println("JSONデータを読み込み・解析中...");
}
private void transformData() {
System.out.println("データをフォーマット変換中...");
}
private void saveToTarget() {
System.out.println("データを保存...");
}
}
この実装では、接続処理や保存処理などの共通ロジックが各クラスにコピーされています。もし接続方法が変更になった場合、すべてのクラスを修正する必要があり、保守性が著しく低下します。
解決策:テンプレートメソッドパターンの適用
テンプレートメソッドパターンを適用すると、アルゴリズムの骨格(テンプレート)を抽象クラスに定義し、変化する部分だけを抽象メソッド(またはフックメソッド)としてサブクラスに実装させることができます。
以下に、共通の処理フローを定義した抽象クラスDataProcessorと、それを継承した具体的な実装クラスを示します。
public abstract class DataProcessor {
// テンプレートメソッド:アルゴリズムの骨格を定義。finalでオーバーライド禁止。
public final void executeProcess() {
connectToSource();
readAndParseData();
transformData();
saveToTarget();
System.out.println(getProcessName() + "処理完了");
}
private void connectToSource() {
System.out.println("データソースに接続...");
}
// サブクラスに実装を強制する抽象メソッド
protected abstract void readAndParseData();
protected abstract void transformData();
private void saveToTarget() {
System.out.println("データを保存...");
}
// サブクラスがオーバーライド可能なメソッド(フック)
protected String getProcessName() {
return "データ";
}
}
続いて、具体的な処理クラスです。共通部分は削除され、各フォーマット固有のロジックのみを実装しています。
public class CsvDataHandler extends DataProcessor {
@Override
protected void readAndParseData() {
System.out.println("CSVデータを読み込み・解析中...");
}
@Override
protected void transformData() {
System.out.println("CSV形式に合わせてデータ変換中...");
}
@Override
protected String getProcessName() {
return "CSV";
}
}
public class JsonDataHandler extends DataProcessor {
@Override
protected void readAndParseData() {
System.out.println("JSONデータを読み込み・解析中...");
}
@Override
protected void transformData() {
System.out.println("JSON形式に合わせてデータ変換中...");
}
}
public class Main {
public static void main(String[] args) {
DataProcessor csvProcessor = new CsvDataHandler();
csvProcessor.executeProcess();
DataProcessor jsonProcessor = new JsonDataHandler();
jsonProcessor.executeProcess();
}
}
この構造により、connectToSourceやsaveToTargetのような共通処理は一箇所に集約され、readAndParseDataなどの多様性が必要な部分のみをサブクラスで記述するようになります。また、executeProcessメソッドをfinalにすることで、サブクラスがアルゴリズムの全体的な流れを意図的に変更してしまうリスクを防いでいます。
フックメソッドの役割
上記の例におけるgetProcessName()は、フックメソッドの一例です。フックメソッドは、抽象クラスにおいてデフォルトの動作を提供しますが、サブクラスが必要に応じてこれをオーバーライドして振る舞いを変更できるようにするためのものです。これにより、サブクラスに過度な実装強制を行わず、柔軟性を高めることができます。
Java標準ライブラリにおける使用例
Javaの標準ライブラリにも、テンプレートメソッドパターンは広く使われています。代表的な例がjava.lang.Threadクラスです。
スレッドを作成する際、開発者は通常run()メソッドをオーバーライドして具体的な処理を記述します。しかし、スレッドの起動はstart()メソッドを呼び出すことで行われます。start()メソッドがスレッドの生成やOSリソースの割り当てといった複雑な手順(アルゴリズムの骨格)を管理し、その過程でrun()メソッド(具体的なステップ)を呼び出す仕組みになっています。
この仕組みにより、開発者はスレッドのライフサイクル管理という複雑な詳細を知らなくても、run()の中身だけに集中できます。
テンプレートメソッドパターンの利点と欠点
利点:
- 共通するコードをスーパークラスに集約できるため、重複が排除され保守性が向上します(DRYの原則)。
- アルゴリズムの構造を変更せずに、サブクラスでの拡張が容易になります(開閉原則)。
- 処理の流れがスーパークラスで一元管理されるため、バグの特定やロジックの追跡が容易になります。
欠点:
- サブクラスの数が増加すると、クラス階層が深くなり、システム全体の複雑度が上がる可能性があります。
- 親クラスの実装内容を知らないと、サブクラスの挙動を完全に理解できない場合があります(カプセル化の逆転)。
- 継承を利用しているため、コンポジション(委譲)に比べて柔軟性が制限されることがあります。