Template Method Design Pattern in the real world — Ruby

Bipin Paudel
Chromeearth
Published in
5 min readMay 26, 2021

Background

As software system grows and the codebase increases, code quality plays a major role to determine the future of that software. Therefore, the longevity of any software is determined by the code architecture, design patterns and the software features around it. In the software engineering paradigm, there are several approaches on how to manage your code and its architecture so that the project can go on for a decade and more.

In trying to answer these questions, GoF came up with a book ‘Design Patterns: Elements of Reusable Object-Oriented Software’ that provides a general discussion on some general design principles. The general ideas boil down to a couple of points.

  • Separate out the thing that stays same from those that stays together
  • Program to an interface, not concrete classes
  • Prefer composition over inheritance
  • Delegate

I will explain the above points through several articles in my Design Pattern Series. Today I am going to explain the Template Design Pattern.

Template Method Design Pattern

Imagine you have a feature to implement that maps data from one form to another which is pretty much simpler. Create a class, pass the source object through its constructor, write a mapping method, return the mapped version and write unit tests for the class.

Let me give you an example. Since I worked in the remittance industry before, I’ll provide an example related to it. Let’s say you are working with Paypal integration and want to send the certain attributes of your object to Paypal.

class PaypalMap
def initialize(payment)
@payment = payment
end
def map
info_to_map = map_basic_information
info_to_map.merge!(map_paypal_information)
end
private def map_basic_information
{
fname: @payment[:first_name],
lname: @payment[:last_name],
email: @payment[:email]
}
end
def map_paypal_information
{
paypal_id: @payment[:id_number],
paypal_pin: @payment[:pin_number]
}
end
end
user_info = {
first_name: 'Harry',
last_name: 'Potter',
email: 'harry@gmail.com',
id_number: 108892,
pin_number: 1234
}
paypal_mapper = PaypalMap.new(user_info)
puts paypal_mapper.map
OUTPUT
{:fname=>"Harry", :lname=>"Potter", :email=>"harry@gmail.com", :paypal_id=>108892, :paypal_pin=>1234}

You finally add some unit tests (if you are a good developer) and complete the feature. After a couple of weeks, your manager comes to you and asks you to do integrate Apple Pay too. Then you think to yourself, “I did the same thing last week, I’ll use the same file and change the payment mapping based on condition”. Then, you change your code to this.

class PaymentMap
def initialize(payment)
@payment = payment
end
def map
info_to_map = map_basic_information
if @payment[:payment_name] == 'paypal'
info_to_map.merge!(map_paypal_information)
elsif @payment[:payment_name] == 'apple_pay'
info_to_map.merge!(map_apple_pay_information)
end
end
private def map_basic_information
{
fname: @payment[:first_name],
lname: @payment[:last_name],
email: @payment[:email]
}
end
def map_paypal_information
{
paypal_id: @payment[:id_number],
paypal_pin: @payment[:pin_number]
}
end
def map_apple_pay_information
{
apple_id: @payment[:apple_email],
password: @payment[:pin_number]
}
end
end
user_info = {
payment_name: 'paypal',
first_name: 'Harry',
last_name: 'Potter',
email: 'harry@gmail.com',
id_number: 108892,
pin_number: 1234
}
paypal_mapper = PaymentMap.new(user_info)
puts paypal_mapper.map
user_info = {
payment_name: 'apple_pay',
first_name: 'Harry',
last_name: 'Potter',
email: 'harry@gmail.com',
pin_number: 1234,
apple_email: 'parry@gmail.com'
}
apple_pay_mapper = PaymentMap.new(user_info)
puts apple_pay_mapper.map
OUTPUT
{:fname=>"Harry", :lname=>"Potter", :email=>"harry@gmail.com", :paypal_id=>108892, :paypal_pin=>1234}
{:fname=>"Harry", :lname=>"Potter", :email=>"harry@gmail.com", :apple_id=>"parry@gmail.com", :password=>1234}

If you are an experienced coder, you’ll have a stomach ache to look at this code. The reason is, you’ll feel this code is not scalable because you realize what will happen in the future if your manager wants to integrate Google Pay, Transferwise and many more. Add multiple if/else statements? However, this idea directly contradicts the general architecture principle ‘Separate out the thing that stays the same from those that stays together’. To make Apple Pay related changes, you have to go through the same file where other payment integrations are located. Eventually, you’ll realize this is against the design principle.

Solution ( Separate the Things That Stay the Same)

What if there’s a simple way to tackle this problem? What if you could separate payment integration specific methods to the separate class files and add a common method in one parent class? This is the essence of the Template Method Design Pattern.

So you can create a base class that does the basic operation common to all the payment flow and let the powerful feature of OOP (Inheritance) come to power.

class BaseMapper
def initialize(payment)
@payment = payment
end
def map
info_to_map = map_basic_information
info_to_map.merge!(map_payment_information)
end
def map_basic_information
{
first_name: @payment[:first_name],
last_name: @payment[:last_name],
email: @payment[:email]
}
end
def map_payment_information
raise 'Override this method in subclass'
end
end

This is the base class that contains the main method ‘map’ that contains the sequence of operation needed to map data from the source object to the required object.

The first method ‘map_basic_information’ is common for both payment types. Hence, the method is defined in the same class. However, the next method ‘map_payment_information’ is different across integrations. So, this is defined as an abstract method in this class. Since, in Ruby, there’s no way to define method abstract like in Java or C++, we raise an exception if the method is not overridden by other payment-related classes.

Then comes separate class pertaining to the specific payment integration.

class PaypalMapper < BaseMapper  def map_payment_information
{
paypal_id: @payment[:id_number],
paypal_pin: @payment[:pin_number]
}
end
end
class ApplePayMapper < BaseMapper def map_payment_information
{
apple_id: @payment[:apple_email],
password: @payment[:pin_number]
}
end
end

Here I defined two separate payment-related classes that inherit the BaseMapper class. Then, we can override only those methods that are specific to the payment integration and implement them accordingly. In this way, we’re able to separate out the functionality. We can finally create an object of specific payment and invoke the method ‘map’ in the same way.

require_relative 'base_mapper'
require_relative 'paypal_mapper'
require_relative 'apple_pay_mapper'
user_info = {
payment_name: 'paypal',
first_name: 'Harry',
last_name: 'Potter',
email: 'harry@gmail.com',
id_number: 108892,
pin_number: 1234
}
paypal_mapper = PaypalMapper.new(user_info)
puts paypal_mapper.map
user_info = {
payment_name: 'apple_pay',
first_name: 'Harry',
last_name: 'Potter',
email: 'harry@gmail.com',
pin_number: 1234,
apple_email: 'parry@gmail.com'
}
apple_mapper = ApplePayMapper.new(user_info)
puts apple_mapper.map
OUTPUT
{:first_name=>"Harry", :last_name=>"Potter", :email=>"harry@gmail.com", :paypal_id=>108892, :paypal_pin=>1234}
{:first_name=>"Harry", :last_name=>"Potter", :email=>"harry@gmail.com", :apple_id=>"parry@gmail.com", :password=>1234}

Conclusion

Congratulations! You’ve discovered the essence of Template Design Pattern which is the simplest of the original GoF patterns. The general idea of the Template pattern is to build an abstract base class with a skeletal method. Meanwhile, this skeletal method is called the template method which is then overridden by the concrete subclasses which lead to varying implementation of the method. In addition to this, we pick the variation we want by selecting one of the concrete subclasses.

Books you can read to gain more information about design patterns.

Design Patterns in Ruby by Russ Olsen :- https://www.amazon.com/Design-Patterns-Reader-Addison-Wesley-Professional-ebook/dp/B004YW6M6G/ref=sr_1_1?dchild=1&keywords=design+pattern+ruby&qid=1622027536&s=digital-text&sr=1-1

GoF Design Pattern :- https://www.amazon.com/Design-Patterns-Object-Oriented-Addison-Wesley-Professional-ebook/dp/B000SEIBB8

Originally published at https://www.chromeearth.com on May 26, 2021.

--

--