DISCLAIMER: Image is generated using ChatGPT.
In this blog post, I am talking about Strategy design pattern in Perl. The example used in the post is taken from my book, Design Patterns in Modern Perl.
Have you seen code like this in your career? I have.
sub publish {
my ($self, $text) = @_;
if ($self->{strategy} =~ /^upper$/i) {
return uc $text;
}
elsif ($self->{strategy} =~ /^lower$/i) {
return lc $text;
}
return $text;
}
Do you see any issues in the above code?
Having studied design patterns, I see one potential issue. In the future, if we want to add another strategy, we will have to revisit the code.
Changing fully functional, tested code always carries the risk of introducing new bugs.
Let’s get to basics first, we all know the famous five core design principles, known as SOLID:
1. [S]ingle Responsibility Principle (SRP)
“A class should have one, and only one, reason to change.”
2. [O]pen/Closed Principle (OCP)
“Software entities should be open for extension, but closed for modification.”
3. [L]iskov Substitution Principle (LSP)
“Objects of a superclass should be replaceable with objects of its subclasses without breaking the application.”
4. [I]nterface Segregation Principle (ISP)
“Many client-specific interfaces are better than one general-purpose interface.”
5. [D]ependency Inversion Principle (DIP)
“Depend upon abstractions, code to an interface, not a concrete implementation.”
You don’t need to treat SOLID as absolute, rigid laws. Instead, think of them as diagnostic tools. If you ever find yourself dreading a new feature request because you know it will require modifying 10 different files, it usually means one of these five principles has been violated.
The Strategy design pattern strictly follows the Open/Closed Principle (OCP), which states that you should be able to add new functionality or behavior to your codebase without modifying any of the existing code you’ve already written and tested. Adhering to the OCP prevents a "fragile codebase", a common pitfall where adding a feature or fixing a bug unexpectedly breaks unrelated parts of the system.
Here is the complete original code that lacks the design pattern.
package TextFormatter;
use Moo;
has strategy => (is => 'rw', requires => 1);
sub publish {
my ($self, $text) = @_;
if ($self->{strategy} =~ /^upper$/i) {
return uc $text;
}
elsif ($self->{strategy} =~ /^lower$/i) {
return lc $text;
}
return $text;
}
package main;
use Test::More;
my $upper_case = TextFormatter->new({ strategy => 'upper' });
my $lower_case = TextFormatter->new({ strategy => 'lower' });
my $text = "IpHonE";
is $upper_case->publish($text), "IPHONE";
is $lower_case->publish($text), "iphone";
done_testing;
Now imagine, we have to add another strategy e.g. Camel case. Then you would have to do something like this:
package TextFormatter;
use Moo;
has strategy => (is => 'rw', requires => 1);
sub publish {
my ($self, $text) = @_;
if ($self->{strategy} =~ /^upper$/i) {
return uc $text;
}
elsif ($self->{strategy} =~ /^lower$/i) {
return lc $text;
}
elsif ($self->{strategy} =~ /^camel$/i) {
$text =~ s/^(\w)(\w)(.*)$/lc($1) . uc($2) . lc($3)/e;
return $text;
}
return $text;
}
package main;
use Test::More;
my $upper_case = TextFormatter->new({ strategy => 'upper' });
my $lower_case = TextFormatter->new({ strategy => 'lower' });
my $camel_case = TextFormatter->new({ strategy => 'camel' });
my $text = "IpHonE";
is $upper_case->publish($text), "IPHONE";
is $lower_case->publish($text), "iphone";
is $camel_case->publish($text), "iPhone";
done_testing;
You can see, where we are heading.
To address this issue, we can implement the Strategy design pattern.
Let’s take the original code, where we had only two strategies: upper case, lower case and convert them to use the Strategy design pattern.
For detailed discussion, you can follow my book. Let’s create a role (interface) to design the generic strategy as below:
package FormatterStrategy;
use Moo::Role;
requires qw/format/;
1;
Now that we have a defined role (interface), we can create as many strategy (concrete class) as we want. Since we want to focus on original code, we will create two strategies (concrete class) using the role (interface) we just created as below:
package UpperCaseFormatter;
use Moo;
with qw/FormatterStrategy/;
sub format {
my ($self, $text) = @_;
return uc $text;
}
1;
package LowerCaseFormatter;
use Moo;
with qw/FormatterStrategy/;
sub format {
my ($self, $text) = @_;
return lc $text;
}
1;
Time to create context class as below:
package TextFormatter;
use Moo;
has strategy => (is => 'rw', requires => 1);
sub publish {
my ($self, $text) = @_;
return $self->strategy->format($text);
}
The application code looks like this now:
package main;
use Test::More;
my $upper_case = TextFormatter->new({ strategy => UpperCaseFormatter->new });
my $lower_case = TextFormatter->new({ strategy => LowerCaseFormatter->new });
my $text = "IpHonE";
is $upper_case->publish($text), "IPHONE";
is $lower_case->publish($text), "iphone";
done_testing;
You would agree with me, this code is clean and structured as compare to the original code.
The benefit of this design pattern, we can easily extend the application to support new strategy (concrete class) e.g. Camel case as below:
package CamelCaseFormatter;
use Moo;
with qw/FormatterStrategy/;
sub format {
my ($self, $text) = @_;
$text =~ s/^(\w)(\w)(.*)$/lc($1) . uc($2) . lc($3)/e;
return $text;
}
1;
You see, no code change needed, just addition of a concrete class..
The application looks like this now:
package main;
use Test::More;
my $upper_case = TextFormatter->new({ strategy => UpperCaseFormatter->new });
my $lower_case = TextFormatter->new({ strategy => LowerCaseFormatter->new });
my $camel_case = TextFormatter->new({ strategy => CamelCaseFormatter->new });
my $text = "IpHonE";
is $upper_case->publish($text), "IPHONE";
is $lower_case->publish($text), "iphone";
is $camel_case->publish($text), "iPhone";
done_testing;
Happy Hacking !!!
