简体   繁体   English

在java中结合装饰器和状态模式 - 关于OO设计的问题

[英]combining decorator and state pattern in java - question about OO design

I am in the middle of solving a problem where I think it's best suited for a decorator and a state pattern.我正在解决一个我认为最适合装饰器和状态模式的问题。 The high level setting is something like a sandwich maker and dispenser, where I have a set amount of ingredients and a few different types of sadnwiches i can make.高级设置类似于三明治机和分配器,在那里我有一定数量的配料和我可以制作的几种不同类型的三明治。 Each ingedient has a cost associated with it.每种成分都有与之相关的成本。 The client would be someone who will use the machine to select ingredients to make a particular swndwich and the machine would dispense it.客户将是使用机器选择成分来制作特定三明治的人,机器将分配它。

So far I have created the ingredients and the different types of sandwiches using the decorator pattern:到目前为止,我已经使用装饰器模式创建了配料和不同类型的三明治:

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

    public String getDescription(){
        return description;
    }

    public double cost(){
        return 0.0;
    }
}

Each ingredient is modeled this this:每个成分都是这样建模的:

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

And further more, a concrete ingredient would be:此外,一个具体的成分是:

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();
    }
}

A specific type of a sandwich can be modeled like this:一种特定类型的三明治可以这样建模:

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

So a client would create a specific sandwich like this:因此,客户会像这样创建一个特定的三明治:

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

As a next step I will create a Dispenser object which will act as an automatic machine, which is pre-loaded with a specific number of ingredients (which are measured in generic units) and a user can press a button to choose one of the pre-set selections:作为下一步,我将创建一个 Dispenser 对象,该对象将充当自动机器,该对象预先加载了特定数量的成分(以通用单位衡量),用户可以按下按钮选择其中一个- 设置选择:

For example例如

  • BLT: 1 unit of tomato, 1 unit of lettuce, 1 unit bacon, 1 unit bread BLT:1 单位番茄、1 单位生菜、1 单位培根、1 单位面包
  • SUB: 1 unit meatballs, 1 unit cheese, 1 unit italian_sauce, 1 unit bread SUB:1个肉丸,1个奶酪,1个意大利酱,1个面包
  • etc..等等..

My Dispenser machine will come preloaded with a fixed number of units per ingredient我的分配器机器将预装每种成分的固定数量的单位

  • tomato: 10番茄:10
  • lettuce: 10生菜:10
  • bacon: 10培根:10
  • etc..等等..

And a list of buttons for the user to select a specific kind of sandwich:以及供用户选择特定类型三明治的按钮列表:

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

The idea is to keep track of the internal capacity of ingredients and be able to tell the user that, say, we don't have enough bacon left to make another BLT这个想法是跟踪成分的内部容量,并能够告诉用户,比如说,我们没有足够的培根来制作另一个 BLT

Now, my initial thought is do create the Dispenser object based on the state design pattern, but I have hit a problem trying to combine the objects of the Ingredient class with some kind of a storage within the Dispenser class.现在,我最初的想法是根据状态设计模式创建 Dispenser 对象,但是我在尝试将 Ingredient 类的对象与 Dispenser 类中的某种存储组合时遇到了问题。 At first I though a map with name/value pairs the ingredient type/ingredient quantity.起初,我通过带有名称/值的地图将成分类型/成分数量配对。 But I am not sure how to combine those patterns together so I can decrement automatically after every use.但我不确定如何将这些模式组合在一起,以便在每次使用后自动递减。

Do you perhaps have a general idea on how to go ahead and implement such a concept?您是否对如何继续实施这样的概念有一个大致的想法? First of all am I on the right track with decorator and state patterns?首先,我在装饰器和状态模式的正确轨道上吗? Would there be a more efficient approach?会有更有效的方法吗? I hope I have explained the problem clearly.我希望我已经清楚地解释了这个问题。

Thank you for any direction, I appreciate any thoughts感谢您的任何指导,我感谢您的任何想法

The Sandwich to Cheese is "has-a" relation, so Sandwich should never be a parent of Cheese. Sandwich 到 Cheese 是“has-a”关系,所以 Sandwich 永远不应该是 Cheese 的父级。

Not sure what you are doing at this line:不确定你在这条线上做什么:

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

Logically speaking, why would you create a Tomato object and passing it a Lettuce?从逻辑上讲,为什么要创建一个 Tomato 对象并将 Lettuce 传递给它? Tomato, Lettuce .... etc should extend Ingredient.番茄、生菜......等应该扩展成分。

I would make it like this我会让它像这样

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

Inside each ingredient class, i would put a static variable, in Tomato, would call it tomatoCount, then initialize it when creating the Dispenser, each time a new Tomato is created would decrement it.在每个成分类中,我会在 Tomato 中放置一个静态变量,将其命名为 TomatoCount,然后在创建 Dispenser 时对其进行初始化,每次创建新的 Tomato 时都会对其进行递减。 If it reach zero then Tomato class would complain如果它达到零,那么番茄班会抱怨

  1. Ingredient is not IS-A Sandwich;成分不是 IS-A 三明治;
  2. It's better to externalize ingredient prices in order to allow their flexible change;最好将原料价格外部化,以允许其灵活变化;
  3. It's better to generate sandwich description in runtime based on its ingredients instead of hardcoding it at class level;最好在运行时根据其成分生成三明治描述,而不是在类级别对其进行硬编码;
  4. Ingredients should know nothing about sandwiches;配料应该对三明治一无所知;

So, I'd offer the following solution:所以,我会提供以下解决方案:

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;
 }
}

The decorator pattern is not appropriate to your problem.装饰器模式不适合您的问题。 An Ingredient does not add new behaviour to a Sandwich, never mind that linking a Sandwich and a (sandwich) Ingredient in a is-a relationship is already a tad contrived.成分不会为三明治添加新的行为,更不用说在is-a关系中将三明治和(三明治)成分联系起来已经有点人为。 (Nested instantiation only looks cool until you have to do it dynamically.) (嵌套实例化看起来很酷,直到您必须动态执行它。)

A Sandwich has Ingredients/Fillings/Condiments.三明治有配料/馅料/调味品。 Establish a class hierarchy for the Ingredients and fold them together with the Sandwich using the Composite Pattern.为成分建立一个类层次结构,并使用复合模式将它们与三明治折叠在一起。

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();
            ....
        }
    }
}

Too, I do not think the State pattern is useful for the Dispenser as it relates to the management of Ingredients or Sandwiches.同样,我不认为状态模式对分配器有用,因为它与配料或三明治的管理有关。 The pattern prescribes the internal use of objects to change the behaviour of a class.该模式规定了在内部使用对象来改变类的行为。 But the DIspenser does not need polymorphic behaviour based on state:但是 DIspenser 不需要基于状态的多态行为:

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

eg, the Dispenser does not have significant variance in internal state that necessitates polymorphic behaviour for its public interface.例如,Dispenser 的内部状态没有显着变化,因此其公共接口需要多态行为。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM