Yianna Kokalas

Design Patterns Decorator

05 Oct 2017

A very handy pattern is the decorator pattern because it simply wraps an object and delegates all responsibilities to that object. It will also add onto that object without mutating the original object, it only decorates it.

Imagine we have a doughnut which is going to have a base cost and a base description. Then we define decorators to wrap this doughnut for every topping we add onto it. Thus adding onto it’s description and cost.

Defining our strategy

Let’s define a strategy for our doughnuts to be, well… doughnuts! We are going to need only a couple of methods, cost and description. If you’re not familiar with the Strategy pattern checkout one of my earlier posts on it. It’s a way of enforcing a specified strategy onto other objects.

module Doughnut
  def cost
    raise NotImplementedError, "Hey, implement a method body for instance method cost."
  end

  def description
    raise NotImplementedError, "Hey, implement a method body for instance method description."
  end
end

class ChocolateDoughnut
  include Doughnut
end

class FrenchCruller
  include Doughnut
end

class PlainDoughnut
  include Doughnut
end


ChocolateDoughnut.new.cost

If we run the above code we will get a NotImplementedError. This is good because it will force us to define a specific base cost and description.

 ruby doughnuts.rb
doughnuts.rb:3:in `cost': Hey, implement a method body for instance method cost. (NotImplementedError)
	from doughnuts.rb:24:in `<main>'

Add a base cost and description

Great, now that we have some errors guiding us let’s add the base cost and description.

class ChocolateDoughnut
  include Doughnut

  def cost
    0.99
  end

  def description
    "Chocolate Doughnut"
  end

end

class FrenchCruller
  include Doughnut

  def cost
    1.25
  end

  def description
    "French Cruller"
  end

end

class PlainDoughnut
  include Doughnut

  def cost
    0.79
  end

  def description
    "Plain Doughnut"
  end

end

Creating a Doughnut Decorator and some toppings

In order for us to decorate objects we need to start with a decorator parent class for the toppings. The job of a decorator is to delegate the cost and description calls to the doughnut object. This is why a strategy is important! If we didn’t have a strategy then we could possibly get a NoMethodError for cost and description. Now we know if we import the Doughnut module and defined the expected methods then everything will work out fine.

class DoughnutDecorator
  attr_reader :doughnut

  def initialize(doughnut)
    @doughnut = doughnut
  end

  def cost
    doughnut.cost
  end

  def description
    doughnut.description
  end

end

Now that we have our DoughnutDecorator lets start by making some toppings. More specifically let’s make frosting and sugar decorators. In order to carry over the overridden behavior we will call super and add onto that.


class WithChocolateFrosting < DoughnutDecorator
  def cost
    super + 0.50
  end

  def description
    super + " with chocolate frosting"
  end

end

class WithSugarGlaze < DoughnutDecorator
  def cost
    super + 0.40
  end

  def description
    super + " with sugar glaze"
  end

end

class WithSugar < DoughnutDecorator
  def cost
    super + 0.50
  end

  def description
    super + " with sugar"
  end

end

chocolate_doughnut = ChocolateDoughnut.new
french_cruller = FrenchCruller.new
plain_doughnut = PlainDoughnut.new

puts "A #{chocolate_doughnut.description} costs #{chocolate_doughnut.cost}."
puts "A #{french_cruller.description} costs #{french_cruller.cost}."
puts "A #{plain_doughnut.description} costs #{plain_doughnut.cost}."

chocolate_doughnut_with_chocolate_frosting = WithChocolateFrosting.new(chocolate_doughnut)
french_cruller_with_sugar_glaze = WithSugarGlaze.new(french_cruller)
plain_doughnut_with_sugar = WithSugar.new(plain_doughnut)

puts "A #{chocolate_doughnut_with_chocolate_frosting.description} costs
#{chocolate_doughnut_with_chocolate_frosting.cost}.".gsub("\n", "")

puts "A #{french_cruller_with_sugar_glaze.description} costs
#{french_cruller_with_sugar_glaze.cost}.".gsub("\n", "")

puts "A #{plain_doughnut_with_sugar.description} costs
#{plain_doughnut_with_sugar.cost}.".gsub("\n", "")

Now when we run our code we get…

♥ ruby doughnuts.rb
A Chocolate Doughnut costs 0.99.
A French Cruller costs 1.25.
A Plain Doughnut costs 0.79.
A Chocolate Doughnut with chocolate frosting costs 1.49.
A French Cruller with sugar glaze costs 1.65.
A Plain Doughnut with sugar costs 1.29.

Awesome, as you can see we have just added onto the doughnut objects and have not changed them in any kind of way. Let’s finish off with a final decorator called AndSprinkles.

...

class AndSprinkles < DoughnutDecorator
  def cost
    super + 0.25
  end

  def description
    super + " and sprinkles"
  end

end
...

puts "A #{plain_doughnut_with_chocolate_frosting_and_sprinkles.description} costs
#{plain_doughnut_with_chocolate_frosting_and_sprinkles.cost}.".gsub("\n", "")

Great, now when we run our code this is what we get.

♥ ruby doughnuts.rb
A Chocolate Doughnut costs 0.99.
A French Cruller costs 1.25.
A Plain Doughnut costs 0.79.
A Chocolate Doughnut with chocolate frosting costs 1.49.
A French Cruller with sugar glaze costs 1.65.
A Plain Doughnut with sugar costs 1.29.
A Plain Doughnut with chocolate frosting and sprinkles costs 1.54.

As you can see with decorators we can have many different combinations of doughnuts and toppings. The idea remains the same, a decorator will delegate all responsibility to it’s wrapped object. Thus you can keep wrapping with as many decorators as you choose.

Tweet me @yonk_nyc if you like this post.

Tweet