メインコンテンツへ移動

デザインパターン - デコレータパターン(Decorator Pattern)

無料2015-03-06#Design_Pattern#装饰者模式#Decorator Pattern

デコレータパターンはフレームワークで広く適用されるデザインパターンです。Java API のファイルストリーム操作でもこのパターンが適用されており、例えば InputStream in = new BufferedInputStream(new FileInputStream(file)); のように、BufferedInputStream が FileInputStream をデコレートして機能拡張を実現します。デコレータパターンは「修正に対して閉じ、拡張に対して開く」という OO 設計原則を満たす、優れたデザインパターンです。

no_mkd

一.デコレータパターンとは?

デコレータパターンは「修正に対して閉じ、拡張に対して開く」という原則を完璧に実現できます。つまり、被デコレータを修正せずに、その機能を拡張できるのです。ファイル操作コードを見てみましょう:

InputStream in = new BufferedInputStream(new FileInputStream(file));

最も内側に「ラップ」されている InputStream オブジェクトは new FileInputStream(file) で、基本的なファイル入力ストリームです。BufferedInputStream オブジェクトでその機能を拡張します。さらにこうすることもできます:

P.S. 上で使った動詞「ラップ」に注目してください。これがデコレータパターンの核心です

InputStream in = new LineNumberInputStream(new BufferedInputStream(new FileInputStream(file)));

新しいデコレータ LineNumberInputStream を追加して、行番号を取得する機能を拡張します。もちろん、新しいデコレータを追加し続ける限り、さらに多くの機能を拡張できます。振り返ってみると、FileInputStream に何層ものデコレータを追加して、次々と機能を獲得しました。この過程で、機能の動的拡張を実現しましたが、被デコレータである FileInputStream の何も修正していません。これがいわゆる「修正に対して閉じ、拡張に対して開く」原則です。

二.例を挙げる

Milk を販売する店を開くと仮定します。選択可能なトッピングにはモカ Mocha(チョコレート風味)、コーヒー Coffee、氷水 IceWater があります。もちろん、売れ行きが良ければ新しい飲料(Orange、Yoghurt など)や新しいトッピング(Salt。。冗談です)を導入する予定です。Milk 自体に価格があり、祝祭日には割引されます。様々なトッピングの価格も異なります。もちろん、IceWater を 2 倍追加した Milk を注文することもできます。。最も簡単に思いつく解決策は:

Milk クラスを定義し、hasMocha、hasCoffee、hasIceWater(追加されたトッピングを表す)などの多くの属性を含めます。さらに discount 属性(割引情報用)、cost 属性(価格用)も必要です。これで完了でしょうか?いいえ、それ以外にも MochaNum、CoffeeNum、IceWaterNum(トッピングの份数を表す。濃厚な味を好む顧客は 2 倍またはそれ以上のトッピングを必要とします。。)も必要です

問題は解決しましたが、これで本当に良いのでしょうか?

以下の状況を考慮してください:1. 新しい飲料 Orange を導入(Orange を定義する必要があります。ほぼ再利用可能な部分がなく、ゼロから開始。。または、Beverage 基底クラスを定義して、飲料に共通する部分を入れることができます);2. 新しいトッピング Salt を導入(Milk クラスを修正して、hasSalt、SaltNum 属性を追加し、塩入り Milk のニーズを満たす必要があります。。)

現在、私たちの解決策は非常に悪く、どんな変化にも適応できないことがわかります。機能を拡張するには、既存のカプセル化されたコードを修正する必要がある可能性があり、さらにパフォーマンス上の問題もあります:飲料価格の計算部分に大量の if...else... 構造が必要で、コードが膨らみ、再利用が困難になります(異なる飲料ではトッピングが異なる可能性があり、価格計算方法も異なります)。では、デコレータパターンを試す時です

まず、被デコレータとデコレータは同じスーパークラスを持たなければならないため(なぜかは暫く説明しません)、以下の Beverage 基底クラスを定義します:

package DecoratorPattern;

/**

  • @author ayqy
  • Beverage スーパークラスの定義。すべての具体的な Beverage と Ingredient はこのクラスを拡張する必要があります

*/ public abstract class Beverage { String desc = "Unknown Beverage";//飲料の説明情報を定義 float cost;//飲料の価格を定義

public abstract float getCost();//この飲料の価格を返す cost メソッドを定義。サブクラスはこのメソッドを実装する必要があります

public String getDesc(){
	return desc;
}

}

Beverage があれば、被デコレータ——Milk を定義し始められます:

package DecoratorPattern;

/**

  • @author ayqy
  • 具体的な Beverage:Milk クラスの定義

*/ public class Milk extends Beverage{

float discount = 1;//割引を定義。祝祭日には Milk が割引される可能性があります(デフォルトは割引なし)

public float getDiscount() {
	return discount;
}

public void setDiscount(float discount) {
	this.discount = discount;
}

public Milk(){
	cost = 4.5f;//Milk の価格を初期化
	desc = "Milk";//説明情報を初期化
}

 @Override
public float getCost(){
	return discount * cost;//割引後の価格を返す
}

}

次にデコレータです。デコレータは Beverage とは異なる特性を持つため、抽象化します:

package DecoratorPattern;

/**

  • @author ayqy
  • Ingredient トッピングクラスの定義。Beverage を継承(デコレータパターンでは、デコレータと被デコレータは同じスーパークラスを持つ必要があります)

*/ public abstract class Ingredient extends Beverage{

Beverage beverage;//このトッピングを追加する飲料

 @Override
public String getDesc() {
	return "(" + desc + ")" + beverage.getDesc();//トッピングの説明には括弧を付けて、トッピングと飲料を区別します
}

 @Override
public float getCost() {
	return cost + beverage.getCost();//トッピングには割引がありません。その価格 + 飲料価格を直接返します
}

//ここで Ingredient が Beverage と異なる他の属性と動作を追加

}

上記の getDesc と getCost メソッドに注目してください。価格計算と説明情報生成の責任を完全にメソッド呼び出しメカニズムに委譲したため、コードがこれほど簡潔になっています。。

次に具体的なトッピング——IceWater、Coffee、Mocha を定義します:

package DecoratorPattern;

/**

  • @author ayqy
  • トッピング IceWater 氷水の��義

*/ public class IceWater extends Ingredient{

public IceWater(Beverage bev)
{
	cost = 0.5f;
	desc = "IceWater";
	beverage = bev;
}

}

package DecoratorPattern;

/**
 * @author ayqy
 * トッピング Coffee コーヒーの定義
 *
 */
public class Coffee extends Ingredient{
	
	public Coffee(Beverage bev)
	{
		cost = 3;
		desc = "Coffee";
		beverage = bev;
	}
}
package DecoratorPattern;

/**
 * @author ayqy
 * トッピング Mocha モカの定義
 *
 */
public class Mocha extends Ingredient{
	
	public Mocha(Beverage bev)
	{
		cost = 2;
		desc = "Mocha";
		beverage = bev;
	}
}

準備完了です。Milk の小店を開業できます。。

三.効果の例

まずテストクラスを定義します:

package DecoratorPattern;

public class Test { public static void main(String[] args){ Beverage bev;

	System.out.println("モカとコーヒーを追加した Milk を作成。。");
	bev = new Milk();//まず Milk を作成
	bev = new Mocha(bev);//Mocha を追加
	bev = new Coffee(bev);//Coffee を追加
	System.out.println(bev.getDesc() + " " + bev.getCost() + "¥");
	
	System.out.println("氷水 2 倍とモカ 2 倍を追加したコーヒー Milk を作成。。");
	bev = new Milk();//Milk を再作成
	bev = new IceWater(bev);//氷水を追加
	bev = new IceWater(bev);//氷水を追加
	bev = new Mocha(bev);//Mocha を追加
	bev = new Mocha(bev);//Mocha を追加
	bev = new Coffee(bev);//Coffee を追加
	System.out.println(bev.getDesc() + " " + bev.getCost() + "¥");
	//もちろんこう書くこともできます:Beverage bev = new Coffee(new Mocha(new Milk()));
	//私たちが熟知するメソッドチェーンと比較:InputStream in = new BufferedInputStream(new FileInputStream(file));
}

}

結果の例:

効果は良いでしょう。以前の拡張問題を再考してください:

1. 新しい飲料 Orange を導入(Beverage 基底クラスを継承する Orange クラスを定義する必要があります。Beverage 基底クラスで既存の部分を再利用できます。それでも満足できない場合、もちろん「具体的な飲料クラス ConcreteBeverage」を抽象化して、Milk などの他の飲料を在此基础上で拡張できます);2. 新しいトッピング Salt を導入(Milk クラスを修正する必要はありません。Salt トッピングを実装して Ingredient クラスを継承するだけです)

デコレータパターンの長所に気づきましたか?では、冷水を浴びせる時です。。

四.デコレータパターンの長所と短所

短所は明白です——こんなに長いコードを見たことがありますか?

XObject o = new XDecorator(new XXDecorator(new XXXDecorator(new XXXXDecorator())));

はい、これは被デコレートオブジェクトの機能を 3 回拡張しただけです。もちろん、もっと多くできます。。つまり、もっと長くできるということです。さらに、使用時に多くの小さなオブジェクトを作成します。例えば:

bev = new Milk();//Milk を再作成
bev = new IceWater(bev);//氷水を追加
bev = new IceWater(bev);//氷水を追加
bev = new Mocha(bev);//Mocha を追加
bev = new Mocha(bev);//Mocha を追加
bev = new Coffee(bev);//Coffee を追加

デコレータパターンに馴染みのない人が上記のコードを読んだ場合、すぐに理解できるでしょうか?

注目してください。上記のコードは冒頭で言及した動詞——「ラップ」を説明しています。对吧?

長所:

上で言及した動的拡張の長所 외에도、さらに重要な長所があります。それは前述の getDesc と getCost メソッドです

その通りです。この呼び出しメカニズムを利用して操作を完了できます(デコレート動作の前または後にカスタム操作を追加するだけです。例では実際にはデコレート動作後に操作を追加しています)。再帰に似た効果を簡単に達成できます

これが「なぜデコレータと被デコレータは同じスーパークラスを持たなければならないのか?」を説明しています。もう少し説明が必要です:

「継承よりコンポジションを使え」という設計原則があります。ここでこの原則に違反しているように見えます

実際には原則に違反していません。デコレータパターンにおける継承は、型のマッチングを取得するためのものであり、継承を利用してクラスの動作を拡張するためのものではありません。「継承よりコンポジションを使え」原則の前提条件「(クラスの動作を拡張する必要がある場合)」が省略されています

コメント

コメントはまだありません

コメントを書く