Design Patterns Explained: Definition, Types, and Relevance
“Describe one of the Design Patterns.” That’s a question I’ve found in past technical interviews. Yes, some interviewers ask about patterns. Design Patterns aren’t only a subject of questions in interviews. They’re helpful in our everyday coding. Let’s see the definition, types, and some examples of Design Patterns.
What are design patterns?
The book “Design Patterns: Elements of Reusable Object-Oriented Software” describes the concept of Design Patterns. It’s a concept translated from the world of architecture to software. These Design Patterns are also known as the “Gang of Four (GoF) Design Patterns” since four authors wrote that book.
A Design Pattern is a blueprint for a solution to a frequent design problem in code. It isn’t a detailed recipe of coding steps but a description of how to solve problems in a context.
The Gang of Four book defines Design Patterns as ” descriptions of communicating objects and classes that are customized to solve a general design problem in a particular context.”
Types of Design Patterns
The Gang of Four classifies the 23 Design Patterns into three categories based on their purpose: Creational, Structural and Behavioral.
Creational patterns describe how to use objects to create other ones. These are some Creational patterns:
- Factory method
- Builder
- Singleton
Structural patterns show how to assemble objects. Some of them are:
- Adapter
- Composite
- Decorator
And Behavioral patterns indicate how a group of objects interact and distribute responsibility. Some Behavioral patterns are:
- Template Method
- Chain of Responsibility
- Command
- Strategy
We don’t need to memorize every single pattern out there. Even the “Gang of Four” book put it that way. Some Design Patterns are more common than others.
For example, we can find Builder in unit tests to create test data. Decorators to add logging, caching, and other responsibilities to existing services without modifying them. Template Method to define an operation and use child classes to replace one or more steps of that operation.
Also, we can find Factory Method to create a child class in a hierarchy from a type identifier. Chain of Responsibility to model a process as a series of steps that updates an input object. And, Adapter to plug in external code translating between our existing code and the external one.
Other patterns aren’t that common these days. For example, dependency containers have replaced Singleton, and language features like foreach...in
or foreach...of
with collections have replaced Iterator.
Three Design Patterns in Examples
Let’s see three Design Patterns in action.
1. Factory Method
First, let’s use a Factory Method to create a type of Employee
based on an employee type.
public class EmployeeFactory
{
public Employee CreateEmployee(EmployeeType type)
{
switch (type)
{
case EmployeeType.Hourly:
return new HourlyEmployee();
case EmployeeType.Salary:
return new SalaryEmployee();
case EmployeeType.Commission:
return new ComissionEmployee();
}
}
}
Notice we wrote a CreateEmployee()
method that receives an employee type and return one of HourlyEmployee
, SalaryEmployee
, and CommissionEmployee
. All of these employee types inherit from a base Employee
class.
With Factory Method, we create one child class in a hierarchy from an object type.
2. Decorator
Next, let’s use a Decorator to add logging to an existing PayEmployeeService
that looks like this,
public class PayEmployeeService : IPayEmployee
{
public void Pay(Employee employee)
{
// Beep, beep, boop...
// Some logic here to pay an employee
}
}
We can write a LoggedPayEmployeeService
inheriting from the same IPayEmployee
but inside it, let’s add some logging statements.
public class LoggedPaymentEmployeeService : IPayEmployee
{
private readonly IPayEmployee _decorated;
private reaondly ILogger<LoggedPaymentEmployeeService> _logger;
public LoggedPaymentEmployeeService(IPayEmployee decorated,
ILogger<LoggedPaymentEmployeeService> logger)
{
_decorated = decorated;
_logger = logger;
}
public void Pay(Employee employee)
{
_logger.LogInformation($"Starting to pay employee.");
try
{
_decorated.Pay(employee);
// ^^^^^^^^^^^^^^^^^^^^^
// Calling the existing PayEmployeeService
}
catch (Exception e)
{
_logger.LogError(e, $"Something wrong happened while paying employee.");
}
_logger.LogInformation($"Finished paying employee");
}
}
A decorator implements and receives the same type. Notice we wrote a new service that received a “decorated” service with the same signature. And inside the Pay()
method, we added the logging statements before and after calling the Pay()
method from the existingPayEmployeeService
service.
With Decorator, we wrap an object to add another responsibility to it.
3. Template Method
For our last example, let’s define the steps to promote an employee while letting child classes define how to increase the salary.
public abstract class PromoteEmployee
{
public void Promote(Employee employee)
{
EvalutePerformance();
IncreaseSalary();
SendNotificationLetter();
MakeAnnouncement();
}
private void EvalutePerformance() { /* ... */ }
protected abstract void IncreaseSalary();
private void SendNotificationLetter() { /* ... */ }
private void MakeAnnouncement() { /* ... */ }
}
Notice that we need four steps to promote an employee. We wrote one abstract method to increase the salary. This way, every type of Employee can define the salary increase. For example,
public PromoteHourlyEmployee : PromoteEmployee
{
protected override void IncreaseSalary()
{
var newHourlyRate = hourlyRate * 1.5;
// Do something with the newHourlyRate
}
}
With Template Method, we “plug” one of the steps of an operation from a child class.
Are design patterns still relevant?
Even though the concept of Design Patterns dates back to the 90s, Design Patterns are still relevant. They are part of the technical jargon or vocabulary of programming. It’s easy to talk about ”a factory method” than ” a method that creates a child class in a hierarchy from a code.”
Design Patterns are also a refactoring tool. They help us to remove duplicate code, simplify logic and communicate the intention behind our code. The book “Refactoring to Patterns” by Joshua Kerievsky teaches precisely that. We shouldn’t see patterns as a separate concept but see them in the context of refactoring.
Next Steps
Remember, Design Patterns are part of the programmer’s jargon, the same way doctors, lawyers, and accountants have their “inside” language.
For your future technical interviews, don’t try to memorize every single design pattern from the Gang of Four book. Instead, choose two or three common ones to understand their context and when to use them.
Study Design Patterns not only to be ready for your interviews but as a refactoring tool for your daily coding. From the Refactoring to Patterns book: “the goal is always to obtain a better design, not to implement patterns.”
Now you’re ready to describe one of the GoF Design Patterns in your future interviews.
Stay tuned to Klever for Solutions for more content about careers, interviews, and programming in general. Follow Klever for Solutions on LinkedIn too.