PG日誌

受託系 PG が C# の事を書いています

C#でTemplate Methodパターンを実装する

GoFのデザインパターンの中でも、振る舞いに関するパターンの1つであるTemplate MethodパターンをC#で実装したいと思います。

Template Methodパターンとは?

Template Methodパターンとは、処理の手順を基底クラスで決めておいて、具体的な動作を後から派生クラスで実装するパターンです。Webサーバー等で利用されているケースがあり、サーバーのロジックはたいていは決まっていますが、オプションの設定や、HTTPリクエストの処理にユーザーコードを挟む場合などに一昔前はこういったパターンがよくあッ田かと思います。

C#で実装する

普通に実装すると以下のようになります。

基底クラスに、Sequence_1(),2(),3()メソッドと、エラーが起きた時の処理を行うErrorHandleメソッドを抽象クラスで定義しておきます。

で、それらを順番に(一定の規則で)実行するRunメソッドを用意します。

// 基底クラス
public abstract class TemplateMethod
{
    protected abstract void Sequence_1();
    protected abstract void Sequence_2();
    protected abstract void Sequence_3();
    protected abstract void ErrorHandle(Exception ex);

    public void Run()
    {
        try
        {
            this.Sequence_1();
            this.Sequence_2();
            this.Sequence_3();
        }
        catch (Exception ex)
        {
            this.ErrorHandle(ex);
        }
    }
}

で、派生クラス側で抽象メソッドに具体的な動作を記述します。

// 派生クラス
public class DerivedClass : TemplateMethod
{
    protected override void Sequence_1()
    {
        Console.WriteLine("Sequence_1を実行しました。");
    }

    protected override void Sequence_2()
    {
        Console.WriteLine("Sequence_2を実行しました。");
    }

    protected override void Sequence_3()
    {
        //Console.WriteLine("Sequence_3を実行しました。");
        throw new FileNotFoundException("Sequence_3でエラーが発生しました。");
    }

    protected override void ErrorHandle(Exception ex)
    {
        Console.WriteLine("エラーが発生しました。");
        Console.WriteLine(ex.ToString());
    }
}

使用方法は以下の通りでメインメソッドで、基底クラスの変数で派生クラスのインスタンスを受けて、Runを呼び出します。

// 利用側コード
public static void Main(string[] args)
{
    TemplateMethod template = new DerivedClass();
    template.Run();
}

// 実行結果
> Sequence_1を実行しました。
> Sequence_2を実行しました。
> エラーが発生しました。
> System.IO.FileNotFoundException: Sequence_3でエラーが発生しました。
>    場所 ...(省略)...
>    場所 ...(省略)...

ラムダ式で作り直す

ただ、テンプレートメソッドは派生クラスを作成する関係上、クラスが増加する欠点があります。フレーワークのような他人に提供するような、いわゆる「固い処理」は確実に決まっておりクラス化したほうが良いですが、ユーザーコード上で気軽に使うならラムダ式でも似たように実装できます。

以下、最初の例と同じ内容をラムダ式にします。

// 利用側コード
public static void Main(string[] args)
{
    RunTemplate(
        () => { Console.WriteLine("Sequence_1を実行しました。"); },
        () => { Console.WriteLine("Sequence_2を実行しました。"); },
        () =>
        {
            Console.WriteLine("Sequence_3を実行しました。");
            throw new FileNotFoundException("Sequence_3でエラーが発生しました。");
        },
        ex =>
        {
            Console.WriteLine("エラーが発生しました。");
            Console.WriteLine(ex.ToString());
        }
    );
}

// テンプレートメソッド側
public static void RunTemplate(Action sequence_1,
    Action sequence_2, Action sequence_3, Action<Exception> error_handle)
{
    try
    {
        sequence_1?.Invoke();
        sequence_2?.Invoke();
        sequence_3?.Invoke();
    }
    catch (Exception ex)
    {
        error_handle?.Invoke(ex);
    }
}

引数が若干長くなってしまいましたが、これはラムダ版の一つの欠点かもしれません。

最後に

基底クラスを継承する必要があって、基底クラスにprotected virtualなメソッドがあって動きをoverrideできるものがあり、そういったメソッドは基底クラス内の他メソッドから呼ばれるけど、自分からは呼び出すことができません。これもある意味TTemplate Methodパターンと言えるかもしれません。

最近はリフレクションはラムダ、アトリビュートが発達してきていてTemplate Methodパターンの出番は以前より減っていますが、第三者へ基本処理を提供する時には依然として有効だと思います。