ヘッドファーストデザインパターンの読み取り。第3章「デコレータパターン」に行きました。
私にとってDecoratorパターンは非常に明確です。 「鉛筆を削る」の1つ
Starbuzzの私たちの友人は、メニューにサイズを導入しています。トール、グランデ、およびベンティのサイズ(翻訳:小、中、大)でコーヒーを注文できるようになりました。 Starbuzzはこれをコーヒークラスの本質的な部分と見なし、BeverageクラスにsetSize()とgetSize()の2つのメソッドを追加しました。彼らはまた調味料がサイズに応じて請求されることを望んでいるので、たとえば、Soyはトールコーヒー、グランデコーヒー、ベンティコーヒーでそれぞれ10セント、15セント、20セントかかります。この変更を処理するためにデコレータクラスをどのように変更しますか要件では?
この章の最後に、この部分を解決するためのコードスニペットを追加しました。このコードスニペットは、すべてのCondimentDecorator
具象クラスに追加されました
public double cost() {
double cost = beverage.cost();
if (getSize() == Beverage.TALL) {
cost += .10;
} else if (getSize() == Beverage.GRANDE) {
cost += .15;
} else if (getSize() == Beverage.VENTI) {
cost += .20;
}
return cost;
}
最初にSize
enumをBeverage
クラスに追加し、次にif..else
さまざまなサイズを確認する
私はこのコードスニペットが将来的に拡張可能になるとは思いません。スターバズがさらにサイズを追加するように要求した場合、すべてのCondimentDecorator
具象クラスが変更され、メンテナンスの悪夢になります。
私はこの部分もデザインパターンにリファクタリングすることを考えていましたが、このトピックにはまだ慣れていません。
このサイズの問題を解決するために適用できる最も適切なパターンは何ですか?
完全なクラス階層は https://github.com/bethrobson/Head-First-Design-Patterns/tree/master/src/headfirst/designpatterns/decorator/starbuzzWithSizes にあります
データベースまたは構成ファイルに接続されたリポジトリを使用して完全なパターンを作成する代わりに、同様の状況に少し修正を加えることをお勧めします。さらに、より複雑なパターンは、実際に追加するものが増えると、正しい答えにはならなくなる可能性があるため、単純にしてください。
そのために、Javaが提供する可能性を考えます:
1:enumは属性を持つことができます。
public enum Beuverage{
TALL(0.10), GRANDE(0.15), VENITI(0.20);
public final int cost;//If i remember well it is mandatory private
private Beuverage(int cost){...}
}
これは、すべての飲み物に同じ追加価格を想定しています。
2:サイズごとにドリンクのタイプごとに価格が異なります:各コンポーネントデコレータの内部でif/elseをMap<Size, int>
に置き換えます
これはかなりseflの説明です。
この例では、さまざまなサイズの調味料ごとに追加コストのテーブルを保存する必要があります。あなたの例では、Soyはトールコーヒー、グランデコーヒー、ベンティコーヒーでそれぞれ10セント、15セント、20セントかかります。多分、キャラメルの対応するコストは12¢、17¢、22¢であり、他のすべての調味料でも異なる追加コストがかかります。この情報はどこかに保存する必要があり、論理的な場所は各具象デコレータクラス内です。
おそらく、デコレータクラスのポイントは、最初に基本的なコーヒーにアクセスし、各調味料にエクストラを追加して外側に向かって作業することにより、最終的なコーヒーの総コストを計算できることです。各デコレータがその仕事をすることができるようにサイズを知る必要があるという事実から逃れることはありません。
したがって、本で提案されている解決策は、あなたが正しく特定したメンテナンスの問題にもかかわらず、状況にとってそれほど悪くはありません。
編集: WalfratのJavaに対する最適な返信を読んだ後、元の返信から役に立たない部分をいくつか削除しました。言語固有ではないが、if-else/switchステートメントブロックを削除する方法はいくつかあります。たとえば、ポリモーフィズム(Robを参照)、コンポジット(Sikorskiを参照)、戦略(Robを再度参照)などですが、言及されていないVisitorも含まれます。 JavaのMapsやEnums、PHPの連想配列、C、C++の関数の配列などの言語固有のオプションもいくつかあり、各決定点でのアクションの複雑さによって異なります。ここで本当に良い一般的な質問のために!
しかし、例は、Decoratorパターンの動作を説明することを目的とする教科書からのものであることを忘れないでください。これは、パターンがどれほど強力であるかを示す非常に良い例ですが、特定の状況でどのようにストレスがかかり始めるかでもあります。
実際には、右心の誰もがこのようなコストを追加するためにDecoratorを使用するという問題に直面することはなく、はるかに簡単なアプローチがあります。デコレータは、機能を階層化することにより、ドキュメントの処理や複雑なユーザーインターフェイスの構築など、より複雑な状況での使用に適しています。
サイズベースのCondimentDecoratorの実装を作成できます。例えば。 LargeCondimentDecorator、SmallCondimentDecoratorなどの各コストメソッドは、そのサイズの適切なコストを返します。
次に、タイプenumを使用するのではなく、適切なサイズがサイズに対して開始されることを確認してください。
私が確実に行うことの1つは、それを独自のメソッドにリファクタリングすることです。そのサイズに基づいてコストを追加しますね。次に、そのメソッドが必要です:
public double cost() {
return beverage.cost() + additionalCostBasedOnSize();
}
private double additionalCostBasedOnSize() {
switch(getSize()){
case Beverage.TALL: return .10;
case Beverage.GRANDE: return .15;
case Beverage.VENTI: return .20;
default: return 0;
}
}
この出発点から、あなたはそれらを一緒にグループ化するために異なる調味料の間のいくつかの一般的なパターンを見つけるかもしれません
戦略パターンを使用できます。疑似コード:
inteface SizePricingPolicy
getCost(Size)
class DefaultSizePricing:SizePricingPolicy{
int getCost(Size){
case Beverage.TALL: return .10;
case Beverage.GRANDE: return .15;
case Beverage.VENTI: return .20;
default: return 0;
}
}
各デコレータには、コンストラクタインジェクションを介して価格設定ポリシーのインスタンスが渡されます。デコレータのcost()メソッドは、ポリシーのgetCost()メソッドを呼び出して、コストを決定する一環としてサイズを引数として渡します。
メリットは次のとおりです。サイズ価格ポリシーは編集する代わりに交換できます。デコレーターはSizePricingPolicyインターフェースへの参照のみを保持するため、サイズ価格の変更から分離されます。
この場合、私はいくつかの疑似複合パターンに行っており、コストロジックはすべて別のクラスにあります。クラス図は以下の通りです:
したがって、Coffeeに入るすべてのものは基本的にCoffeeComponent
であり、したがってインターフェースです。 3つの主要なメソッドgetSize()
、getName()
およびcontributionToCost()
があります。 contributionToCost()
メソッドはパラメーターPricingService
を取ることに注意してください。これは、コストロジックが価格設定サービスで使用されるためです。 AbstractCoffeeComponent
はCoffeeComponent
インターフェイスを実装して、一般的なメソッドの実装を提供しています。 AbstractCoffeeComponent
から、2つのクラスはサブクラスです:Type
とIngredient
。したがって、espress、mochaはType
クラスを参照し、砂糖、ミルクはIngredient
インスタンスを参照します。アイデアは、PricingServiceには、CoffeeComponent
のname
とsize
に基づいてコストを提供できる検索メソッドがあるということです。階層の残りの部分は、コーヒーのコンセプトをどのようにコーディングできるかを示すためだけのものです。以下は、PricingService
、Client
、およびCoffee
のコードです。
PricingService.Java
package coffee;
import Java.util.HashMap;
import Java.util.Map;
import Java.util.Objects;
/**
* Pricing ideally would be querying database for costs but here HashMap will do
*/
public class PricingService {
private static final class CostComponent {
private String componentName;
private Size componentSize;
public CostComponent(String name, Size size) {
this.componentName = name;
this.componentSize = size;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
CostComponent costComponent1 = (CostComponent) o;
return componentName.equalsIgnoreCase(costComponent1.componentName) &&
componentSize == costComponent1.componentSize;
}
@Override
public int hashCode() {
return Objects.hash(componentName, componentSize);
}
}
private final static Map<CostComponent, Double> costs = new HashMap<>();
static {
costs.put(new CostComponent("Espresso", Size.TALL), 10.0);
costs.put(new CostComponent("Rum", Size.TALL), 40.0);
}
public double costOf(CoffeeComponent component){
return costs.getOrDefault(new CostComponent(component.getName(), component.getSize()),0.0);
}
}
Coffee.Java
package coffee;
import Java.util.HashSet;
import Java.util.Set;
/**
* Container class - because we want coffee
*/
public class Coffee {
private final Type type;
private Set<Ingredient> ingredients;
public Coffee(Type type) {
this.type = type;
this.ingredients = new HashSet<>();
this.ingredients.addAll(type.getDefaultIngredients());
}
public void addIngredient(Ingredient ingredient){
this.ingredients.add(ingredient);
}
/**
* Propagate size to all igredients and type
*/
public void setSize(Size size){
for(Ingredient ingredient: ingredients){
ingredient.setSize(size);
}
type.setSize(size);
}
/**
* Aggregate of all pieces for cost
*/
public double cost(PricingService pricingService){
double cost = 0.0;
for(Ingredient ingredient: ingredients){
cost+=ingredient.contributionToCost(pricingService);
}
cost+=type.contributionToCost(pricingService);
return cost;
}
}
Client.Java
package coffee;
public class Client {
public static void main(String[] args) {
PricingService starbuzz = new PricingService();
//Order an Espresso
Coffee espresso = new Coffee(new Type("Espresso"));
/**
* Here new Type, new Ingredient is done, ideally this will also be some sort of lookup using Factory pattern
*/
//add sugar
espresso.addIngredient(new Ingredient("Sugar"));
//add rum :)
espresso.addIngredient(new Ingredient("Rum"));
//make it large
espresso.setSize(Size.TALL);
double cost = espresso.cost(starbuzz);
System.out.println("Cost : " + cost);
}
}
CoffeeComponent.Java
package coffee;
public interface CoffeeComponent {
String getName();
void setSize(Size size);
/**
* Default lookup, implementations can override if wanted
*/
default double contributionToCost(PricingService pricingService){
return pricingService.costOf(this);
}
/**
* By default coffee is normal unless explicitly stated
*/
default Size getSize(){
return Size.NORMAL;
}
}
Ingredient.Java
package coffee;
/**
* Sugar, milk, soy etc come here
*/
public class Ingredient extends AbstractCoffeeComponent {
public Ingredient(String name) {
super(name);
}
}
Type.Java
package coffee;
import Java.util.Collections;
import Java.util.Set;
/**
* Espresso, Mocha etc types come here
*/
public class Type extends AbstractCoffeeComponent {
public Type(String name) {
super(name);
}
/**
* Default ingredients that come with this type
*/
public Set<Ingredient> getDefaultIngredients(){
return Collections.emptySet();
}
}
AbstractCoffeeComponent.Java
package coffee;
public abstract class AbstractCoffeeComponent implements CoffeeComponent {
protected final String name;
protected Size size;
public AbstractCoffeeComponent(String name) {
this.name = name;
}
@Override
public String getName() {
return this.name;
}
@Override
public void setSize(Size size) {
this.size = size;
}
@Override
public Size getSize() {
return size;
}
}
型、材料、サイズをさらに追加する必要があるかどうかを確認できるように、PricingService
のHashmap
に行を追加するだけです。 @bcperthには同意しますが、この例は、Decoratorパターンの長所と短所を示すことでした。