Inheritance and template pattern - simple example
- 04 October 2016
- Patterns Ruby Design
- Inheritance and template pattern - simple example
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!