【问题标题】:combining decorator and state pattern in java - question about OO design在java中结合装饰器和状态模式 - 关于OO设计的问题
【发布时间】:2010-12-16 04:00:01
【问题描述】:

我正在解决一个我认为最适合装饰器和状态模式的问题。高级设置类似于三明治机和分配器,我可以在其中制作一定数量的配料和几种不同类型的三明治。每个成分都有与之相关的成本。客户将使用机器选择成分来制作特定的swndwich,然后机器会分配它。

到目前为止,我已经使用装饰器模式创建了成分和不同类型的三明治:

public abstract class Sandwich {
    String description = "Unknown Sandwich";

    public String getDescription(){
        return description;
    }

    public double cost(){
        return 0.0;
    }
}

每种成分都是这样建模的:

public abstract class Ingredient extends Sandwich {
    public abstract String getDescription();
}

此外,具体成分是:

public class Cheese extends Ingredient {
    private Sandwich sandwich;

    public Cheese(Sandwich sandwich){
        this.sandwich = sandwich;
    }

    public String getDescription() {
        return sandwich.getDescription() + ", cheese";
    }

    public double cost() {
        return 0.25 + sandwich.cost();
    }
}

一种特定类型的三明治可以这样建模:

public class BLT extends Sandwich {
    public BLT(){
        description = "Bacon, Lettuce and Tomato";
    }
}

所以客户会像这样创建一个特定的三明治:

Sandwich order_a_blt = new Tomato(new Lettuce(new Bacon(new Bread(new BLT()))));

下一步,我将创建一个 Dispenser 对象,该对象将充当自动机器,预先加载特定数量的成分(以通用单位测量),用户可以按下按钮选择一个预设选择:

例如

  • BLT:1 单位番茄,1 单位番茄 生菜、1 个培根、1 个面包
  • SUB:1 个肉丸,1 个奶酪, 1 个意大利酱,1 个面包
  • 等等..

我的分配器机器将预装每种成分的固定数量的单位

  • 番茄:10个
  • 生菜:10个
  • 培根:10个
  • 等等..

以及供用户选择特定种类三明治的按钮列表:

  • 1-BLT
  • 2-SUB
  • 3-烧烤
  • ..等

这个想法是跟踪成分的内部容量,并能够告诉用户,比如说,我们没有足够的培根来制作另一个 BLT

现在,我最初的想法是基于状态设计模式创建 Dispenser 对象,但我在尝试将成分类的对象与 Dispenser 类中的某种存储组合时遇到了问题。起初,我通过名称/值对成分类型/成分数量的映射。但我不确定如何将这些模式组合在一起,以便在每次使用后自动递减。

您是否对如何继续实施这样的概念有一个大致的了解?首先,我在装饰器和状态模式的正确轨道上吗?会有更有效的方法吗?我希望我已经清楚地解释了这个问题。

感谢您的任何指导,感谢您的任何想法

【问题讨论】:

    标签: java oop design-patterns decorator state-pattern


    【解决方案1】:
    1. 成分不是 IS-A 三明治;
    2. 最好将原料价格外部化,以使其灵活变化;
    3. 最好生成三明治 运行时的描述基于其 成分而不是硬编码 在课堂上;
    4. 成分应该一无所知 关于三明治;

    所以,我会提供以下解决方案:

    package com;
    
    public enum Ingredient {
    
     CHEESE, TOMATO, LETTUCE, BACON, BREAD, MEATBALL, ITALIAN_SAUCE;
    
     private final String description;
    
     Ingredient() {
      description = toString().toLowerCase();
     }
    
     Ingredient(String description) {
      this.description = description;
     }
    
     public String getDescription() {
      return description;
     }
    }
    
    
    package com;
    
    import static com.Ingredient.*;
    
    import java.util.*;
    import static java.util.Arrays.asList;
    
    public enum SandwitchType {
    
     BLT(
       asList(TOMATO, LETTUCE, BACON, BREAD),
                 1  ,    1,      1  ,   1
     ),
     SUB(
       asList(MEATBALL, CHEESE, ITALIAN_SAUCE, BREAD),
                  1   ,    1  ,      1       ,   1
     );
    
     private final Map<Ingredient, Integer> ingredients = new EnumMap<Ingredient, Integer>(Ingredient.class);
     private final Map<Ingredient, Integer> ingredientsView = Collections.unmodifiableMap(ingredients);
    
     SandwitchType(Collection<Ingredient> ingredients, int ... unitsNumber) {
      int i = -1;
      for (Ingredient ingredient : ingredients) {
       if (++i >= unitsNumber.length) {
        throw new IllegalArgumentException(String.format("Can't create sandwitch %s. Reason: given ingedients "
          + "and their units number are inconsistent (%d ingredients, %d units number)", 
          this, ingredients.size(), unitsNumber.length));
       }
       this.ingredients.put(ingredient, unitsNumber[i]);
      }
     }
    
     public Map<Ingredient, Integer> getIngredients() {
      return ingredientsView;
     }
    
     public String getDescription() {
      StringBuilder result = new StringBuilder();
      for (Ingredient ingredient : ingredients.keySet()) {
       result.append(ingredient.getDescription()).append(", ");
      }
    
      if (result.length() > 1) {
       result.setLength(result.length() - 2);
      }
      return result.toString();
     }
    }
    
    
    package com;
    
    import java.util.Map;
    import java.util.concurrent.ConcurrentHashMap;
    import java.util.concurrent.ConcurrentMap;
    
    public class PriceList {
    
     private static final int PRECISION = 2;
    
     private final ConcurrentMap<Ingredient, Double> prices = new ConcurrentHashMap<Ingredient, Double>();
    
     public double getPrice(SandwitchType sandwitchType) {
      double result = 0;
      for (Map.Entry<Ingredient, Integer> entry : sandwitchType.getIngredients().entrySet()) {
       Double price = prices.get(entry.getKey());
       if (price == null) {
        throw new IllegalStateException(String.format("Can't calculate price for sandwitch type %s. Reason: "
          + "no price is defined for ingredient %s. Registered ingredient prices: %s",
          sandwitchType, entry.getKey(), prices));
       }
       result += price * entry.getValue();
      }
      return round(result);
     }
    
     public void setIngredientPrice(Ingredient ingredient, double price) {
      prices.put(ingredient, round(price));
     }
    
     private static double round(double d) {
      double multiplier = Math.pow(10, PRECISION);
      return Math.floor(d * multiplier + 0.5) / multiplier;
     }
    }
    
    
    package com;
    
    import java.util.Map;
    import java.util.EnumMap;
    
    public class Dispenser {
    
     private final Map<Ingredient, Integer> availableIngredients = new EnumMap<Ingredient, Integer>(Ingredient.class);
    
     public String buySandwitch(SandwitchType sandwitchType) {
      StringBuilder result = new StringBuilder();
      synchronized (availableIngredients) {
    
       Map<Ingredient, Integer> buffer = new EnumMap<Ingredient, Integer>(availableIngredients);
       for (Map.Entry<Ingredient, Integer> entry : sandwitchType.getIngredients().entrySet()) {
        Integer currentNumber = buffer.get(entry.getKey());
        if (currentNumber == null || currentNumber < entry.getValue()) {
         result.append(String.format("not enough %s (required %d, available %d), ",
           entry.getKey().getDescription(), entry.getValue(), currentNumber == null ? 0 : currentNumber));
         continue;
        }
        buffer.put(entry.getKey(), currentNumber - entry.getValue());
       }
    
       if (result.length() <= 0) {
        availableIngredients.clear();
        availableIngredients.putAll(buffer);
        return "";
       }
      }
      if (result.length() > 1) {
       result.setLength(result.length() - 2);
      }
      return result.toString();
     }
    
     public void load(Ingredient ingredient, int unitsNumber) {
      synchronized (availableIngredients) {
       Integer currentNumber = availableIngredients.get(ingredient);
       if (currentNumber == null) {
        availableIngredients.put(ingredient, unitsNumber);
        return;
       }
       availableIngredients.put(ingredient, currentNumber + unitsNumber);
      }
     }
    }
    
    
    package com;
    
    public class StartClass {
     public static void main(String[] args) {
      Dispenser dispenser = new Dispenser();
      for (Ingredient ingredient : Ingredient.values()) {
       dispenser.load(ingredient, 10);
      }
      PriceList priceList = loadPrices();
      while (true) {
       for (SandwitchType sandwitchType : SandwitchType.values()) {
        System.out.printf("About to buy %s sandwitch. Price is %f...",
          sandwitchType, priceList.getPrice(sandwitchType));
        String rejectReason = dispenser.buySandwitch(sandwitchType);
        if (!rejectReason.isEmpty()) {
         System.out.println(" Failed: " + rejectReason);
         return;
        }
        System.out.println(" Done");
       }
      }
     }
    
     private static PriceList loadPrices() {
      PriceList priceList = new PriceList();
      double i = 0.1;
      for (Ingredient ingredient : Ingredient.values()) {
       priceList.setIngredientPrice(ingredient, i);
       i *= 2;
      }
      return priceList;
     }
    }
    

    【讨论】:

    • 我不确定为什么我们需要一个并发结构。我们假设分配器就像一个汽水机,带有选择按钮和一个输出,可以让罐子出来。我的意思是每次都会有一个用户。在这种情况下并发不会增加开销吗?
    • 一个用户并不意味着一个线程 :) 此外,您不能确定您的要求在未来不会改变。无论如何,这里的开销可以忽略不计,尤其是如果您使用 java6。
    • 感谢您的回复。只是想知道:对成分和三明治类型使用枚举,这不是很难扩展,以防万一,就像你在需求中说的那样,我们需要添加更多成分或提供更多类型的三明治?
    • 我的意思是,我不确定以这种方式使用枚举的优势,而不是创建一个成分抽象类并从它扩展所有不同的成分。三明治也是如此
    • 两种方式都可以接受。枚举为允许的对象定义了更严格的契约。我的意思是,如果您有一个独立的类(在我的示例中为 PriceList),您可以引用所有声明的成分类型。如果您将成分作为抽象类/接口,则您不知道使用了哪些特定实现。
    【解决方案2】:

    装饰器模式不适合您的问题。成分不会向三明治添加新行为,更不用说在 is-a 关系中链接三明治和(三明治)成分已经有点做作了。 (嵌套实例化看起来很酷,除非您必须动态执行它。)

    三明治有配料/馅料/调味品。为成分建立一个类层次结构,并使用复合模式将它们与三明治折叠在一起。

    public abstract class Ingredient {
        protected Ingredient(Object name) { ... }
        public String name() { ... }
        public abstract String description();
        public abstract double cost();
    }
    
    public Cheese extends Ingredient {
        public Cheese() { super("Cheese"); }
        public String description() { ... }
        public double cost() { return 0.25; }
    |
    
    public abstract class Sandwich {
       public abstract double cost();
       public Set<Ingredient> fillings() { ... }
       public boolean addFilling(Ingredient filling) { ... }
       public boolean removeFilling(Ingredient filling) { ... }
       public double totalFillingsCost();
       ...
    }
    
    public class SubmarineSandwich extends Sandwich {
       public SubmarineSandwich() { ... }
       public double cost() { return 2.50 + totalFillingsCost(); }   
    }
    
    public enum SandwichType { 
        Custom,
        Blt,
        Sub,
        ...
    }
    
    public class SandwichFactory  {
        public Sandwich createSandwich(SandwichType type) {
            switch (type) {
                case Custom:
                    return new Sandwich() { public double cost() { return 1.25; } };
                case Blt:
                    return new BaconLettuceTomatoSandwich();
                case Sub:
                   return new SubmarineSandwich();
                ....
            }
        }
    }
    

    同样,我不认为状态模式对分配器有用,因为它与成分或三明治的管理有关。该模式规定了对象的内部使用来改变类的行为。但是 DISpenser 不需要基于状态的多态行为:

    public class SandwichDispenser {
        ...
        public void prepareSandwich(SandwichType type) throws SupplyException { ... }
        public Sandwich finalizeSandwich() throws NotMakingASandwichException { ... }
        public boolean addFilling(Ingredient filling) throws SupplyException { ... } 
    }
    

    例如,Dispenser 的内部状态没有显着变化,这需要其公共接口的多态行为。

    【讨论】:

    • 感谢您的示例。您是否仍然喜欢使用静态柜台来确保食材供应足以制作或不制作特定的三明治?
    • 我想计数应该是分配器而不是成分类别的问题,对吧?
    • 我同意数量是成分对象外部的关注点,是的。对于数量跟踪问题,我认为并发不是问题(一个分配器到一个用户),速度也不是问题,所以某种 Map, Integer> 就足够了。
    【解决方案3】:

    Sandwich 到 Cheese 是“has-a”关系,因此 Sandwich 永远不应该是 Cheese 的父级。

    不确定你在这行做什么:

    Sandwich order_a_blt = new Tomato(new Lettuce(new Bacon(new Bread(new BLT()))));
    

    从逻辑上讲,为什么要创建一个 Tomato 对象并传递给它一个生菜? 番茄、生菜 .... 等应扩展成分。

    我会变成这样的

    class Sandwich{ public Sandwich(Ingredients ...ing){}}
    

    在每个成分类中,我会在 Tomato 中放置一个静态变量,将其命名为tomatoCount,然后在创建 Dispenser 时对其进行初始化,每次创建新的 Tomato 时都会将其递减。如果它达到零,那么番茄班会抱怨

    【讨论】:

    • 那么,Sandwich objectt 的构造函数会接受可变数量的参数,对吧?
    • 是的,使用 ... (Java 5) 你可以添加 0 个或多个变量,这在你的程序中是有意义的,例如你可以说: s1 = new Sandwich(Tomato,Lettuce,酱); s2 = 新三明治(番茄); s3 = 新三明治();
    猜你喜欢
    • 2010-10-07
    • 2013-11-28
    • 1970-01-01
    • 1970-01-01
    • 2020-05-13
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-05-19
    相关资源
    最近更新 更多