本記事ではGoFのデザインパターンのプログラムの構造に関するパターンの一つである「Decorator」パターンを解説します。このパターンを一言で説明するならば、「元となるオブジェクトに装飾(デコレート)を行うことで機能を拡張させる方法」と言えるでしょう。「Decorator」パターンを利用しない場合と、利用する場合の両方の例を挙げてわかりやすく解説します。
「Decorator」パターンとは
先述した通り、「Decorator」とは「元となるオブジェクトに装飾(デコレート)を行うことで機能を拡張させる方法」です。「Decorator」を直訳すると「装飾者」という意味があります。
「Decorator」パターンは元となるオブジェクトをラップするように機能を追加していきます。元となるオブジェクトには一切変更を加えることなく、機能を追加していくことが可能です。
「Decorator」パターンの書き方
この記事では、コーヒーを注文する時の例で考えてみます。コーヒーといっても、いろいろな種類があります。またそのコーヒーにはミルクを追加したり、砂糖を追加したりとさまざまなアレンジを加えることができます。そのアレンジによっては料金が変更される可能性もあります。このような事象をコード上で表現してみます。
✖︎ Decoratorパターンを使用しない例 ✖︎
まずはDecoratorパターンを使用しない例です。シンプルなコーヒーからクラスにしていきます。このSimpleCoffeeクラスにはこのコーヒーがどんなコーヒーかを取得するgetDescription()と価格を取得するgetPrice()のメソッドを用意します。
// SimpleCoffee
public class SimpleCoffee {
private static final String DESCRIPTION = "SimpleCoffee";
private static final int PRICE = 400;
public String getDescription() {
return DESCRIPTION;
}
public int getPrice() {
return PRICE;
}
}
このコーヒーを提供していると、ミルクを追加して欲しいとの要望が出たため、ミルクを追加したコーヒーのクラス(SimpleCoffeeWithMilk)を作成しました。普通のコーヒーにミルクを加えるので、SimpleCoffeeWithMilkクラスのフィールドにはSimpleCoffeeのオブジェクトを持っています。それぞれのメソッドもミルクを追加した説明と価格になるように変更しています。
// SimpleCoffee+Milk
public class SimpleCoffeeWithMilk {
protected SimpleCoffee simpleCoffee;
private static final String DESCRIPTION = "+Milk";
private static final int PRICE = 80;
public SimpleCoffeeWithMilk(SimpleCoffee simpleCoffee) {
this.simpleCoffee = simpleCoffee;
}
public String getDescription() {
return simpleCoffee.getDescription() + DESCRIPTION;
}
public int getPrice() {
return simpleCoffee.getPrice() + PRICE;
}
}
ここで普通のコーヒーに砂糖のみを追加して欲しいとの要望がでたため、SimpleCoffeeWithSyrupクラスを作りました。ミルク追加時と同様にgetDescription()とgetPrice()をシロップを追加した説明と料金に変更します。
// SimpleCoffee+Syrup
public class SimpleCoffeeWithSyrup {
protected SimpleCoffee simpleCoffee;
private static final String DESCRIPTION = "+Syrup";
private static final int PRICE = 50;
public SimpleCoffeeWithSyrup(SimpleCoffee simpleCoffee) {
this.simpleCoffee = simpleCoffee;
}
public String getDescription() {
return simpleCoffee.getDescription() + DESCRIPTION;
}
public int getPrice() {
return simpleCoffee.getPrice() + PRICE;
}
}
これで一件落着ですね。と、思いきやミルクも砂糖も追加して欲しいとの要望が出ました。また新たにクラスを追加する必要があります。SimpleCoffeeWithSyrupAndMilkクラスを作成します。コーヒーにミルクとシロップを追加した説明と料金を返却するようにメソッドを作成します。
// SimpleCoffee+Syrup+Milk
public class SimpleCoffeeWithSyrupAndMilk {
protected SimpleCoffee simpleCoffee;
private static final String DESCRIPTION = "+Syrup+Milk";
private static final int PRICE = 130;
public SimpleCoffeeWithSyrupAndMilk(SimpleCoffee simpleCoffee) {
this.simpleCoffee = simpleCoffee;
}
public String getDescription() {
return simpleCoffee.getDescription() + DESCRIPTION;
}
public int getPrice() {
return simpleCoffee.getPrice() + PRICE;
}
}
上記のクラスを使ってそれぞれのコーヒーオブジェクトを生成して出力してみます。それぞれのコーヒーの情報を表示するshowCoffe()のメソッド用意しています。
ublic class Main {
public static void main(String[] args) {
// SimpleCoffeeベース
SimpleCoffee simpleCoffee = new SimpleCoffee();
showCoffe(simpleCoffee.getDescription(), simpleCoffee.getPrice());
SimpleCoffeeWithSyrup simpleCoffeeWithSyrup = new SimpleCoffeeWithSyrup(new SimpleCoffee());
showCoffe(simpleCoffeeWithSyrup.getDescription(), simpleCoffeeWithSyrup.getPrice());
SimpleCoffeeWithMilk simpleCoffeeWithMilk = new SimpleCoffeeWithMilk(new SimpleCoffee());
showCoffe(simpleCoffeeWithMilk.getDescription(), simpleCoffeeWithMilk.getPrice());
SimpleCoffeeWithSyrupAndMilk simpleCoffeeWithSyrupAndMilk = new SimpleCoffeeWithSyrupAndMilk(new SimpleCoffee());
showCoffe(simpleCoffeeWithSyrupAndMilk.getDescription(), simpleCoffeeWithSyrupAndMilk.getPrice());
// AmericanCoffeeベース
AmericanCoffee americanCoffee = new AmericanCoffee();
showCoffe(americanCoffee.getDescription(), americanCoffee.getPrice());
AmericaCoffeeWithSyrup americaCoffeeWithSyrup = new AmericaCoffeeWithSyrup(new AmericanCoffee());
showCoffe(americaCoffeeWithSyrup.getDescription(), americaCoffeeWithSyrup.getPrice());
AmericanCoffeeWithMilk americanCoffeeWithMilk = new AmericanCoffeeWithMilk(new AmericanCoffee());
showCoffe(americanCoffeeWithMilk.getDescription(), americanCoffeeWithMilk.getPrice());
AmericanCoffeeWithSyrupAndMilk americanCoffeeWithSyrupAndMilk = new AmericanCoffeeWithSyrupAndMilk(new AmericanCoffee());
showCoffe(americanCoffeeWithSyrupAndMilk.getDescription(), americanCoffeeWithSyrupAndMilk.getPrice());
}
// コーヒーの詳細を表示するだけの静的メソッド
public static void showCoffe(String description, int price) {
System.out.println("コーヒの種類: " + description + ", 価格: " + price);
}
}
>> 実行結果
コーヒの種類: SimpleCoffee, 価格: 400
コーヒの種類: SimpleCoffee+Syrup, 価格: 450
コーヒの種類: SimpleCoffee+Milk, 価格: 480
コーヒの種類: SimpleCoffee+Milk+Syrup, 価格: 530
コーヒの種類: AmericanCoffee, 価格: 350
コーヒの種類: AmericanCoffee+Syrup, 価格: 400
コーヒの種類: AmericanCoffee+Milk, 価格: 430
コーヒの種類: AmericanCoffee+Syrup+Milk, 価格: 480
これで全てのお客様の要望に応えられそうです。しかし新たな要望がきます。ミルクではなく豆乳を入れて欲しいとのことです。豆乳を追加するには新しくクラスをつくって、ミルクやシロップが入ったバージョンのクラスも作って、、、どんどんクラスが増えていってしまっています。またコーヒーにはエスプレッソコーヒー等もあり、元となる基本のオブジェクトを追加する必要も出てくるため、その分オプションを設定できクラスの数が膨大になります。
これでは新しい要望が出るたびにクラスを作る必要があり、1個のオプション追加だけならクラスは2個、2個のオプション追加ならクラスは4個、3個のオプション追加ならクラスは8個と2のn乗でクラスを追加していく必要が出てきます。そんなの無理です。めんどくさすぎます。
またシロップとミルクの価格と説明は、それぞれのコーヒーオブジェクトで各々持っています。これでは価格の変更や説明の変更があった場合に、全ての出現箇所を修正する必要があります。一つでも価格を間違えると、そのコーヒーを頼んだ時にだけ価格が違い、事故の原因になってしまいます。必ず1箇所で価格と説明は保管しておきたいです。
このような場合にDecoratorパターンが威力を発揮します。次はDecoratorパターンを適用してコーヒーのクラスを作ってみます。
⭕️ Decoratorパターンを使用する例 ⭕️
まず、どのコーヒーにもgetDescription()とgetPrice()のメソッドはあるので、コーヒーのインターフェースを作成します。そのCoffeeインターフェースを実装するようにSimpleCofee()クラスを作成します。こうすることで、全てのコーヒーを同じオブジェクトとして扱うことが可能です。
// コーヒーのインターフェース
public interface Coffee {
String getDescription();
int getPrice();
}
// SimpleCofee
public class SimpleCofee implements Coffee {
private static final String DESCRIPTION = "SimpleCoffee";
private static final int PRICE = 400;
@Override
public String getDescription() {
return DESCRIPTION;
}
@Override
public int getPrice() {
return PRICE;
}
}
これで、普通のコーヒーのオブジェクトは作成できるようになりました。仮にアメリカンコーヒーのクラスを作るとすれば、同じようにCoffeクラスを実装するようにすれば簡単にアメリカンコーヒークラスが作成できます。
次にコーヒーにオプションを追加するためのDecoratorクラスを作成します。このクラスはコーヒーのクラスをラップするように作成します。そのためCoffeクラスのインターフェースを実装するようにDecoratorクラスを作成します。
// Decoratorの抽象クラス
public abstract class CoffeeDecorator implements Coffee {
protected Coffee decoratedCoffee;
public CoffeeDecorator(Coffee decoratedCoffee) {
this.decoratedCoffee = decoratedCoffee;
}
@Override
public String getDescription() {
return decoratedCoffee.getDescription();
}
@Override
public int getPrice() {
return decoratedCoffee.getPrice();
}
}
このクラスは抽象クラスとして作り、このクラスを継承して各オプションのクラスを作成します。このクラスはフィールドにCoffeeのオブジェクトを持つことで、装飾するコーヒーをコンストラクタに設定して装飾元のコーヒーをラップしています。
次に上記のCoffeeDecoratorを継承してMilkDecoratorクラスとSyrupDecoratorクラスを作成します。
// ミルク追加用のDecorator
public class MilkDecorator extends CoffeeDecorator {
private static final String DESCRIPTION = "+Milk";
private static final int PRICE = 80;
public MilkDecorator(Coffee decoratedCoffee) {
super(decoratedCoffee);
}
@Override
public String getDescription() {
return super.getDescription() + DESCRIPTION;
}
@Override
public int getPrice() {
return super.getPrice() + PRICE;
}
}
// シロップ追加用のDecorator
public class SyrupDecorator extends CoffeeDecorator {
private static final String DESCRIPTION = "+Syrup";
private static final int PRICE = 50;
public SyrupDecorator(Coffee decoratedCoffee) {
super(decoratedCoffee);
}
@Override
public String getDescription() {
return super.getDescription() + DESCRIPTION;
}
@Override
public int getPrice() {
return super.getPrice() + PRICE;
}
}
これでDecoratorの実装が完了しました。これらのクラスのフィールドにはCoffeeのオブジェクトを持っているため、CoffeeオブジェクトをDecoratorでラップして、さらにそのオブジェクトを別のDecoratorでラップすることが可能になります。
これらのクラスを利用して、それぞれのコーヒーオブジェクトを生成してみましょう。アメリカンコーヒーのクラスもSimpleCofeeクラスと同様にCoffeeインターフェースを実装して作ってあります。
public class Main {
public static void main(String[] args) {
// SimpleCoffeeベース
Coffee simpleCofee = new SimpleCofee();
showCoffe(simpleCofee);
Coffee simpleCoffeeWithSyrup = new SyrupDecorator(new SimpleCofee());
showCoffe(simpleCoffeeWithSyrup);
Coffee simpleCoffeeWithMilk = new MilkDecorator(new SimpleCofee());
showCoffe(simpleCoffeeWithMilk);
Coffee simpleCoffeeWithSyrupAndMilk = new MilkDecorator(new SyrupDecorator(new SimpleCofee()));
showCoffe(simpleCoffeeWithSyrupAndMilk);
// AmericanCofeeベース
Coffee americanCofee = new AmericanCoffee();
showCoffe(americanCofee);
Coffee americanCoffeeWithSyrup = new SyrupDecorator(new AmericanCoffee());
showCoffe(americanCoffeeWithSyrup);
Coffee americanCoffeeWithMilk = new MilkDecorator(new AmericanCoffee());
showCoffe(americanCoffeeWithMilk);
Coffee americanCoffeeWithSyrupAndMilk = new MilkDecorator(new SyrupDecorator(new AmericanCoffee()));
showCoffe(americanCoffeeWithSyrupAndMilk);
}
// コーヒーの詳細を表示するだけの静的メソッド
public static void showCoffe(Coffee coffee) {
System.out.println("コーヒの種類: " + coffee.getDescription() + ", 価格: " + coffee.getPrice());
}
}
実行結果はDecoratorパターンを使用しない例 と同様の結果になります。ここで注目して欲しいのが「new MilkDecorator(new SyrupDecorator(new SimpleCofee()))」です。SimpleCoffeeをSyrupDecoratorでラップして、さらにMilkDecoratorでラップするようにオブジェクトを生成しています。
全てのオブジェクトの型はCoffee型のため、わざわざオプションごとのクラスを作成する必要がなく、すべてのオブジェクトを同一の型として扱うことができるため型の判定が必要ありません。showCoffe()メソッドも型が全て同じCoffee型なので、引数もそのままCoffeeオブジェクトを渡すだけですべてのコーヒーで同じ処理を実施することが可能になります。
さらにオプションがどれだけ増えても、追加するクラスはDecoratorを継承したクラスをオプションの数だけ追加すれば済みます。豆乳を追加してもホイップクリームを追加しても2のn乗でクラスが増えることもありません。n個オプションを追加したらn個クラスを追加するだけです。非常に楽に実装できます。
まとめ
本記事で利用したサンプルケースのクラス図は下記になります。
最後に「Decorator」パターンとは「元となるオブジェクトに装飾(デコレート)を行うことで機能を拡張させる方法」です。このパターンを使うことで格段に実装すべきクラスの数を減らすことができます。またすべてのオブジェクトを同じ型で扱うことが可能になります。うまく使っていきましょう。
本記事で利用したコードは下記のgit上で公開しております。
コメント