Inheritance and template pattern - simple example

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

Today I would like to show by simple example how to use template pattern in Ruby.

I'm sure you will find many ways of using that pattern in your application.

Let's imagine we're developing system which will work with third-party systems. We should generate files with data about users for third-party which will grab them out and process. We have class User which have: first_name, last_name and email.

class User
  attr_reader :first_name, :last_name, :email

  def initialize(first_name, last_name, email)
    @first_name = first_name
    @last_name  = last_name
    @email      = email
  end
end

The idea is to be able to generate files for different third-parties in different formats. Each third-party system expects different format of file which we should be able to generate.

We have two third-party clients: Froogle and Gamazon.

Froogle expects us to generate data in the following format:

---current_time---
first_name|last_name|email
------

Gamazon wants first_name and last_name to be one field and data should be splitted by commas:

---current_time---
first_name last_name,email
------

At first sight we see that both files have the same first (---current_time---) and last lines (------).

Let's implement that logic without using any patterns:

class FroogleFileGenerator
  attr_reader :user

  def initialize(user)
    @user = user
  end

  def generate
    File.write('froogle.csv', content)
  end

  private

  def header
    "---#{Time.now.strftime("%Y-%m-%d %H:%M:%S")}---"
  end

  def body
    [user.first_name, user.last_name, user.email].join('|')
  end

  def footer
    '------'
  end

  def content
    [header, body, footer].join("\n")
  end

end

class GamazonFileGenerator
  attr_reader :user

  def initialize(user)
    @user = user
  end

  def generate
    File.write('gamazon.csv', content)
  end

  private

  def header
    "---#{Time.now.strftime("%Y-%m-%d %H:%M:%S")}---"
  end

  def body
    ["#{user.first_name} #{user.last_name}", user.email].join(",")
  end

  def footer
    '------'
  end

  def content
    [header, body, footer].join("\n")
  end

end

user = User.new("John", "Doe", "john@doe.com")
FroogleFileGenerator.new(user).generate
GamazonFileGenerator.new(user).generate

This code works and we got proper files for each third-party.

But you've already noticed that there is too many duplication. Each class accepts object user and has methods: generate, header, footer, body, content.

Generally speaking, all we need is just unique implementation of body method. Other parts of files are being generated by the same template. Which shows us that we actually can use template design pattern in this case.

Let's see by example the main idea of template. We will use inheritance and extract common logic into parent class BasicFileGenerator:

class BasicFileGenerator
  attr_reader :user, :file_name

  def initialize(user, file_name)
    @user = user
    @file_name = file_name
  end

  def generate
    File.write(file_name, content)
  end

  private

  def header
    "---#{Time.now.strftime("%Y-%m-%d %H:%M:%S")}---"
  end

  def body
    raise NotImplementedError
  end

  def footer
    '------'
  end

  def content
    [header, body, footer].join("\n")
  end
end

class FroogleFileGenerator < BasicFileGenerator
  private

  def body
    [user.first_name, user.last_name, user.email].join('|')
  end
end

class GamazonFileGenerator < BasicFileGenerator
  private

  def body 
    ["#{user.first_name} #{user.last_name}", user.email].join(",")
  end
end

Looks much better. Let's see what we have: BasicFileGenerator#content implements logic of file generation. It combines all required parts: header, body and footer. Since header and footer had the same implementation - we extracted them into BasicFileGenerator. And body method should be implemented in each particular generator. If method is not implemented in generator - NotImplementedError will be raised.

Now it's much easier to add new generators. All we need to do is inherit from BasicFileGenerator and implement body method. That's it!

It's the most basic example of work with template pattern which allows to write less code and avoid duplication. It's so easy to extract common logic into base class and keep custom logic in child classes.

In this case inheritance is a perfect fit. Generators have a lot of common logic. But you should be aware that inheritance can be evil as well so you should keep an eye on that one. It's so easy to miss that point when inheritance is rather bad idea to use than good practice.

I described good side of inheritance for template pattern implementation. But let's consider another example.

We made a good work and implemented all generators for third-party services. Our code looks good and follows template pattern. But now we need to connect our service with company called Switter.

Besides information about user, they want to get some meta-data about request in footer. Let's call those attributes request_priority and request_type. So footer should look this way:

---#{request type}---#{request priority}---

Obviously these fields are not related to user, so we shouldn't add it to User class. We should extract that data into Request class (name is too general, but let's keep everything simple in this example):

class Request
  attr_reader :type, :priority

  def initialize(type, priority)
    @type = type
    @priority = priority
  end
end

It's just a simple data structure which could be replaced by Struct. But let's keep it as a class for now.

So having Request and User classes, let's write SwitterFileGenerator:

class SwitterFileGenerator < BasicFileGenerator  
  attr_reader :request

  def initialize(user, file_name, request)
    @request = request
    super(user, file_name)
  end

  private

  def body
    [user.first_name, user.last_name, user.email].join(",")
  end

  def footer
    "---#{request.type}---#{request.priority}---"
  end
end

This code works good and solves our task, but something wrong here. We still inherit from BasicFileGenerator but have to re-define initialize to be able to accept request. Also we had to re-define footer as well.

That's the main problem with inheritance. With time, insensibly, classes are getting bigger and re-define more and more methods from basic class. At some point it's not clear if we still need to inherit from parent class or it's time to refactor and use composition instead. Sad to say but usually developers don't want to break anything and keep using inheritance from base class.

In this particular case I would leave it as is too. But in case of connection to another third-party service which would require more data than User have - I would use another approach instead of template pattern.

Let's sum up: use template pattern if your code really do the same work by defined template. It begins from a, then do b and then c. In this case you can extract common logic into base class and custom implementation will stay in child classes. Very useful.

But keep an eye on custom classes: are they still following basic template which lives in parent class? how many additional arguments we pass? which data we process?

If you loose moment to get rid from inheritance - it will turn into very bad idea and will make your code more complex.

If you want to read books about patterns in Ruby - there is a good book : «Design Patterns in Ruby» — Russ Olsen.

Cheers!

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

Comments