デコレーターパターン
CoddyのC#ジャーニー「オブジェクト指向プログラミング」セクションの一部 — レッスン 58/70。
デコレーターパターン(Decorator pattern)は、オブジェクトを「デコレーター」と呼ばれる特殊なオブジェクトでラップすることで、そのオブジェクトに新しい振る舞いを追加できるようにする構造パターンの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これは Decorator パターンの柔軟性を示しています。各デコレータは、それがプレーンピザであれ、すでにデコレーションされたピザであれ、受け取ったピザをラップするため、動作を動的に積み重ねることができます!
チートシート
Decorator パターンは、オブジェクトを特別なデコレータオブジェクトでラップすることによって、新しい振る舞いをオブジェクトに付加する構造に関するパターンです。元のクラスを変更することなく、実行時に動的に機能を追加します。
デコレータは、ラップするオブジェクトと同じインターフェースを実装し、ラップされたオブジェクトへの参照を保持し、独自の振る舞いを追加しながら呼び出しをそのオブジェクトに委譲します。
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 パターンを使用します。各デコレータは単一の関心事に集中し、必要に応じて組み合わせることができます。
自分で試してみよう
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: 最終的な説明と価格を出力する
}
}
このレッスンには短いクイズがあります。レッスンを始めて解答し、進捗を記録しましょう。