デコレーターパターン
CoddyのC#ジャーニー「オブジェクト指向プログラミング」セクションの一部 — レッスン 58/70。
デコレーターパターンは、オブジェクトをデコレーターと呼ばれる特殊なオブジェクトでラップすることで、オブジェクトに新しい振る舞いを追加できるようにする構造パターンの1つです。コンパイル時に振る舞いを追加する継承とは異なり、デコレーターは元のクラスを変更することなく、実行時に動的に機能を追加します。
このパターンは、デコレータがラップするオブジェクトと同じインターフェースを実装することによって機能します。各デコレータはラップされたオブジェクトへの参照を保持し、そのオブジェクトに呼び出しを委譲し、その前後で独自の動作を追加します。
public interface ICoffee
{
string GetDescription();
decimal GetCost();
}
public class SimpleCoffee : ICoffee
{
public string GetDescription() => "Coffee";
public decimal GetCost() => 2.00m;
}
public abstract class CoffeeDecorator : ICoffee
{
protected ICoffee _coffee;
public CoffeeDecorator(ICoffee coffee) => _coffee = coffee;
public virtual string GetDescription() => _coffee.GetDescription();
public virtual decimal GetCost() => _coffee.GetCost();
}
public class MilkDecorator : CoffeeDecorator
{
public MilkDecorator(ICoffee coffee) : base(coffee) { }
public override string GetDescription() => _coffee.GetDescription() + ", Milk";
public override decimal GetCost() => _coffee.GetCost() + 0.50m;
}デコレータの強力な点は、それらを積み重ねることができる点にあります。デコレータを適用したオブジェクトを、さらに別のデコレータでラップすることができます:
ICoffee coffee = new SimpleCoffee();
coffee = new MilkDecorator(coffee);
coffee = new MilkDecorator(coffee); // ミルクを2回追加!
Console.WriteLine(coffee.GetDescription()); // Coffee, Milk, Milk
Console.WriteLine(coffee.GetCost()); // 3.00デコレーターパターンは、サブクラスを急増させることなく、オブジェクトに責務を追加する必要がある場合に最適です。各デコレーターは単一の関心事に焦点を当てており、必要に応じてそれらを組み合わせることで、望み通りの正確な振る舞いを構築できます。
チャレンジ
簡単Decorator パターンを使用して、ピザ注文システムを構築しましょう。ベースとなるピザを作成し、そこにトッピングを動的に追加していきます。各トッピングは前のレイヤーをラップし、説明と価格の両方を追加します。
コードは 3 つのファイルに分けて構成します:
Pizza.cs:PizzaShop名前空間内に、文字列を返すGetDescription()と decimal を返すGetPrice()の 2 つのメソッドを持つIPizzaインターフェースを定義します。次に、このインターフェースを実装するPlainPizzaクラスを作成します。このクラスは、説明として"Pizza"を、基本価格として8.00mを返すようにします。ToppingDecorators.cs:IPizzaを実装し、ラップされたピザへの参照(コンストラクタ経由で渡される)を保持する抽象クラスToppingDecoratorを作成します。その仮想メソッドは、ラップされたピザに処理を委譲する必要があります。次に、3 つの具体的なデコレータを作成します:CheeseTopping- 説明に", Cheese"を追加し、価格に1.50mを加算しますPepperoniTopping- 説明に", Pepperoni"を追加し、価格に2.00mを加算しますMushroomTopping- 説明に", Mushrooms"を追加し、価格に1.25mを加算します
Program.cs: プレーンピザから始めて、入力に基づいてトッピングでラップすることで、すべてをまとめます。各トッピングデコレータは前のピザをラップし、最終的な注文をレイヤーごとに構築します。
1 つの入力を受け取ります:
- 追加するトッピングのカンマ区切りリスト(例:
cheese,pepperoni,cheese)
PlainPizza から開始し、出現順に各トッピングに対応するデコレータでラップします。有効なトッピングは cheese、pepperoni、mushrooms です。すべてのトッピングを適用した後、1 行目に説明を、次の行に合計金額を出力してください。
例えば、入力が cheese,pepperoni,mushrooms の場合、出力は以下のようになります:
Pizza, Cheese, Pepperoni, Mushrooms
12.75同じトッピングを複数回追加できることに注目してください!入力が cheese,cheese の場合、以下のようになります:
Pizza, Cheese, Cheese
11.00これは Decorator パターンの柔軟性を示しています。各デコレータは、それがプレーンピザであれ、すでにデコレーションされたピザであれ、受け取ったピザをラップするため、振る舞いを動的に積み重ねることができます!
自分で試してみよう
using System;
using PizzaShop;
class Program
{
public static void Main(string[] args)
{
// カンマ区切りのトッピングのリストを読み込む
string input = Console.ReadLine();
string[] toppings = input.Split(',');
// TODO: PlainPizza から開始する
// TODO: 各トッピングをループで処理し、ピザをラップする
// トッピング名に基づいて適切なデコレータで:
// - "cheese" -> CheeseTopping
// - "pepperoni" -> PepperoniTopping
// - "mushrooms" -> MushroomTopping
// TODO: 最終的な説明と価格を出力する
}
}
このレッスンには短いクイズがあります。レッスンを始めて解答し、進捗を記録しましょう。