Nothing is easier than complicating something simple. That’s what I think in many cases when design patterns are mentioned, and in this post, we’re going to see why.
Index
1 - What are design patterns?
I want to start by mentioning what design patterns are, because contrary to what many people think, a design pattern is not a piece of code but a name for a solution to a problem in code.
We classify these patterns into three different groups: creational, structural, and collaborations. This distinction comes from the book Design Patterns: Elements of Reusable Object-Oriented Software where the authors identify these groups, distinguishing intention rather than implementation in software development within object-oriented programming.
If you’ve been in this field for a long time, you’ve probably noticed that the use of design patterns varies depending on the developer’s experience. There’s a simple reason for this: less experienced people don’t know them, so they don’t apply any. One day, you learn about a certain pattern and want to apply it everywhere, and finally, there’s the third stage, when you realize that applying design patterns in certain cases is overkill, or worse, you try to solve a problem you don’t actually have, but by trying to implement a pattern you create one.
2 - So, should we use design patterns or not?
And this is what I wanted to get to, because design patterns are very helpful both for code and developers when they’re implemented where and how they should be. They provide a common language for developers and keep code easier for everyone to read and maintain.
If you don’t use them well, you can end up with a builder pattern (which I mention later) that doesn’t provide value. For example, if you have a pizza object, you have several options: you can have an object with its constructor:
new pizza(toppings, size)
, or you can have the following:
new Pizza.WithToping(a).WithTopping(b).WithTopping(c).Size(medium).build()
; You might think this adds value, but in reality, it’s just a wrapper around the constructor. A better implementation is new Pizza.Margarita(size)
, or new Pizza.Hawaiian().WithExtraPineaple().Build()
;
Although the first alternative I mentioned is better than having 20 different constructors with default values. So, using patterns correctly is as important as implementing them well.
There are situations where we can avoid them, or use alternatives that might be more suitable.
For example, one design pattern is the singleton, where you only have one instance of an object. But today you can use dependency injection, where the end result is pretty much the same but the implementation, in my opinion, is better with dependency injection.
Sometimes patterns can create more work instead of reducing it. In that case, it’s better not to use them. Or if you use builder, you don’t have to use it for every object, just implement it where you need it.
A pattern can even require you to modify or force practices in your code that don’t make sense just so the pattern fits in.
With that in mind, there are a series of patterns that I find very useful when programming in a work environment, they can help a lot in different stages of software development.
3 - Design patterns
I don’t want to write a post, or make a course, that talks about every design pattern, but I’ll put the key ones, in my opinion, alongside real-world implementation examples.
3.1- Creational design patterns
Creational design patterns are those used when creating objects.
Among them we have Singleton, Factory Method, Abstract Factory.
3.1.1 - Builder Pattern
Although I mentioned it briefly before, with the builder pattern we build objects step by step using methods instead of using the constructor.
This ensures that the resulting object will always be valid and, in the long run, if we have many variables (as with a pizza), makes the code easier to maintain.
In C#, this can be done as follows:
namespace DesignPatterns.Creationals;
public enum Size { Small, Medium, Large }
public class Pizza
{
public Size Size { get; }
public IReadOnlyList<string> Toppings { get; }
private Pizza(Size size, IEnumerable<string> toppings)
{
Size = size;
Toppings = toppings.ToList().AsReadOnly();
}
public sealed class PizzaBuilder
{
private Size _size = Size.Medium;
private readonly List<string> _toppings = new();
public PizzaBuilder WithSize(Size size)
{
_size = size;
return this;
}
public PizzaBuilder AddTopping(string topping)
{
_toppings.Add(topping);
return this;
}
public PizzaBuilder AddExtraTopping(string topping)
{
_toppings.Add(topping);
_toppings.Add(topping); // +1 unidad extra
return this;
}
public PizzaBuilder RemoveTopping(string topping)
{
_toppings.RemoveAll(t => t.Equals(topping, StringComparison.OrdinalIgnoreCase));
return this;
}
public PizzaBuilder AddCheese() => AddTopping("Cheese");
public PizzaBuilder AddExtraCheese() => AddExtraTopping("Cheese");
public PizzaBuilder RemoveCheese() => RemoveTopping("Cheese");
public PizzaBuilder AddHam() => AddTopping("Ham");
public PizzaBuilder AddExtraHam() => AddExtraTopping("Ham");
public PizzaBuilder RemoveHam() => RemoveTopping("Ham");
public PizzaBuilder AddPineapple() => AddTopping("Pineapple");
public PizzaBuilder AddExtraPineapple() => AddExtraTopping("Pineapple");
public PizzaBuilder RemovePineapple() => RemoveTopping("Pineapple");
public PizzaBuilder AddTomatoes() => AddTopping("Tomatoes");
public PizzaBuilder AddExtraTomatoes() => AddExtraTopping("Tomatoes");
public PizzaBuilder RemoveTomatoes() => RemoveTopping("Tomatoes");
public PizzaBuilder AddBasil() => AddTopping("Basil");
public PizzaBuilder AddExtraBasil() => AddExtraTopping("Basil");
public PizzaBuilder RemoveBasil() => RemoveTopping("Basil");
public Pizza Build()
{
return new Pizza(_size, _toppings);
}
}
public static PizzaBuilder Margarita(Size size = Size.Medium) =>
new PizzaBuilder().WithSize(size)
.AddCheese()
.AddTomatoes()
.AddBasil();
public static PizzaBuilder Hawaiian(Size size = Size.Medium) =>
new PizzaBuilder()
.WithSize(size)
.AddHam()
.AddPineapple();
}
Where to use the builder pattern?
In objects where you can have many variations of the same type, as in the pizza example, and when you want them to be immutable.
3.1.2 - Factory method Pattern
In the factory method pattern, we define a class where we delegate the responsibility of object creation and its functionality to its subclasses, so the main class acts as a base for others and never as an independent object.
In C#, we do this with abstract classes. This would be the implementation in C# if we continue with the pizza example.
public abstract class PizzaStore
{
protected abstract Pizza CreateStarPizza();
}
public class MadridStore : PizzaStore
{
protected override Pizza CreateStarPizza()
{
return Pizza.Margarita().Build();
}
}
public class ParisStore : PizzaStore
{
protected override Pizza CreateStarPizza()
{
return Pizza.Margarita().AddTopping("baguette").Build();
}
}
Each of the child classes implements the abstract parent class.
Where to use the Factory method pattern?
When the main class that uses a service shouldn’t know its implementation. In .NET, ILogger is used as an interface for logging, but behind that interface you can put Serilog, Log4Net, etc. For the consumer, it doesn’t matter which one.
3.2 - Structural design patterns
Structural design patterns are mainly used for handling relationships among objects and how to separate or decouple them so a change in one doesn’t affect the whole system.
Within this group, we have bridge, decorator, composite, facade, adapter, flyweight, or proxy.
3.2.1 - Adapter
One of the most widely used patterns in the professional environment is the adapter because it allows two interfaces to work together when they’re not directly compatible.
The clearest example of where this code is used is when working with systems you don’t have access to. These systems could be external APIs or even modernizing current systems (from monoliths to microservices) where you want to leave behind the original practices and build your microservice according to the latest standards.
This way, with the adapter, you can maintain temporary compatibility until all uses of the “old way” change (assuming that change ever happens).
Other common cases are when you work with external services requiring data your usual process doesn’t produce. Instead of modifying your normal process, you simply handle this at your layer where the adapter is.
3.3 - Behavioral design patterns
Behavioral patterns are used to identify and define communication among objects.
There are some that are more famous or widely used, actually, this group has the most: chain of responsibility, command, memento, state, iterator, mediator, observer, strategy, template method, and visitor.
Here we’ll look at the two most popular or common ones in enterprise applications.
3.3.1 - Mediator pattern
Everyone’s favorite pattern, especially in C#, is the mediator pattern. We already saw a post about it but to sum up, it’s a pattern where objects or classes communicate indirectly. In C#, MediatR handles the communication between them, that’s the implementation of the pattern.
[ApiController]
[Route("[controller]")]
public class MediatRExampleController : ControllerBase
{
private readonly ISender _sender;
public MediatRExampleController(ISender sender)
{
_sender = sender;
}
[HttpPut("item")]
public async Task<bool> UpdateItem(ItemDto itemDto)
=> await _sender.Send(new UpdateItemCommand()
{
Id = itemDto.Id,
Price = itemDto.Price,
Title = itemDto.Title
});
}
// Logic execution
public class ItemUpdatedEventHandler : INotificationHandler<ItemUpdated>
{
//We dont need this working for the example.
public async Task Handle(ItemUpdated notification, CancellationToken cancellationToken)
{
decimal percentageDifference = ((notification.NewPrice - notification.OldPrice) / notification.NewPrice) * 100;
if (percentageDifference <= -30)
{
Console.WriteLine("Logic to get who is wishlisting that ID and send the emails");
}
}
From our code, we invoke the mediator, and it’s the mediator who redirects the call.
When to use the mediator pattern?
When you want to decouple different layers of a system. It’s especially helpful in microservices to avoid spaghetti code by cutting the direct dependency.
3.3.2 - Observer pattern
The observer pattern allows an object in the system to maintain a list of subscribers, who will be automatically notified when its state changes or a new event occurs.
This way, neither the subscribers nor the subscribed objects are coupled to each other.
// Client subscribes and reacts
Stock st = new Stock();
st.PropertyChanged += (_, e) =>
Console.WriteLine($"{e.PropertyName} changes to {st.Price}");
st.Price = 10;
st.Price = 35;
class Stock : INotifyPropertyChanged
{
private int _price;
public int Price
{
get => _price;
set
{
if (_price == value) return;
_price = value;
OnPropertyChanged(nameof(Price));
}
}
public event PropertyChangedEventHandler? PropertyChanged;
private void OnPropertyChanged(string p) =>
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(p));
}
In C# we have a widely used implementation of the observer pattern if you use WPF or MVVM in general, since the INotifyPropertyChanged
property is basically the observer pattern.
When to use the observer pattern?
When you need elements to react to others without touching the original object.
In the example above, we’re printing to the screen, but it could be an interface with a chart that updates from the database, thus you could update the chart in real time.
Conclusion
Design patterns are a great tool, but like any other, we should use them with good judgment when necessary. The only way to gain the knowledge of when to implement them is through experience. There’s no other way.
If there is any problem you can add a comment bellow or contact me in the website's contact form