views:

406

answers:

3

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

+3  A: 

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

medopal
So, the Sandwich objectt would take a variable number of arguments to its constructor right?
denchr
yea, using the ... (Java 5) you can add 0 or more variables, and thats make sense in your program, for example you can say:s1 = new Sandwich(Tomato,Lettuce,Sauce);s2 = new Sandwich(Tomato);s3 = new Sandwich();
medopal
A: 

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.

Noel Ang
Thanks for the example. Would you still favor a static counter to ensure ingredient supply is enough to make or not make a particular sandwich?
denchr
I would imagine the counting should be a concern of the dispenser not the ingredient class, right?
denchr
I would agree that quantity is a concern external to an Ingredient object, yes.For the quantity tracking concern, I'm thinking that concurrency is not a concern (one dispenser to one user), nor is speed, so some sort of Map<Class<Ingredient>, Integer> would be sufficient.
Noel Ang
+1  A: 
  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;
 }
}
denis.zhdanov
I am not sure why we would need a concurrent structure. We assume that the dispenser is like a soda machine, with selection buttons and one output for the can to come out. I mean there would be one user every time. Doesn't concurrency add overhead in this case?
denchr
One user doesn't mean one thread :) Also you can't be sure that your requirements are not changed at the future. Anyway the overhead is too negligible here to consider especially if you use java6.
denis.zhdanov
Thanks for the response. Just wondering: using enums for ingredients and sandwich types, isn't that making hard to extend in case, like you said a chance in the requirements, we need to add more ingredients or offer more types of sandwiches?
denchr
I mean, I am not sure the advantage of using enums in this way, rather than creating an Ingredient abstract class and have all different ingredients extend from it. The same for Sandwiches
denchr
Both ways are acceptable. Enums define more strict contract to allowed objects. I mean that if you have a standalone class (PriceList in my example) you can reference all declared ingrediant types. If you have Ingredient as an abstract class/interface you have no idea on what particular implementations are used.
denis.zhdanov
I think your solution is quite neat actually. On one hand having multiple Sandwich type objects and Ingredient object which extend/implement and abstract class or an interface adds to clutter but gives more flexibility. On the other hand using enums keeps the number of classes down, but require to change the Ingredient and Sandwichtype classes, by adding more enums, in case we need to extend.. Is this a correct way to see the tradeoffs here?
denchr
hey Denis, I was wondering, could you perhaps please explain a little bit how the SandwichType constructor works, I am a bit unclear how the increment of i happens. Also, the getPrice() method in the PriceList class? -- I am seeing some advanced use of Enums and Collections and trying to catch up :)
denchr