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

发布于 2024-08-11 05:11:15 字数 1935 浏览 2 评论 0原文

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

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

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

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

例如

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

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

  • 番茄:10
  • 生菜:10
  • 培根:10
  • 等。

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

  • 1-BLT
  • 2-SUB
  • 3-BBQ
  • ..etc

这个想法是跟踪成分的内部容量,并能够告诉用户,比如说,我们

现在 没有足够的培根来制作另一个 BLT ,我最初的想法是基于状态设计模式创建 Dispenser 对象,但我在尝试将 Ingredient 类的对象与 Dispenser 类中的某种存储结合起来时遇到了问题。首先,我通过一张带有名称/值的地图来配对成分类型/成分数量。但我不确定如何将这些模式组合在一起,以便我可以在每次使用后自动递减。

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

谢谢你的任何指导,我很感激任何想法

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:

For example

  • BLT: 1 unit of tomato, 1 unit of
    lettuce, 1 unit bacon, 1 unit bread
  • SUB: 1 unit meatballs, 1 unit cheese,
    1 unit italian_sauce, 1 unit bread
  • etc..

My Dispenser machine will come preloaded with a fixed number of units per ingredient

  • tomato: 10
  • lettuce: 10
  • bacon: 10
  • etc..

And a list of buttons for the user to select a specific kind of sandwich:

  • 1-BLT
  • 2-SUB
  • 3-BBQ
  • ..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

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. 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

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(3

御弟哥哥 2024-08-18 05:11:15

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

不确定您在这一行做什么:

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

从逻辑上讲,为什么要创建一个 Tomato 对象并向其传递一个 Lettuce?
番茄、生菜...等应添加成分。

我会像这样

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

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

The Sandwich to Cheese is "has-a" relation, so Sandwich should never be a parent of 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 .... 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. If it reach zero then Tomato class would complain

凉栀 2024-08-18 05:11:15
  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;
 }
}
  1. Ingredient is not IS-A Sandwich;
  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;
 }
}
同尘 2024-08-18 05:11:15

装饰器模式不适合您的问题。成分不会向三明治添加新行为,更不用说以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();
            ....
        }
    }
}

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

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

例如,分配器的内部状态没有显着变化,因此其公共接口需要多态行为。

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. (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:

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

e.g., the Dispenser does not have significant variance in internal state that necessitates polymorphic behaviour for its public interface.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文