Menu
Coddy logo textTech

Strategy Pattern

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

The strategy pattern puts one part of an algorithm into its own object and lets the main object delegate to it. Swapping the strategy changes the behaviour without changing the main class:

class Standard
  def cost(kg)
    5 + kg * 1.5
  end
end

class Express
  def cost(kg)
    15 + kg * 3
  end
end

class Shipment
  attr_writer :strategy

  def initialize(kg, strategy)
    @kg = kg
    @strategy = strategy
  end

  def cost
    @strategy.cost(@kg)
  end
end

s = Shipment.new(2, Standard.new)
puts s.cost
s.strategy = Express.new
puts s.cost

Output:

8.0
21

The strategies share only a method name, so any object with cost works: duck typing again. Adding a new way to ship means adding one class, not another branch inside Shipment:

class Pickup
  def cost(_kg)
    0
  end
end

shipment.strategy = Pickup.new     # Shipment is unchanged

In Ruby a strategy is often a lambda. Anything that responds to call can be stored and invoked, whether it is a lambda, a method object or an object of your own class:

class TextFormatter
  def initialize(strategy)
    @strategy = strategy
  end

  def format_all(words)
    words.map { |w| @strategy.call(w) }.join(" ")
  end
end

class Censor
  def call(word)
    word.size > 3 ? "*" * word.size : word
  end
end

words = %w[ruby is great]
puts TextFormatter.new(->(w) { w.upcase }).format_all(words)
puts TextFormatter.new(:capitalize.to_proc).format_all(words)
puts TextFormatter.new(Censor.new).format_all(words)

Output:

RUBY IS GREAT
Ruby Is Great
**** is *****

Choose strategy over subclasses when one object must change its behaviour while the program runs, or when the variation is a single step. A subclass fixes the behaviour for the object's whole life:

# subclassing: fixed at creation
ExpressShipment.new(2)

# strategy: can change later
shipment = Shipment.new(2, Standard.new)
shipment.strategy = Express.new
challenge icon

Challenge

Easy

Complete the shipping strategies and ShippingCalculator. Standard#cost(kg) is 5 plus 1.50 per kg, Express#cost(kg) is 15 plus 3 per kg, and Pickup#cost(kg) is 0. The calculator is created with a strategy, can change it with strategy=, and quote(kg) returns 2.5 kg by Express: 22.50, using the strategy's class name.

The supplied code starts with Standard and reads commands use express, use pickup, use standard or quote 2.5, printing every quote.

Your code goes in standard.rb, express.rb, pickup.rb and shipping_calculator.rb. main.rb holds the supplied input/output code and cannot be edited.

Try it yourself

require_relative 'standard'
require_relative 'express'
require_relative 'pickup'
require_relative 'shipping_calculator'

# Supplied input/output code: keep it as it is
input = []
while (line = gets)
  input << line.chomp
end
strategies = { "standard" => Standard, "express" => Express, "pickup" => Pickup }
calc = ShippingCalculator.new(Standard.new)
input.each do |line|
  cmd, arg = line.split
  if cmd == "use"
    calc.strategy = strategies[arg].new
  else
    puts calc.quote(arg.to_f)
  end
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