English 中文(简体)
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:

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

最佳回答
  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;
 }
}
问题回答

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

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.





相关问题
Spring Properties File

Hi have this j2ee web application developed using spring framework. I have a problem with rendering mnessages in nihongo characters from the properties file. I tried converting the file to ascii using ...

Logging a global ID in multiple components

I have a system which contains multiple applications connected together using JMS and Spring Integration. Messages get sent along a chain of applications. [App A] -> [App B] -> [App C] We set a ...

Java Library Size

If I m given two Java Libraries in Jar format, 1 having no bells and whistles, and the other having lots of them that will mostly go unused.... my question is: How will the larger, mostly unused ...

How to get the Array Class for a given Class in Java?

I have a Class variable that holds a certain type and I need to get a variable that holds the corresponding array class. The best I could come up with is this: Class arrayOfFooClass = java.lang....

SQLite , Derby vs file system

I m working on a Java desktop application that reads and writes from/to different files. I think a better solution would be to replace the file system by a SQLite database. How hard is it to migrate ...

热门标签