Sonntag, 8. Februar 2015

Go with the Strategy pattern in Ruby!

Solving varying algorithms with the Template Method pattern is reasonable as long as inheritance is reasonable. Since inheritance also comes with some drawbacks, it should be avoided if possible. Especially when it comes to decoupling classes: prefer delegation.
The Strategy pattern also solves varying algorithms but takes advantage of delegation.
In a nutshell, the varying algorithms are extracted into several classes, which define the so called Strategy objects. Each object has the same named API message receiver, but follows a different strategy how to achieve its goal. Apart from the Strategy objects, there is the particular Context object. It is the context in which the strategy works.
The example Context class:
class Person
  attr_reader :id, :resource_name
  attr_accessor :strategy

  def initialize strategy
    @id = 1 
    @resource_name = 'people'
    @strategy = strategy
  end 

  def request
    strategy.query self
  end 
end
is characterized by some (hard coded for example reasons) values: @id and @resource_name. Furthermore the Person class is a proxy and its objects are persisted somewhere else. They can be requested via different kind of webservices, the different strategies. Adding 2 types of webservices as strategies:
require 'uri'
require 'net/http'
class WebserviceRequest
  attr_reader :domain

  def initialize domain
    uri = URI.parse domain
    @http = Net::HTTP.new uri.host, uri.port
    @http.use_ssl = false
  end 

  def query context
    raise "#{self.class}#query not yet implemented."
  end 
end

class SoapRequest < WebserviceRequest
  def query context
    @http.post "/#{context.resource_name}", 
      soap_body(context) { |c|
          "<m:GetPerson>
            <m:Id>#{c.id}</m:Id>
          </m:GetPerson>"
      }, { 'Host' => @http.address, 'Content-Type' => 'text/xml' }
  end 
private
  def soap_body context, &block
    "<?xml version='1.0'?>
      <soap:Envelope xmlns:soap='http://www.w3.org/2001/12/soap-envelope' soap:encodingStyle='http://www.w3.org/2001/12/soap-encoding'>
        <soap:Body xmlns:m='http://#{@http.address}/#{context.resource_name}'>
          #{yield(context)}
        </soap:Body>
      </soap:Envelope>"
  end
end

class RestRequest < WebserviceRequest
  def query context
    @http.get "/#{context.resource_name}/#{context.id}"
  end
end
There is a SOAP (ugh!) and a REST strategy. Both expect a domain address to be requested:
Person.new(RestRequest.new 'http://localhost').request
=> #<Net::HTTPOK 200 OK readbody=true>
Person.new(SoapRequest.new 'http://localhost').request
=> #<Net::HTTPNotFound 404 Not Found readbody=true>
Both requests send a HTTP request to localhost on port 80 with some parameters. Apparently the SOAP query was not the right strategy in the Person context.
But the pattern is obvious: the person Context object has a Strategy object and sends the query message to it with itself as the parameter. The method itself is the implementation detail, the varying part.
Strategies can be added as many as required. For example a Strategy to query a database:
class SqlRequest
  attr_reader :connection

  def initialize connection
    @connection = connection
  end
  
  def query context
    @connection.execute "SELECT * FROM #{context.resource_name} WHERE ID = #{context.id};"
  end
end
and
Person.new(SqlRequest.new database_connection).request
=> SELECT * FROM people WHERE ID = 1;
The advantage is the complete class decoupling. It only relies on duck typing. Any class can be a Strategy as long as it has the right interface.
Further articles of interest:

Supported by Ruby 2.1.1

Keine Kommentare:

Kommentar veröffentlichen