Decoratorパターン
Decoratorパターンは、継承の代わりにコンポジションを使用するデザインパターン。
継承の代替手段として知られ、継承を利用するよりも柔軟に機能追加を行うことができる。継承を避けたい場合に特に有用。
継承がコンパイル時に機能を追加しているのに対して、Decoratorパターンはプログラムの実行時に機能を追加しているため、クラスの変更が最小限に抑えられ、柔軟性が向上する。
具体的には、オブジェクトをラップし、親クラスへの参照を保持したクラスを利用することで、ラップするごとに独自の機能を追加していくことができる。
ランタイム時に機能を追加することができ、これによって機能を拡張させることができる。
コードが複雑になるという欠点を持つ。
java.io
パッケージのInputStream
に関連するものや、UIフレームワークで利用されている。
Decoratorパターンが利用できるシチュエーション
バーガーショップの注文システムを作成するとする。
バーガーは「ハンバーガー」「チーズバーガー」の2種類があり、その他オプションとして「ポテト」「サラダ」があるとして、オプションを追加したときの値段を求めるシステムとする。
必要な抽象クラス
場合によっては抽象クラスではなく、インターフェースを用いる場合もある。
ここで重要なのはToppingsDecorator
は、BurgerComponent
の機能を利用するために継承しているのではなく、同等に扱えるようにするために継承を行っているという点。
ToppingsDecorator
とBurgerComponent
には「is - a」ではなく「has - a」の関係がある。
public abstract class BurgerComponent {
// クライアントはcost()だけ知っていれば良い
public abstract int cost();
}
// Decoratorパターンでは、「同じ型として扱うための継承」を行う
// Decorator is Component. ではないが、Decorator has Component. の関係になっている
public abstract class ToppingsDecorator extends BurgerComponent {
// Componentへの参照を保持する
BurgerComponent burger;
// Component を Decorator がラップする
public ToppingsDecorator(BurgerComponent burger) {
this.burger = burger;
}
public abstract int cost();
}
抽象クラスの継承
Componentを継承したクラス
// ハンバーガーは Decorator ではなく、 Component
public class Hamburger extends BurgerComponent {
int price;
public Hamburger(){
price = 120;
}
@Override
public int cost() {
return price;
}
}
// チーズバーガーも Decorator ではなく、 Component
public class CheeseBurger extends BurgerComponent {
int price;
public CheeseBurger() {
price = 120;
}
@Override
public int cost() {
return price;
}
}
Decoratorを継承したクラス
// ポテトは Decorator
public class Potato extends ToppingsDecorator {
int price;
public Potato(BurgerComponent burger){
// Component を Decorator でラップする
super(burger);
price = 50;
}
@Override
public int cost() {
// Decoratorが保持するComponentへの参照を利用して、機能を追加できる
return burger.cost() + price;
}
}
// サラダも Decorator
public class Salad extends ToppingsDecorator {
int price;
public Salad(BurgerComponent burger) {
// Component を Decorator でラップする
super(burger);
price = 70;
}
@Override
public int cost() {
// Decoratorが保持するComponentへの参照を利用して、機能を追加できる
return burger.cost() + price;
}
}
Decoratorパターンの利用
クライアントは、Component
がcost()
を使えることだけを知っている。
public class Main {
public static void main(String[] args) {
// オプションをつけない注文
BurgerComponent burger1 = new Hamburger();
System.out.println("It's " + burger1.cost() + "yen");
// >> It's 120yen
// Component を Decorator でラップする
BurgerComponent burger2 = new Salad(new Hamburger());
System.out.println("It's " + burger2.cost() + "yen");
// >> It's 190yen
// 好きなだけラップできる
BurgerComponent burger3 = new Potato(
new Potato(
new Salad(
new Hamburger())));
System.out.println("It's " + burger3.cost() + "yen");
// >> It's 290yen
}
}
ポイント
- Decoratorパターンでの機能の追加は、オブジェクトが保持している(has a)親クラスの処理の前後に、追加したい処理を加えることで行う。
- Decoratorによるラップは、いくらでも(何層でも)行うことができる。
- クライアントから見た時、Componentの実装を気にする必要がない(
cost()
というメソッドを持つことさえ知っていれば良い = 透過的
)。 - Factoryパターンや、Builderパターンと併用されることが多い。
-
Hamburger
やCheeseBurger
などのクラス(Componentを継承したクラス)や、Salad
やPotato
などの小さなクラス(Decoratorを継承したクラス)が大量に作成される可能性があり、コードが複雑になることがある。