Ruby - Strategy Pattern

Subscribe to receive new articles. No spam. Quality content.

Hi, today we will continue learning new patterns (see also: decorator and template). This time we will go through very popular pattern - Strategy.

Wikipedia describes pattern this way:

The strategy pattern (also known as the policy pattern) is a software design pattern that enables an algorithm's behavior to be selected at runtime.

The strategy pattern:

  • defines a family of algorithms,
  • encapsulates each algorithm, and
  • makes the algorithms interchangeable within that family.

That's too general description, so I suggest to go to examples straight away.

Example #1

Let's create method which calculates net salary (salary you have after all taxes paid). Different countries have different rules for taxes, so code could look like this:

def net_salary(amount, country)
  taxes = case country
    when "Ukraine"
      (amount * 0.05) + 313    
    when "U.S."
      (amount * 0.2) + 100
    when "Poland"  
      amount * 0.3
    else
      0
    end

  amount - taxes
end

net_salary(1000, "Poland")  # => 700.0
net_salary(1000, "Ukraine")  # => 637.0

As we see from example - each country has own rules (strategies) of calculation. I've simplified logic of calculation, but actual implementation of each rule could contain a lot of code which would make net_salary method huge.

The main idea of Strategy pattern - to define set of objects (strategies) which solve the same problem in different way, depending on conditions. In our case they calculate amount of taxes differently, depending on selected country. All strategies should have the same interface to be interchangeable.

Let's rewrite example implementing strategy pattern:

class UkraineTaxes
  def self.taxes(amount)
    (amount * 0.05) + 313
  end
end

class PolandTaxes
  def self.taxes(amount)
    amount * 0.3
  end
end

class UsTaxes
  def self.taxes(amount)
    (amount * 0.2) + 100
  end
end

class Taxes
  def initialize
    @strategies = {}
    @strategies['Ukraine'] = UkraineTaxes
    @strategies['Poland'] = PolandTaxes
    @strategies['U.S.'] = UsTaxes
  end

  def net_salary(amount, country)
    strategy = @strategies[country]

    strategy ? amount - strategy.taxes(amount) : amount
  end
end

Taxes.new.net_salary(1000, "U.S.") # => 700.0

For each country we've created separate class which has method .taxes. Method .taxes contains all logic which calculates amount of taxes.

In Taxes#initialize we define a list of available strategies. Method net_salary dynamically selects proper strategy basing on country param. It will return amount If there is no applicable strategy.

Implementation of each strategy takes only one line of code, so we can avoid creating separate classes for that. We can use lambdas. That will make our code shorter:

class Taxes
  attr_reader :amount

  def initialize(amount)
    @amount = amount

    @strategies = {}
    @strategies['Ukraine'] = -> { (amount * 0.05) + 313 }
    @strategies['Poland'] = -> { amount * 0.3 }
    @strategies['U.S.'] = -> { (amount * 0.2) + 100 }        
  end

  def net_salary(country)
    strategy = @strategies[country]

    strategy ? amount - strategy.call : amount
  end

end

Taxes.new(1200).net_salary("U.S.") # => 700.0

If you didn't use lambdas yet - don't worry. To simplify: lambdas are bits of code which we can assign to variables and execute them later (strategy.call). Also it's good to remember that lambdas execute width scope they've been defined.

This approach allows us to decouple implementation of each strategy from place where it might be used. Polymorphism helps us a lot here as well. Each strategy supports the same interface so we can call strategy.call even if we don't know which exact strategy will be executed.

I believe that simple examples allow us to develop skill of using patterns properly. Let's move forward and check another example.

Example #2

In this example we receive data from external API and depending on response decide how to process it.

Response is an object which has methods status and data. status can be: success, error or fail. If status is success - we can use response.data. If returned status is error - we should display response.error_message. The fail status means that service is down and not responding at all.

def show
  response = external_api.get(params[:id])

  return handle_error if response.status == "error"
  return handle_fail if response.status == "fail"

  if response.status == "success"
    "Successful response: #{response.data}"
  end
end

def handle_error
  puts "Error: #{response.error_message}"
end

def handle_fail
  puts "Request Failed"
end

I've simplified that code, but the problem is obvious. We have response object and should decide how to process it. In this example we do that using a lot of if..else statements which is not good. Imagine how much code we will get if each action will have all these if's.

It's a good place to use strategy pattern. We know that logic of processing should be extracted into classes or lambdas that have the same interface.

It's easy to decide which strategy to run because we have response.status. Let's implement that:

class ResponseHandler
  def self.handle(response, strategies)
    strategies[response.status.to_sym].call
  end
end

def show
  response = external_service.get(params[:id])

  on_success = -> { "Successful response: #{response.data}" }
  on_error = -> { "Error: #{response.error_message}" }
  on_fail = -> { "Request Failed" }

  ResponseHandler.handle(response, success: on_success, error: on_error, fail: on_fail)
end

We did following steps:

  1. Created class ResponseHandler which will handle responses from external API
  2. Also, ResponseHandler accepts strategies which we want to run depending on status of response.
  3. All strategies have the same interface and respond to .call method.
  4. Having strategies with the same interface, response object and ResponseHandler class - we can easily run proper strategy: strategies[response.status.to_sym].call

I think that's a good code. ResponseHandler can be extended by default strategies, for example for on_fail. So we don't need to pass the same strategy for failed request every time.

If we have more complex logic - it will be easy to extract strategies into separate classes and get rid from lambdas. If classes support .call method - everything will work.

Also you see how easy to add new strategies. Just create new strategy class or lambda for corresponding response status and ResponseHandler will use that automatically. Which is good because it's an Open/Closed principle from SOLID.

If you see that your code contains a lot of if..else or case..when, check that code twice, there might be a chance that you're missing Strategy pattern there.

Cheers!

Subscribe to receive new articles. No spam. Quality content.

Comments