Tuesday, 27 January 2015

DESIGN PATTERN BY EXAMPLE (IN RUBY) - THE DECORATOR PATTERN

In my previous article, I have shown you a practical application of the State Pattern in Ruby.

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:



So let's implement this pattern in our application.
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

No comments:

Post a Comment