C#でGoFのデザインパターン~Decoratorパターン編~
2019-11-01
azblob://2022/11/11/eyecatch/2019-11-01-csharp-gof-decoratorpattern-000.jpg

はじめに

GoFのデザインパターンについて解説し始めて早5本目です。
全部で23個のデザインパターンがあるので、これまででその4分の1までやってきたことになります。
デザインパターンを学ぶにあたって、書籍や解説を読んで概念を知るだけでは理解できないこともあります。
実際にコードを書きながら、そのパターンがどのように実装されるかを知ることで学習の助けになればと思います。

Decoratorパターンとは

Decoratorという言葉は、「飾る」という意味のDecorateから来ています。
例えば、あるクラスで行う処理に対して、いくつかの追加の処理をしたい場合があるとして、その処理をどのように追加していけばいいでしょうか?
そういったニーズに応えるのがDecoratorパターンになります。
Decoratorパターンでは、装飾されるクラスに次々に機能を装飾していくことによって、処理を追加していくことができます。
・・・言葉だけ聞いてもわからないと思うので、実装しながら解説していきましょう。

れっつ実装

いつものようにコンソールアプリとしてプロジェクトを作成したら、まずは機能を装飾される側の抽象クラスを作成していきましょう。
実際の装飾される側のクラスは、これを継承して作ることでデフォルトの実装を引き継いで他のメソッドを持たせることもできます。
Descriptionフィールドがprotectedになっていますが、これはこのクラスを継承したクラスや抽象クラスでもこのフィールドを利用するためです。
一方で、外部からはアクセスさせないのでinternalやpublicにはしません。

abstract class AbstractDecoratee
{
    protected string Description;
    public virtual string GetDescription()
    {
        return Description;
    }
}

次に、機能を装飾する側のDecoratorクラスを実装します。
こちらでは、装飾される側で持っているフィールドを利用するため、AbstractDecorateeを継承しています。
また、こちらでは新たにGetDescriptionメソッドを実装するために、AbstractDecorateeのGetDescriptionをオーバーライドして抽象メソッドとしています。
Decoratorパターンの肝はこのクラスであるといえます。
このクラスの中にはAbstractDecorateeを継承したクラスのインスタンスを格納するフィールドがあり、コンストラクターでそれを代入しています。
これによって、この抽象クラスを継承したクラスの中でインスタンス化時に受け取ったクラスに対して昨日の装飾を行えるようになります。

abstract class AbstractDecorator : AbstractDecoratee
{
    protected AbstractDecoratee _decoratee;
    public AbstractDecorator(AbstractDecoratee decoratee)
    {
        _decoratee = decoratee;
    }
    public abstract override string GetDescription();
}

ここまで出来たら抽象の部分の実装は完了です。
次は具体的なクラスの実装をしていきます。
Decorateeクラスは装飾される側のクラスの派生クラスで、AbstractDecorateeで実装したフィールドとメソッドを継承しています。
なので、ここでDescriptionを定義し直さなくてもコンストラクターでDescription変数を呼び出すことができます。
ここでは、クラスの説明として「Decoratee」を代入しています。

class Decoratee : AbstractDecoratee
{
    public Decoratee()
    {
        Description = "Decoratee";
    }
}

今度は、装飾を行う側の派生クラスの実装を行っていきます。
こちらではAbstractDecoratorから派生し、規定されたGetDescriptionメソッドを実装していきます。
ここでは、コンストラクターで受け取ってきた装飾対象のクラスのGetDescriptionメソッドを呼び出し、出力される文字列の後に装飾されたしるしであるdecoratedという文字列を加えて送り返します。

class Decorator : AbstractDecorator
{
    public Decorator(AbstractDecoratee decoratee) : base(decoratee) {}
    public override string GetDescription()
    {
        Console.WriteLine("Decorateされました");
        return _decoratee.GetDescription() + "、decorated";
    }
}

ここまででDecoratorパターンの実装は完了です。
最後に、このパターンを呼び出し元であるクライアント側でどのように呼び出すかを見てみましょう。
3行目で、最初に作ったクラスを引数にとってDecoratorクラスをインスタンス化しています。
ここで、Decoratorクラスの_decoratorにDecorateeクラスが代入されています。
なので、4行目でGetDescription()を読んだとき、まずDecoratorクラス内の_decoratee.GetDescription()の部分でDecorateeのGetDescription()から"Decoratee"という文字列が返ってきます。
これに加えて、Decoratorクラス内で"、decorated"という文字列が付け加えられるので、最終出力は"Decoratee、decorated"となります。
同様に、そのDecorateeをさらに引数にとって新しいインスタンスを作って代入してGetDescriptionメソッドを呼ぶと、Decoratee,Decorator,Decoratorの順で処理が加えられていくので、さらに文字列を加えていくことができます。
今回は文字列でしたが、他の型のものでも同様に実装できるので、この形の実装によって型の中身を加工する機能を次々に追加していくことができるようになります。

static void Main(string[] args)
{
    AbstractDecoratee Decoratee = new Decoratee();
    Console.WriteLine(Decoratee.GetDescription());
    Decoratee = new Decorator(Decoratee);
    Console.WriteLine(Decoratee.GetDescription());
    Decoratee = new Decorator(Decoratee);
    Console.WriteLine(Decoratee.GetDescription());
}

ここまで作成したコードを走らせてみて、以下のような結果が出力されれば成功です。
最初の状態だと「Decoratee」という文字列しか出力されないDescriptionが、Decoratorによって文字列を追加されていく様子が確認できると思います。

Decoratee
Decorateされました
Decoratee、decorated
Decorateされました
Decorateされました
Decoratee、decorated、decorated

おわりに

今回はDecoratorパターンについて解説して実装してきました。
Decoratorパターンは、装飾されるオブジェクトに同じ抽象クラスを継承したクラスを用いることで次々に責務を付与していきます。
このことは機能拡張に対して柔軟に対応できるようにしてくれるという点において大きな意味を持っているのですが、気になる方はSOLID原則、解放・閉鎖の原則といった言葉についても調べてみてください。
デザインパターンの学習を通して、保守性、拡張性の高いコードの書き方についても知識を深めてみませんか?