Menu
Coddy logo textTech

Decorator Pattern

Part of the Object Oriented Programming section of Coddy's Ruby journey. Lesson 47 of 57.

The decorator pattern wraps an object in another object with the same interface, which adds something before or after passing the call on. Decorators can be stacked in any order:

class Coffee
  def cost
    2.0
  end

  def description
    "coffee"
  end
end

class Milk
  def initialize(drink)
    @drink = drink
  end

  def cost
    @drink.cost + 0.5
  end

  def description
    "#{@drink.description}, milk"
  end
end

order = Milk.new(Milk.new(Coffee.new))
puts format("%s: %.2f", order.description, order.cost)

Output:

coffee, milk, milk: 3.00

Every decorator stores the wrapped object and forwards to it, so a shared base class can hold that part. Each concrete decorator then overrides only what it changes:

class Coffee
  def cost
    2.0
  end

  def description
    "coffee"
  end
end

class Extra
  def initialize(drink)
    @drink = drink
  end

  def cost
    @drink.cost
  end

  def description
    @drink.description
  end
end

class Sugar < Extra
  def cost
    super + 0.2
  end

  def description
    super + ", sugar"
  end
end

order = Sugar.new(Coffee.new)
puts format("%s: %.2f", order.description, order.cost)

Output:

coffee, sugar: 2.20

The standard library's SimpleDelegator (require 'delegate') forwards every method it does not define to the wrapped object, which keeps decorators short when the interface is large:

require 'delegate'

class Loud < SimpleDelegator
  def to_s
    super.upcase + "!"
  end
end

text = Loud.new("hello")
puts text
puts text.size
puts text.start_with?("he")

Output:

HELLO!
5
true

Decorators add behaviour to single objects at run time and can be combined freely. With subclasses, every combination such as MilkSugarCoffee would need a class of its own:

Sugar.new(Milk.new(Coffee.new))      # any combination, any order
Whip.new(Sugar.new(Coffee.new))

# with subclasses: MilkCoffee, SugarCoffee, MilkSugarCoffee, ...
challenge icon

Challenge

Easy

Complete the coffee decorators. Coffee costs 2.00 and is described as coffee. Milk adds 0.50 and , milk, Sugar adds 0.20 and , sugar, and Whip adds 0.70 and , whip. The three decorators share the base class Extra, which stores the wrapped drink and forwards cost and description; each subclass uses super.

Each input line is an order such as milk sugar. The supplied code wraps a new Coffee in the decorators in that order and prints coffee, milk, sugar: 2.70.

Your code goes in coffee.rb, extra.rb, milk.rb, sugar.rb and whip.rb. main.rb holds the supplied input/output code and cannot be edited.

Try it yourself

require_relative 'coffee'
require_relative 'extra'
require_relative 'milk'
require_relative 'sugar'
require_relative 'whip'

# Supplied input/output code: keep it as it is
input = []
while (line = gets)
  input << line.chomp
end
extras = { "milk" => Milk, "sugar" => Sugar, "whip" => Whip }
input.each do |line|
  drink = line.split.reduce(Coffee.new) { |d, name| extras[name].new(d) }
  puts format("%s: %.2f", drink.description, drink.cost)
end
quiz iconTest yourself

This lesson includes a short quiz. Start the lesson to answer it and track your progress.

All lessons in Object Oriented Programming

Practice on your own: Online Ruby compiler