What are Design Patterns
a software design pattern is a general, reusable solution to a commonly occurring problem within a given context What does that mean? Well it means we have a general approach to a recurrent problem. In itself it is not an algorithm because it doesn't solve the issue on its own. The are multiple reasons to take those:
- They are well-defined and increase the readability of your code as other develops can identify the pattern
- They are reusable. Not like an algorithm which solves one problem
- It can increase code maintainability
The following graphic shows where design patterns are. They are an abstract concept. Unlike for example clean code where it is clear that you for example have to name variables / functions properly, design patterns are way more general.
We can distinguish between 4 types of design patterns:
- creational
- structural
- behavoriual
- concurrency
In this part we will have a look at two very commonly used design pattern: the Factory and the Builder pattern. The latter one will have a little twist.
Factory - Pattern
With the Factory pattern we want create objects without knowing the exact type. That decouples the logic how to create an object.
Simple Implementation
First have a look at the simple UML:
The factory can determine which concrete type we create given the SweetType
enumeration. It returns always an ISweet
. As we can see those design patterns enforce easily decoupling.
using System;
public interface ISweet
{
int CaloriesPer100Gramms { get;}
}
public class GummyBear : ISweet
{
public int CaloriesPer100Gramms => 450;
}
public class CandyBar : ISweet
{
public int CaloriesPer100Gramms => 500;
}
public enum Flavour
{
Chocolate,
SoftSweet
}
public class SweetFactory
{
public ISweet Create(Flavour flavour)
{
return flavour switch
{
Flavour.Chocolate => new CandyBar(),
Flavour.SoftSweet => new GummyBear(),
_ => throw new NotSupportedException("We don't have that kind of candy")
};
}
}
public class Program
{
public static void Main()
{
var sweet = new SweetFactory().Create(Flavour.Chocolate);
Console.Write($"Calories per 100g: {sweet.CaloriesPer100Gramms}");
}
}
This will output:
Calories per 100g: 500
Our SweetFactory
is the central point which decides which concrete object we want to create given the context. From a consumer point of view we only handle the interface ISweet
. Perfect all the domain knowledge in one single object.
(Fluent) Builder Pattern
With the builder pattern we try to decouple the construction of an complex object from its representation. I want to show you a version with a twist: the fluent builder pattern. The fluent builder returns itself so you can use a fluent notation to construct your object. Personally I use this often when it comes down to creating business objects for tests. We can set the properties via default in that way that our business object is valid.
Sample implementation
public class Person
{
public string Name { get; set; }
public Job Job { get; set; }
public Person(string name, Job job)
{
Name = name ?? throw new ArgumentNullException("name");
Job = job ?? throw new ArgumentNullException("job");
}
}
public class Job
{
public string Title { get; set; }
}
public class PersonBuilder
{
private string name = "Test name";
private Job job = new Job { Title = "Random Title" };
public PersonBuilder WithName(string name)
{
this.name = name;
return this;
}
public PersonBuilder WithJob(Action<JobBuilder> configuration)
{
var jobBuilder = new JobBuilder();
configuration(jobBuilder);
job = jobBuilder.Build();
return this;
}
public Person Build()
{
var person = new Person(name, job);
return person;
}
}
public class JobBuilder
{
private string title = "Title";
public JobBuilder WithTitle(string title)
{
this.title = title;
return this;
}
public Job Build()
{
return new Job { Title = title };
}
}
We see that we can decouple the logic of creating a person from the logic of creating a job but still in the fluent notation we are able to create multiple related objects with one call.
public static void Main()
{
var person = new PersonBuilder().WithName("Mike").WithJob(j => j.WithTitle("New Title")).Build();
Console.Write($"Person {person.Name} has Job: {person.Job.Title}");
}
Prints:
Person Mike has Job: New Title
If we don't care at all for the specific properties and we want just to have a valid Person
we call directly the Build
method: var person = new PersonBuilder().Build();
and we have a valid domain object.
This brings a big advantage when your domain layer emerges. You don't have to update all your tests because you have a new required constructor argument.
Conclusion
We saw how we can easily decouple the logic for creating objects and there specific type with the Factory pattern and how we can decouple the representation of a complex object with how it is constructed with the Builder Pattern.
Both are very commonly used.