datchの日記

気がついたら社会人。気になる技術的なことについて少しずつ書いていけたらと思っております。

【デザインパターン】デコレータパターン

どうも、看病した次の日に見事に風邪が感染っていて、大学から帰ったら激しい悪寒に襲われて次の日は一日中寝ていました。
彼岸なので実家に帰ったりで土日になかなか時間が取れませんし、後輩に向けて開く勉強会に向けての学習も進捗がかなり遅れているので、そろそろヤバメですね。

さて、それでは今週は前から引き続き、デザインパターンの【Decorator】についての記事を書いていきます。

参考書



さらっと読める「OREILLY Head First デザインパターン」を参考にしながら書いています。
しばらくはこいつを参考にデザインパターンについて書いていくよ!

Decoratorパターンって何ですか?



基板となるソースコード全くの変更を加えることなく、新しい機能を加える事が出来るデザインパターンです。
プログラム上ではオプション的な処理として、「この処理はしたいけど、あれは意図的に呼び出された時に機能して欲しい」といった機能に適しています。

恒例のWikipedia大先生も載せておきます 【Decorator パターン - Wikipedia

Decorator パターンの方針は、既存のオブジェクトを新しい Decorator オブジェクトでラップすることである。 その方法として、Decorator のコンストラクタの引数でラップ対象の Component オブジェクトを読み込み、コンストラクタの内部でそのオブジェクトをメンバに設定することが一般的である。
Decorator パターンは、既存のクラスを拡張する際にクラスの継承の代替手段として用いられる。継承がコンパイル時に機能を拡張するのに対し、Decorator パターンはプログラムの実行時に機能追加をする点が異なる。

身近で簡単な使用例



参考書のケースを参考にして説明を行っていきたいと思います。
参考書では「Decorator」というくらいなので「飲み物のトッピングによるデコレーション」を使ってこのデザインパターンを説明しています。

とあるコーヒーショップのシステム

急成長中のとあるコーヒーショップの注文システムを更新しています。
事業を始めた当初は4種類の飲み物を扱っており、トッピングを扱ってはいませんでした。
その為、当初は以下のように継承で料金だけそれぞれの飲み物に対して実装を行って対応していました。

// 返り値の型は省略
class Beverage
{
    private string description;

    public getDiscription();
    public abstract cost();
}

// Extends
class HouseBlend extends Beverage
{
    public cost();
}

class DarkRoast extends Beverage
{
    public cost();
}

class Espresso extends Beverage
{
    public cost();
}

class Decaf extends Beverage
{
    public cost();
}

しかし、これからはコーヒーに加えて、スチームミルク、豆乳、モカ(チョコレート)などのトッピングを扱うことになりました。
これでは全てのコーヒーとトッピングの組み合わせのクラスを作る必要が出てきてしまいます。

新しいシステムの定義

そこでこのようなクラスを定義して問題に対応しました。

// 返り値の型は省略
class Beverage
{
    private string description;
    private bool milk;
    private bool soy;
    private bool mocha;
    private bool whip;
    
    public getDescription();
    public abstract cost();

    public hasMilk();
    public setMilk();
    public hasSoy();
    ::: // 同じように定義
}

これによりトッピングが追加されても基底クラスのcost()内で計算し、オーバーライドした関数がそれを受け取って計算することで対応することが出来るようになりました。

しかしこれって?

これでトッピングに対する問題は解決したかのように見えます。
しかし、このような事態が発生した場合はどうなるのでしょうか?

  1. トッピングの値段が変わったら基底クラスのコードを変更する必要がある(影響範囲が広い)
  2. 新しいトッピングが追加されても同様
  3. 新しい飲み物が現れた時、特定のトッピングを受け付けないという要件が出た場合でもこの基底クラスを継承して関係のないメソッドを継承してしまう
  4. もしダブルモカ、ダブルホイップが欲しいといった場合

といった状況に果たして対応出来るのだろうか?

これらの問題を解決するのが今回紹介するDecoratorパターンだ。

Decoratorパターンの書き方



それでは、先ほどの問題に似た形でコーヒーのトッピングを例にして書いていこう。
まずはベースのコーヒーがあり、これに対して以下のトッピングを選べるとしよう。

  1. モカ(チョコレート)
  2. スチームミルク
  3. ホイップクリーム
  4. アイスクリーム

これに対して以下の様なクラスを用意する。

class Coffee
{
    public cost(){return 1.5;}
}

// 抽象Decorator
class CondimentDecorator extends Coffee
{
}

// 具象Decorator
class Mocha extends ComdimentDecorator
{
    Coffee coffee;
    public Mocha(Coffee coffee){this.coffee = coffee;}
    public cost(){return .2 + coffee.cost();}
}

class Milk extends ComdimentDecorator
{
    Coffee coffee;
    public Milk(Coffee coffee){this.coffee = coffee;}
    public cost(){return .5 + coffee.cost();}
}

class Whip extends ComdimentDecorator
{
    Coffee coffee;
    public Whip(Coffee coffee){this.coffee = coffee;}
    public cost(){return .8 + coffee.cost();}
}

class Ice extends ComdimentDecorator
{
    Coffee coffee;
    public Ice(Coffee coffee){this.coffee = coffee;}
    public cost(){return 1.15 + coffee.cost();}
}

そして、以下の様に実行をすることで様々なトッピングに対応することが出来る。

// ダブルモカのアイスクリーム乗っけ!
Coffee c1 = new Coffee();
c1 = new Mocha(c1);
c1 = new Mocha(c1);
c1 = new Ice(c1);
c1.cost(); // 3.05
// 以下でも同じ
// Coffee c1 = new Ice(new Mocha(new Mocha(new Coffee())));

// ホイップクリームにアイスクリームを乗せて、モカも追加して!それと、スチームミルクも欲しい!
Coffee c2 = new Coffee();
c2 = new Whip(c2);
c2 = new Ice(c2);
c2 = new Mocha(c2);
c2 = new Milk(c2);
c2.cost(); // 4.15

不思議な事に追加したい機能の中に基底クラスを引数に取るだけでどんどんトッピングが追加されていきますね。
このようにDecoratorパターンはコンストラクタ引数に基底クラスを取り、それを再帰的に呼び出し続けることで機能しています。

JavaのInputStream,OutputStreamもこのDecoratorパターンを採用しています。

最後に



最後まで記事を呼んでいただきありがとございました。
徐々にデザインパターンにも慣れてきた感じがありますね。
目指せ、デザインパターンマスター!