In this case I have gone for some psuedo Composite pattern and the cost logic is altogether in a different class. Class diagram is as below :

So every thing that goes in Coffee is basically a CoffeeComponent
hence the interface. It has three main methods getSize()
, getName()
and contributionToCost()
. Note that contributionToCost()
method takes a parameter PricingService
, this is because the cost logic is with Pricing Service. AbstractCoffeeComponent
is implementing the CoffeeComponent
interface to provide common methods' implementation. From AbstractCoffeeComponent
two classes are subclass : Type
and Ingredient
. So espress, mocha will refer to Type
class and sugar, milk will be Ingredient
instances. Idea is that PricingService has a lookup method which can give the cost on the basis of CoffeeComponent
's name
and size
. Rest of the hierarchy is just to showcase how coffee concept can be coded. Below is the code for PricingService
, Client
and Coffee
.
PricingService.java
package coffee;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
/**
* Pricing ideally would be querying database for costs but here HashMap will do
*/
public class PricingService {
private static final class CostComponent {
private String componentName;
private Size componentSize;
public CostComponent(String name, Size size) {
this.componentName = name;
this.componentSize = size;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
CostComponent costComponent1 = (CostComponent) o;
return componentName.equalsIgnoreCase(costComponent1.componentName) &&
componentSize == costComponent1.componentSize;
}
@Override
public int hashCode() {
return Objects.hash(componentName, componentSize);
}
}
private final static Map<CostComponent, Double> costs = new HashMap<>();
static {
costs.put(new CostComponent("Espresso", Size.TALL), 10.0);
costs.put(new CostComponent("Rum", Size.TALL), 40.0);
}
public double costOf(CoffeeComponent component){
return costs.getOrDefault(new CostComponent(component.getName(), component.getSize()),0.0);
}
}
Coffee.java
package coffee;
import java.util.HashSet;
import java.util.Set;
/**
* Container class - because we want coffee
*/
public class Coffee {
private final Type type;
private Set<Ingredient> ingredients;
public Coffee(Type type) {
this.type = type;
this.ingredients = new HashSet<>();
this.ingredients.addAll(type.getDefaultIngredients());
}
public void addIngredient(Ingredient ingredient){
this.ingredients.add(ingredient);
}
/**
* Propagate size to all igredients and type
*/
public void setSize(Size size){
for(Ingredient ingredient: ingredients){
ingredient.setSize(size);
}
type.setSize(size);
}
/**
* Aggregate of all pieces for cost
*/
public double cost(PricingService pricingService){
double cost = 0.0;
for(Ingredient ingredient: ingredients){
cost+=ingredient.contributionToCost(pricingService);
}
cost+=type.contributionToCost(pricingService);
return cost;
}
}
Client.java
package coffee;
public class Client {
public static void main(String[] args) {
PricingService starbuzz = new PricingService();
//Order an Espresso
Coffee espresso = new Coffee(new Type("Espresso"));
/**
* Here new Type, new Ingredient is done, ideally this will also be some sort of lookup using Factory pattern
*/
//add sugar
espresso.addIngredient(new Ingredient("Sugar"));
//add rum :)
espresso.addIngredient(new Ingredient("Rum"));
//make it large
espresso.setSize(Size.TALL);
double cost = espresso.cost(starbuzz);
System.out.println("Cost : " + cost);
}
}
CoffeeComponent.java
package coffee;
public interface CoffeeComponent {
String getName();
void setSize(Size size);
/**
* Default lookup, implementations can override if wanted
*/
default double contributionToCost(PricingService pricingService){
return pricingService.costOf(this);
}
/**
* By default coffee is normal unless explicitly stated
*/
default Size getSize(){
return Size.NORMAL;
}
}
Ingredient.java
package coffee;
/**
* Sugar, milk, soy etc come here
*/
public class Ingredient extends AbstractCoffeeComponent {
public Ingredient(String name) {
super(name);
}
}
Type.java
package coffee;
import java.util.Collections;
import java.util.Set;
/**
* Espresso, Mocha etc types come here
*/
public class Type extends AbstractCoffeeComponent {
public Type(String name) {
super(name);
}
/**
* Default ingredients that come with this type
*/
public Set<Ingredient> getDefaultIngredients(){
return Collections.emptySet();
}
}
AbstractCoffeeComponent.java
package coffee;
public abstract class AbstractCoffeeComponent implements CoffeeComponent {
protected final String name;
protected Size size;
public AbstractCoffeeComponent(String name) {
this.name = name;
}
@Override
public String getName() {
return this.name;
}
@Override
public void setSize(Size size) {
this.size = size;
}
@Override
public Size getSize() {
return size;
}
}
As you can see if you need to add any more types, ingredients, size just add more rows in PricingService
's Hashmap
Hope this helps. Although I agree with @bcperth that the example was to show pros and cons of Decorator pattern.