Strategy Design Pattern in the real world, Ruby
Background
If you went through my previous post on the Design Pattern series about Template Method Pattern, you can see that the basic restructuring of your code helps you in improving your code quality effectively. The code in the previous Template Method Pattern maintains reusability and single responsibility which is the main principle of Design Pattern. If you are coming to this article directly and are new to the Template Method Pattern, I highly urge you to take 5 minutes out and go through the previous article because the context here follows the previous article.
Despite the advantages of the Template pattern, it fails to provide all the flexibility that we require when we’re building a complex system. I’ve mentioned in the previous article ‘Prefer composition over inheritance’, and Template Method Pattern fails to acknowledge this. This pattern revolves around inheritance which is its major drawback. Additionally, as you add several subclasses, they are bound to get tangled upon each other. Let me show you an example.
Let’s say you had to add a new payment service Google Pay and map its data as you did for other payments. The attributes for a Google Pay are as follows:-
owner_1_first_name
owner_1_last_name
google_id
pin_code
If you look at the attributes for Paypal and Apple Pay, they have some similar attributes while Google Pay has completely different ones. Now, the problem is should you go and change the base class to accommodate the change? If we do this, we directly violate the major design principle OCP (Open for extension, close for modification). In complex systems, you will surely encounter the problem I mentioned above. So, how good your code design is, the inheritance will bite you later on.
In this article, I am going to discuss the Strategy Pattern which mitigates the above issue and work on the principle of ‘ Delegation ‘. The main idea of Strategy Pattern is to tear out the subclasses from their parent class and isolate it into their own class. The strategy pattern lets us define the set of algorithm it requires. Now we can handle the mapping of payment data in their own class. Here’s an example of a Strategy pattern.
Strategy Pattern Example
A separate module for mapping general payment data that’s used by Paypal and Apple Pay but not Google Pay
module GeneralInfoMapper
def map_basic_information(payment)
{
first_name: payment[:first_name],
last_name: payment[:last_name],
email: payment[:email]
}
end
end
Include the above module in Paypal mapper and Apple Pay mapper class
require_relative 'general_info_mapper'
class PaypalMapper
include GeneralInfoMapper
def initialize(payment)
@payment = payment
end
def map_payment_information
basic_info = map_basic_information(@payment)
paypal_info =
{
paypal_id: @payment[:id_number],
paypal_pin: @payment[:pin_number]
}
paypal_info.merge(basic_info)
end
endrequire_relative 'general_info_mapper'
class ApplePayMapper
include GeneralInfoMapper
def initialize(payment)
@payment = payment
end
def map_payment_information
basic_info = map_basic_information(@payment)
apple_pay_info =
{
apple_id: @payment[:apple_email],
password: @payment[:pin_number]
}
apple_pay_info.merge(basic_info)
end
end
Then a Google Pay Mapper that has it’s own implementation different from Paypal and Apple Pay
class GooglePayMapper
def initialize(payment)
@payment = payment
end
def map_payment_information
{
google_id: @payment[:google_id],
pin_code: @payment[:pin_code],
owner_1_first_name: @payment[:owner_1_first_name],
owner_1_last_name: @payment[:owner_1_last_name],
}
end
end
The base mapper expects a payment mapper object and calls its method which leads to their own implementation.
class BaseMapper
def initialize(payment, mapper_obj)
@payment = payment
@mapper_obj = mapper_obj
end
def map
puts "Starting #{@payment[:payment_name]} map operation"
puts @mapper_obj.map_payment_information
puts 'Mapping Ended'
end
end
Finally, we can test this
require_relative 'paypal_mapper'
require_relative 'apple_pay_mapper'
require_relative 'google_pay_mapper'
require_relative 'base_mapper'
user_info = {
payment_name: 'paypal',
first_name: 'Harry',
last_name: 'Potter',
email: 'harry@gmail.com',
id_number: 108892,
pin_number: 1234
}
paypal_object = PaypalMapper.new(user_info)
paypal_mapped = BaseMapper.new(user_info, paypal_object)
puts paypal_mapped.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_object = ApplePayMapper.new(user_info)
apple_pay_mapped = BaseMapper.new(user_info, apple_pay_object)
puts apple_pay_mapped.map
user_info = {
payment_name: 'google_pay',
owner_1_first_name: 'Harry',
owner_1_last_name: 'Potter',
google_id: 1200,
pin_code: 5000
}
google_pay_object = GooglePayMapper.new(user_info)
google_pay_mapped = BaseMapper.new(user_info, google_pay_object)
puts google_pay_mapped.map
OUTPUT
Starting paypal map operation
{:paypal_id=>108892, :paypal_pin=>1234, :first_name=>"Harry", :last_name=>"Potter", :email=>"harry@gmail.com"}
Mapping Ended
Starting apple_pay map operation
{:apple_id=>"parry@gmail.com", :password=>1234, :first_name=>"Harry", :last_name=>"Potter", :email=>"harry@gmail.com"}
Mapping Ended
Starting google_pay map operation
{:google_id=>1200, :pin_code=>5000, :owner_1_first_name=>"Harry", :owner_1_last_name=>"Potter"}
Mapping Ended
The benefit of Strategy Pattern over Template is that it is flexible. In addition to this, we can easily incorporate classes that have different kind of implementation than the rest. This is one of the benefits of Composition over inheritance. Therefore, if any changes come at one of the class, it does not have to affect the rest of the classes. This is the primary reason Strategy pattern is beneficial than Template Method Pattern.
Another advantage of the Strategy pattern is that it’s easier to change the implementation of a strategy at runtime. As shown below, we simply swap out the strategy object.
paypal_object = PaypalMapper.new(user_info)
mapped = BaseMapper.new(user_info, paypal_object)
mapped.mapper_obj = ApplePayMapper.new(user_info)
Conclusion
In conclusion, there’s no strict rule on which pattern is the best and which is not. In addition to this, it is based upon the requirement of the system. Strategy is better than Template because of the low dependency between the classes. However, one can use both the pattern to get the benefits out of them. So, it’s upon the person who is trying to design and implement the system.
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 June 3, 2021.