Today problem regards drinks; In particular our loved coffee will be at center of our attention.
We have two Coffee classes, Decaf and Espresso. Two types of Condiments can be added to the coffee, Milk and Sugar.
class
Decaf
def description "decaffinated coffee" end def cost 2.0 end end class
Espresso
def description "expresso coffee" end def cost 1.5 end end class
Milk
def description "milk" end def cost 0.50 end end class
Sugar
def description "sugar" end def cost 0.20 end end
The class DecafWithMilkAndSugar has been created for making a Decaf with Milk and Sugar as the name says.
class DecafWithMilkAndSugar
def initialize
@coffee = Decaf.new
@milk = Milk.new
@sugar = Sugar.new
end
def description
"#{@coffee.description} #{@milk.description} #{@sugar.description}"
end
def cost
@coffee.cost + @milk.cost + @sugar.cost
end
end
my_coffee = DecafWithMilkAndSugar.new
puts "My coffee is:"
puts my_coffee.description
puts "and costs:"
puts my_coffee.cost
And we are happy it works:
My coffee is:
decaffinated coffee milk sugar
and costs:
2.7
However we are concerned at the 5 other combination classes you still have to write. We also hear that more Coffee types are on the way (DarkRoast and HouseBlend) and further condiments are possible (Syrup, Sweetener, Soy). This kind of code obviously is not capable to accommodate this new requirements in any elegant way. In fact we would have to create an exponential number of classes for all the possible combination of coffees and condiments.In this scenario we can refactor this code to apply the Decorator pattern;The decorator pattern can be used to extend (decorate) the functionality of a certain object statically, or in some cases at run-time, independently of other instances of the same class, provided some groundwork is done at design time. This is achieved by designing a new decorator class that wraps the original class. This wrapping could be achieved by the following sequence of steps:
- Subclass the original "Component" class into a "Decorator" class (see UML diagram);
- In the Decorator class, add a Component pointer as a field;
- Pass a Component to the Decorator constructor to initialize the Component pointer;
- In the Decorator class, redirect all "Component" methods to the "Component" pointer;
- In the ConcreteDecorator class, override any Component method(s) whose behavior needs to be modified.
This is the typical class diagram structure for an application using the decorator pattern:
We start from the main component which is the class Coffee:
class Coffee
def description
puts "abstract method"
end
def cost
puts "abstract method"
end
end
This would be an abstract class, but as we know there is not such concept in Ruby, so we just implement the methods of this class with a default behavior.
As the original description of the problem states, we have two types of coffees, Decaf and Espresso. Let's create two classes for modelling these entities:
class Decaf < Coffee
def description
"decaffinated coffee"
end
def cost
2.0
end
end
class Espresso < Coffee
def description
"expresso coffee"
end
def cost
1.5
end
end
Both classes extend the parent class Coffee and override its method with a concrete and specific implementation.
Let's now talk about the decorator class. We are going to call this class CoffeeDecorator and make it extend Coffee:
require_relative 'coffee.rb'
class CoffeeDecorator < Coffee
attr_reader :coffee
def initialize(coffee)
@coffee = coffee
end
def description
@coffee.description + " | "
end
def cost
@coffee.cost
end
end
Now we need two more classes for the condiment, milk and sugar, that extend the decorator class:
require_relative 'coffee.rb'
class Milk < CoffeeDecorator
def initialize(coffee)
super(coffee)
end
def description
super + " with milk"
end
def cost
super + 0.50
end
end
class Sugar < CoffeeDecorator
def initialize(coffee)
super(coffee)
end
def description
super + " with sugar"
end
def cost
super + 0.20
end
end
You can see that all these three classes take a coffee as a constructor argument and define new behaviour for the methods they implement. This is the mechanism we are going to use for decorating each coffee. The main application now can create any kind of coffee with any condiment, simply chaining the constructors:
require_relative 'coffee.rb'
require_relative 'coffee_decorator.rb'
coffeeWithMilkAndSugar = Milk.new(Sugar.new(Decaf.new))
puts "My coffee is:"
puts coffeeWithMilkAndSugar.description
puts "and costs:"
puts coffeeWithMilkAndSugar.cost
This will still print something like this:
My coffee is:
decaffinated coffee | with sugar | with milk
and costs:
2.7
You can the code became extremely flexible. Any coffee composed by any combination of condiments can be created in the same way. What if the bar wants to make a new promotion and offer some RandomCoffee ? Here the code:
And here the output:
require_relative 'coffee.rb'
require_relative 'coffee_decorator.rb'
class RandomCoffee
def initialize
num = Kernel.rand(1..4)
if num == 1
@coffee = Milk.new(Sugar.new(Decaf.new))
end
if num == 2
@coffee = Sugar.new(Milk.new(Decaf.new))
end
if num == 3
@coffee = Sugar.new(Espresso.new)
end
if num == 4
@coffee = Sugar.new(Milk.new(Espresso.new))
end
end
def description
@coffee.description
end
def cost
@coffee.cost
end
end
randomCoffee = RandomCoffee.new
puts "My coffee is:"
puts randomCoffee.description
puts "and costs:"
puts randomCoffee.cost
And here a possible output:
My coffee is:
decaffinated coffee | with milk | with sugar
and costs:
2.7
The latest piece show how is to decorate a coffee now.
The full class diagram for the application is reported below:
HAPPY CODING!
LUCA