Most of the code I’ve been writing is automating business processes. As a programmer I tell the computer what to do under specific conditions, and often those conditions change. Trying to make correct decisions can be paralyzing because it’s hard to know what new requirements will come.
Thank goodness for software design patterns.
Software design patterns can help write code that is resilient to future change. Here’s my take on the Strategy Pattern and the Null Object Pattern.
Let’s go through a typical request from the business. My example begins with a payroll app. The application handles paying employees and deducting their benefits. It looks like this:
Employee
model that’s persisted to the databasePaymentService
that has our business logicIHumanResourcesGateway
that actually does the e-transfer and benefitsPaymentsController
for the REST API
// Employee.cs
public class Employee
{
public string Id { get; set; }
public string Email { get; set; }
public decimal HourlyWage { get; set; }
public Guid? BenefitsPackage { get; set; }
}
// PaymentService.cs
private IHumanResourcesGateway _hr;
public PaymentService(IHumanResourcesGateway hr)
{
_hr = hr;
}
public void Pay(Employee employee)
{
_hr.DeductBenefits(employee.Id, employee.benefitsPackage);
_hr.SendETransfer(employee.Email, employee.HourlyWage);
}
// IHumanResourcesGateway.cs
public interface IHumanResourcesGateway
{
void DeductBenefits(string id, Guid? benefitsPackage);
void SendETransfer(string email, decimal hourlyWage);
}
// PaymentsController.cs
[HttpPost("payments")]
public IActionResult Pay(Employee employee)
{
_paymentService.Pay(employee); // _paymentService is injected in the constructor
return Ok();
}
Requirement 1: Pay part-time employees
Now that we have the app up and running, the business has a new requirement. The system needs to handle part-time employees. Part-time employees also get paid, but do not receive benefits.
Seems simple. One approach is to differentiate the type of employment to allow us to
figure out how to handle their payment, so let’s add an enum
called EmploymentType
.
// EmploymentType.cs
public enum EmploymentType
{
FullTime,
PartTime
}
I’ll also need to add the type to the Employee
model.
// Employee.cs
public class Employee
{
public string Id { get; set; }
public string Email { get; set; }
public EmploymentType Type { get; set; } // <-- new
public decimal HourlyWage { get; set; }
public Guid? BenefitsPackage { get; set; }
}
Great, now let’s conditionally change the Pay(...)
method based on the type.
The quick and simple solution would be to add an if
condition.
// PaymentService.cs
public void Pay(Employee employee)
{
if (employee.Type == EmploymentType.FullTime) // <-- new
{ // <-- new
_hr.DeductBenefits(employee.Id, employee.benefitsPackage);
_hr.SendETransfer(employee.Email, employee.HourlyWage);
} // <-- new
else if (employee.Type == EmploymentType.PartTime) // <-- new
{ // <-- new
_hr.SendETransfer(employee.Email, employee.HourlyWage); // <-- new
} // <-- new
}
This a little verbose but it works. Let’s figure out if I need to refactor.
- Can I unit test the condition separate from the behavior?
- Will there be changes to the condition?
- Will there be changes to the behavior?
I don’t know if there are future changes coming, but I do know that I cannot unit test the conditions separate from the behaviors.
And even worse, now that we introduced types it won’t be long before some other part of the system
will depend on checking the EmploymentType
. The same if
condition will be duplicated.
It would be nice to have solid test cases that can test the conditions separate from
the behavior. That would allow us to have confidence in our future changes.
I’ve seen this type of solution a lot, and I mean A LOT! This is fine in small applications, but in enterprise applications, it’s just a matter of time before an “oops”, especially because it’s hard to test.
Let’s refactor.
Strategy Pattern
There are two types of payments, full-time and part-time, I can consider each of these payments as a strategy.
Remember, strategies are just different implementations of the same action.
They take in the same parameters and return the same type, but what they do inside is the difference.
I’ll start with defining the IPaymentStrategy
interface.
public interface IPaymentStrategy
{
void Pay(Employee employee, IHumanResourcesGateway hr);
}
Now I can implement the strategy for full-time and part-time.
I basically moved the contents of the original if
blocks into each concrete strategy.
public class FullTimePay : IPaymentStrategy
{
public void Pay(Employee employee, IHumanResourcesGateway hr)
{
hr.DeductBenefits(employee.Id, employee.BenefitsPackage);
hr.SendETransfer(employee.Email, employee.HourlyWage);
}
}
public class PartTimePay : IPaymentStrategy
{
public void Pay(Employee employee, IHumanResourcesGateway hr)
{
hr.SendETransfer(employee.Email, employee.HourlyWage);
}
}
Next, I need a way to select the correct strategy. If there is complex
logic I could use a PaymentStrategyContext
class to place the conditional logic,
but in this case I’m just checking the EmploymentType
, which is a single field,
so a dictionary will work fine. I’ll store the dictionary in the PaymentService
class
for now.
// PaymentService.cs
private Dictionary<EmploymentType, IPaymentStrategy>
_paymentStrategies = new Dictionary<EmploymentType, IPaymentStrategy>
{
{ EmploymentType.FullTime, new FullTimePay() },
{ EmploymentType.PartTime, new PartTimePay() }
}
Finally, I can update our service to use the newly created strategies.
Say goodbye to the if
condition!
// PaymentService.cs
public void Pay(Employee employee)
{
_paymentStrategies[employee.Type].Pay(employee, _hr);
}
Wow nice! We wrote a whole bunch more code just to remove two if
conditions.
Is this actually helpful?
If the system never changes again then we wasted our time.
The strategy pattern prevents duplication of conditional logic and helps us to follow DRY: Don’t Repeat Yourself. By consolidating the conditional behavior into one place, I can easily add new behaviors and modify conditions. Also, I can now unit test the conditions separately from the behavior.
Requirement 2: “Pay” those Volunteers
Now the business is interested in hiring some volunteers. The thing about volunteers
is that, they receive no pay and no benefits. Let’s first add to our EmployementType
.
// EmploymentType.cs
public enum EmploymentType
{
FullTime,
PartTime,
Volunteer // <-- new
}
One option is to just add the if
condition that checks their type but that’s the same as before.
We don’t want to do that.
// PaymentService.cs
public void Pay(Employee employee)
{
if (employee.Type != EmploymentType.Volunteer) // <-- BAD
_paymentStrategies[employee.Type].Pay(employee, _hr);
}
Instead, I’ll take the strategy pattern a step further. Let’s define a strategy for when someone doesn’t get paid.
Null Object Pattern
A payment strategy for no payment.
public class NoPay : IPaymentStrategy
{
public void Pay(Employee employee, IHumanResourcesGateway hr)
{
return; // do nothing
}
}
I add it to the dictionary of payment strategies, and the rest of the code just works.
// PaymentService.cs
private Dictionary<EmploymentType, IPaymentStrategy>
_paymentStrategies = new Dictionary<EmploymentType, IPaymentStrategy>
{
{ EmploymentType.FullTime, new FullTimePay() },
{ EmploymentType.PartTime, new PartTimePay() },
{ EmploymentType.Volunteer, new NoPay() } // <-- new
}
Summary
The bigger an application grows the easier it is to duplicate conditional logic. In the case of conditional behavior, we can leverage the Strategy Pattern and the Null Object Pattern to help with managing change. This makes it easier to add and modify behavior to adapt throughout the system.
A bonus is that we can separate the testing of condition from testing of behavior.
In our example we did not need to unit test the dictionary, but when the
conditional logic in the Dictionary<Key,Strategy>
becomes more complex we can move it to a context class.
Then we can unit test the GetStrategy(...)
method on its own.
The context will be the single source of truth for selecting concrete IPaymentStrategy
.
// PaymentStrategyContext.cs
public class PaymentStrategyContext
{
public IPaymentStrategy GetStrategy(Employee employee)
{
if (...)
return new FullTime();
else if (...)
return new PartTime();
else
return new NoPay();
}
}
So many videos, blogs, and readings have helped me grow my understanding of these patterns. I am not an expert at these patterns, so I apprectiate your feedback.
Thanks for reading!