← til

Refactoring with simple polymorphism

February 2, 2019
ruby

I'm in the process of writing a gem, and I've not been really focused on writing the best possible code, in order to build something in the least amount of time. I now have some time on my hands, so I've decided to try to make it clearer.

This is the code I've had:

Output = Struct.new(:body, :method, :headers, :url)
  def initialize
    self.method = 'GET'
    self.headers = {}
    self.url = ''
    self.body = ''
  end

  def sort_headers
    self.headers = headers.sort.to_h
  end

  def to_http
    output = "#{method} #{url} HTTP/1.1\n\n"

    headers.each do |key, value|
      output += "#{key}: #{value}\n"
    end

    output
  end
end

class Parser
  def parse(command, format:)
    self.output = Output.new

    output.sort_headers

    case format
    when :json
      output.to_h.to_json
    when :http
      output.to_http
    else
      output.to_h
    end
  end
end

What's itching me is how Output knows about the different formats this gem supports. What happens when another format is introduced?

I've decided to tackle this problem with some good ol' polymorphism.

Output = Struct.new(:body, :method, :headers, :url) do
  def initialize
    self.method = 'GET'
    self.headers = {}
    self.url = ''
    self.body = ''
  end

  def sort_headers
    self.headers = headers.sort.to_h
  end

  def convert
    raise NotImplementedError
  end
end

class HTTPOutput < Output
  def convert
    output = "#{method} #{url} HTTP/1.1\n"

    headers.each do |key, value|
      output += "#{key}: #{value}\n"
    end

    output += "\n#{body}"

    output
  end
end

class JSONOutput < Output
  def convert
    to_h.to_json
  end
end

class HashOutput < Output
  def convert
    to_h
  end
end

class Parser
  def self.parse(command, format: nil)
    self.output = create_output(format)

    output.sort_headers
    output.convert
  end

  private

  OUTPUTS = {
    http: HTTPOutput,
    json: JSONOutput,
    hash: HashOutput
  }.freeze

  def create_output(format)
    OUTPUTS.fetch(format) { HashOutput }.new
  end
end

Notice how we know have a nice hash of all the formats we support and how easy it is to introduce another format.