visit
I am sure this is not the first time for you to hear about the Builder Design Pattern. However, I promise you that you would find something different in this article.
In this article, we would walk through the whole process of developing a Fluent API using the Builder Design Pattern, from the early steps of thinking about it, to the latest of testing it.
It is a creational design pattern which allows creating complex objects into small simple steps one by one.
Create a new Class Library or Console Application. I named my project as FluentApi.
Now you need to keep an important thing in your mind, we will need to jump back and forth between Interfaces and Dtos while working on the implementation, this is normal.
Now let’s start with our first interface, IMemberBuilder
. Here is an important trick. I created a file under the Interfaces folder and named it 01.IMemberBuilder.cs
This 01. at the start of the name helps me easily track the sequence of the whole process. Otherwise, for a small change, you might need to go through all the files to spot the place to apply your changes.
namespace FluentApi.Builder.Interfaces
{
public interface IMemberBuilder
{
IHuman New { get; }
}
}
We know, from the Sketch, that our Builder should expose a New
property and this property should lead us to something that exposes two methods; WithName(name)
and WithAge(age)
.
So, the New property should return, let’s say a new Interface called IHuman
.
Moving to the next step, let’s define the IHuman
interface. So, create a 02.IHuman.cs file, and define the interface as follows:
namespace FluentApi.Builder.Interfaces
{
public interface IHuman
{
IHaveAgeAndCanHaveName WithAge(int age);
IHaveNameAndCanHaveAge WithName(string name);
}
}
We know, from the Sketch, that the IHuman
interface should have the two methods WithName(name)
and WithAge(age)
. However, these two methods should have different return types. Why???
Because we want that once the WithName(name)
is called, the only available option is to call WithAge(age)
, not another WithName(name)
. And the same applies to WithAge(age)
.
Note: You might also prefer to have only one method which takes in both the name and age, this is also right but I preferred here to seize the chance to show you different options.
Moving to the next step, let’s define the IHaveAgeAndCanHaveName
interface. So, create a 03.IHaveAgeAndCanHaveName.cs file, and define the interface as follows:
namespace FluentApi.Builder.Interfaces
{
public interface IHaveAgeAndCanHaveName
{
IHasRole WithName(string name);
}
}
We know, from the Sketch, that the IHaveAgeAndCanHaveName
interface should have the method WithName(name)
. And this method should return something that exposes the AsTeacher
and AsStudent
properties.
Also, following the same way, let’s define the IHaveNameAndCanHaveAge
interface. So, create a 03.IHaveNameAndCanHaveAge.cs file (note that the file is numbered as 03 because it is still on the third step on the whole process), and define the interface as follows:
namespace FluentApi.Builder.Interfaces
{
public interface IHaveNameAndCanHaveAge
{
IHasRole WithAge(int age);
}
}
We know, from the Sketch, that the IHaveNameAndCanHaveAge
interface should have the method WithAge(age)
. And this method should return something that exposes the AsTeacher
and AsStudent
properties, the same as IHaveAgeAndCanHaveName.WithName(name)
.
Moving to the next step, let’s define the IHasRole
interface. So, create a 04.IHasRole.cs file, and define the interface as follows:
namespace FluentApi.Builder.Interfaces
{
public interface IHasRole
{
IAmTeaching AsTeacher { get; }
IAmStudying AsStudent { get; }
}
}
We know, from the Sketch, that the IHasRole
interface should have the two properties AsTeacher
and AsStudent
. And every one of these properties should return something different according to the following step on the sketch.
Moving to the next step, let’s define the IAmStudying
interface. So, create a 05.IAmStudying.cs file, and define the interface as follows:
using FluentApi.Builder.Dtos;
namespace FluentApi.Builder.Interfaces
{
public interface IAmStudying
{
IHasStudyingSchedule Studying(params Subject[] subjects);
}
}
We know, from the Sketch, that the IAmStudying
interface should have the method Studying(subjects)
. This method should expect an input of type array of Subject
. So, we need to define the class Subject
.
Also, the Studying(subjects)
should return something exposing WithSchedule(subjectsSechedules)
.
So, we create a Subject.cs file inside the Dtos folder and the code would be as follows:
using System;
using System.Collections.Generic;
using System.Linq;
namespace FluentApi.Builder.Dtos
{
public sealed class Subject : IEquatable<Subject>
{
public Subject(string name)
{
Name = name;
}
public Subject(Subject other)
{
if (other != null)
{
Name = other.Name;
}
}
public string Name { get; }
public bool Equals(Subject other)
{
if (ReferenceEquals(null, other)) return false;
if (ReferenceEquals(this, other)) return true;
return Name == other.Name;
}
public override bool Equals(object obj)
{
if (ReferenceEquals(null, obj)) return false;
if (ReferenceEquals(this, obj)) return true;
if (obj.GetType() != this.GetType()) return false;
return Equals((Subject)obj);
}
public override int GetHashCode()
{
return (Name != null ? Name.GetHashCode() : 0);
}
public static bool operator ==(Subject left, Subject right)
{
return Equals(left, right);
}
public static bool operator !=(Subject left, Subject right)
{
return !Equals(left, right);
}
}
public static class SubjectExtensions
{
public static IEnumerable<Subject> Clone(this IEnumerable<Subject> subjects)
{
return (subjects != null)
? subjects
.Where(s => s != null)
.Select(s => new Subject(s))
: new List<Subject>();
}
}
}
Name
property.IEquatable<Subject>
interface and we generated all the required members.public Subject(Subject other)
to provide a way of cloning the Subject from another Subject. The cloning capability in the Builder Pattern is so important because at every step you need to deal with a totally separate object (with different reference) than the ones on previous and next steps.Clone
to IEnumerable<Subject>
to avoid repeating the same code on different places.public Subject(Subject other)
constructor we defined in the Subject
class.Moving to the next step, let’s define the IAmTeaching
interface. So, create a 05.IAmTeaching.cs file, and define the interface as follows:
using FluentApi.Builder.Dtos;
namespace FluentApi.Builder.Interfaces
{
public interface IAmTeaching
{
IHasTeachingSchedule Teaching(Subject subject);
}
}
We know, from the Sketch, that the IAmTeaching
interface should have the method Teaching(subject)
. This method should expect an input of type Subject
.
Also, the Teaching(subject)
should return something exposing WithSchedule(sechedules)
.
Moving to the next step, let’s define the IHasStudyingSchedule
interface. So, create a 06.IHasStudyingSchedule.cs file, and define the interface as follows:
using FluentApi.Builder.Dtos;
namespace FluentApi.Builder.Interfaces
{
public interface IHasStudyingSchedule
{
ICanBeBuilt WithSchedule(params SubjectSchedule[] subjectsSchedules);
}
}
We know, from the Sketch, that the IHasStudyingSchedule
interface should have the method WithSchedule(subjectsSchedules)
. This method should expect an input of type array of SubjectSchedule
.
Also, the WithSchedule(subjectsSchedules)
should return something exposing the method Build()
.
So, we create Schedule.cs and SubjectSchedule.cs files inside the Dtos folder and the code would be as follows:
using System;
using System.Collections.Generic;
using System.Linq;
namespace FluentApi.Builder.Dtos
{
public class Schedule
{
public Schedule(DateTime from, DateTime to)
{
From = from;
To = to;
}
public Schedule(Schedule other)
{
if (other != null)
{
From = other.From;
To = other.To;
}
}
public DateTime From { get; }
public DateTime To { get; }
}
public static class ScheduleExtensions
{
public static IEnumerable<Schedule> Clone(this IEnumerable<Schedule> schedules)
{
return (schedules != null)
? schedules
.Where(s => s != null)
.Select(s => new Schedule(s))
: new List<Schedule>();
}
}
}
using System.Collections.Generic;
using System.Linq;
namespace FluentApi.Builder.Dtos
{
public class SubjectSchedule
{
public SubjectSchedule(Subject subject, Schedule schedule)
{
Subject = subject;
Schedule = schedule;
}
public SubjectSchedule(SubjectSchedule other)
{
if (other != null)
{
Subject = new Subject(other.Subject);
Schedule = new Schedule(other.Schedule);
}
}
public Subject Subject { get; }
public Schedule Schedule { get; }
}
public static class SubjectScheduleExtensions
{
public static IEnumerable<SubjectSchedule> Clone(this IEnumerable<SubjectSchedule> subjectsSchedules)
{
return (subjectsSchedules != null)
? subjectsSchedules
.Where(s => s != null)
.Select(s => new SubjectSchedule(s))
: new List<SubjectSchedule>();
}
}
}
Here we follow the same rules as in the Subject
class.
Moving to the next step, let’s define the IHasTeachingSchedule
interface. So, create a 06.IHasTeachingSchedule.cs file, and define the interface as follows:
using FluentApi.Builder.Dtos;
namespace FluentApi.Builder.Interfaces
{
public interface IHasTeachingSchedule
{
ICanBeBuilt WithSchedule(params Schedule[] schedules);
}
}
We know, from the Sketch, that the IHasTeachingSchedule
interface should have the method WithSchedule(schedules)
. This method should expect an input of type array of SubjectSchedule
.
Also, the WithSchedule(schedules)
should return something exposing the method Build()
.
Moving to the next step, let’s define the ICanBeBuilt
interface. So, create a 07.ICanBeBuilt.cs file, and define the interface as follows:
using FluentApi.Builder.Dtos.Descriptors;
namespace FluentApi.Builder.Interfaces
{
public interface ICanBeBuilt
{
MemberDescriptor Build();
}
}
We know, from the Sketch, that the ICanBeBuilt
interface should have the method Build()
which returns the final composed MemberDescriptor
.
So, we create a SubjectSchedule.cs file inside the Dtos>Descriptors folder.
This MemberDescriptor
class should expose all the details of a member whether he is a Teacher or Student.
MemberDescriptor
namespace FluentApi.Builder.Dtos
{
public enum MemberRole
{
Teacher = 1,
Student = 2
}
}
namespace FluentApi.Builder.Dtos.Descriptors
{
public class MemberDescriptor
{
public MemberDescriptor(MemberDescriptor other = null)
{
if (other != null)
{
Name = other.Name;
Age = other.Age;
Role = other.Role;
}
}
public string Name { get; set; }
public int Age { get; set; }
public MemberRole Role { get; set; }
public virtual MemberDescriptor Clone()
{
return new MemberDescriptor(this);
}
}
}
MemberDescriptor
class is exposing the basic info about a member. The more specific info about a Teacher or Student would reside in other two classes for Teacher and Student.public MemberDescriptor(MemberDescriptor other = null)
constructor for cloning purposes as explained before.public virtual MemberDescriptor Clone()
method for an important reason. At some steps on the process, you would be merging from a more specific case to a more generic one. In this kind of cases, your implementations of the interfaces would need to deal with the parent MemberDescriptor
class, not any of its children. And, it would need to clone the entity without knowing it is originally a Teacher or Student.
When implementing the ICanBeBuilt
interface, it would be expecting an instance of MemberDescriptor
, it can’t be a specific descriptor for a Teacher or Student as it is a common step for both paths. Additionally, you would need at the end to clone the passed in MemberDescriptor
.
TeacherDescriptor
using System.Collections.Generic;
using System.Linq;
namespace FluentApi.Builder.Dtos.Descriptors
{
public class TeacherDescriptor : MemberDescriptor
{
public TeacherDescriptor(MemberDescriptor member = null) : base(member)
{
if (member is TeacherDescriptor teacher)
{
Subject = teacher.Subject != null ? new Subject(teacher.Subject) : null;
Schedules = teacher.Schedules != null
? teacher.Schedules.Clone().ToList()
: new List<Schedule>();
}
}
public Subject Subject { get; set; }
public List<Schedule> Schedules { get; set; } = new List<Schedule>();
public override MemberDescriptor Clone()
{
return new TeacherDescriptor(this);
}
}
}
IEnumerable<Schedule>
extension method for cloning.Clone
method and now we are using our type-specific clone constructor.
StudentDescriptor
using System.Collections.Generic;
using System.Linq;
namespace FluentApi.Builder.Dtos.Descriptors
{
public class StudentDescriptor : MemberDescriptor
{
public StudentDescriptor(MemberDescriptor member = null) : base(member)
{
if (member is StudentDescriptor student)
{
Subjects = student.Subjects != null
? student.Subjects.Clone().ToList()
: new List<Subject>();
SubjectsSchedules =
student.SubjectsSchedules != null
? student.SubjectsSchedules.Clone().ToList()
: new List<SubjectSchedule>();
}
}
public List<Subject> Subjects { get; set; } = new List<Subject>();
public List<SubjectSchedule> SubjectsSchedules { get; set; } = new List<SubjectSchedule>();
public override MemberDescriptor Clone()
{
return new StudentDescriptor(this);
}
}
}
Following the same concept as in TeacherDescriptor
.
Let’s define the MemberBuilder
class implementing the IMemberBuilder
interface. So, create a 01.MemberBuilder.cs file, and define the class as follows:
using FluentApi.Builder.Dtos.Descriptors;
using FluentApi.Builder.Interfaces;
namespace FluentApi.Builder.Implementations
{
public class MemberBuilder : IMemberBuilder
{
public IHuman New => new Human(new MemberDescriptor());
}
}
The New
property, should return an IHuman
interface. So, we would now move to implementing the IHuman
interface but we need to keep in mind something important. We need to keep passing around the partially composed MemberDescriptor
because each step would add some detail to it till it is finally complete.
On the MemberBuilder
class, we don’t have any detail to add, however, this is our starting point, so the class should create the initial MemberDescriptor
to start with and then pass it to the next step.
Moving on to define the Human
class implementing the IHuman
interface. So, create a 02.Human.cs file, and define the class as follows:
using FluentApi.Builder.Dtos.Descriptors;
using FluentApi.Builder.Interfaces;
namespace FluentApi.Builder.Implementations
{
internal class Human : IHuman
{
private readonly MemberDescriptor m_Descriptor;
public Human(MemberDescriptor descriptor)
{
m_Descriptor = descriptor;
}
public IHaveNameAndCanHaveAge WithName(string name)
{
var clone = new MemberDescriptor(m_Descriptor) { Name = name };
return new HaveNameAndCanHaveAge(clone);
}
public IHaveAgeAndCanHaveName WithAge(int age)
{
var clone = new MemberDescriptor(m_Descriptor) { Age = age };
return new HaveAgeAndCanHaveName(clone);
}
}
}
We defined a constructor which takes in a MemberDescriptor
and saves it to a local read-only variable.
We also implemented the two methods but what is important to notice here is that before adding any detail to the MemberDescriptor
we first create a clone of it. To create a clone, we can use the cloning constructor or call the Clone
method on the MemberDescriptor
class.
Moving on to define the HaveAgeAndCanHaveName
class implementing the IHaveAgeAndCanHaveName
interface. So, create a 03.HaveAgeAndCanHaveName.cs file, and define the class as follows:
using FluentApi.Builder.Dtos.Descriptors;
using FluentApi.Builder.Interfaces;
namespace FluentApi.Builder.Implementations
{
internal class HaveAgeAndCanHaveName : IHaveAgeAndCanHaveName
{
private readonly MemberDescriptor m_Descriptor;
public HaveAgeAndCanHaveName(MemberDescriptor descriptor)
{
m_Descriptor = descriptor;
}
public IHasRole WithName(string name)
{
var clone = new MemberDescriptor(m_Descriptor) { Name = name };
return new HasRole(clone);
}
}
}
Moving on to define the HaveNameAndCanHaveAge
class implementing the IHaveNameAndCanHaveAge
interface. So, create a 03.HaveNameAndCanHaveAge.cs file, and define the class as follows:
using FluentApi.Builder.Dtos.Descriptors;
using FluentApi.Builder.Interfaces;
namespace FluentApi.Builder.Implementations
{
internal class HaveNameAndCanHaveAge : IHaveNameAndCanHaveAge
{
private readonly MemberDescriptor m_Descriptor;
public HaveNameAndCanHaveAge(MemberDescriptor descriptor)
{
m_Descriptor = descriptor;
}
public IHasRole WithAge(int age)
{
var clone = new MemberDescriptor(m_Descriptor) { Age = age };
return new HasRole(clone);
}
}
}
Moving on to define the HasRole
class implementing the IHasRole
interface. So, create a 04.HasRole.cs file, and define the class as follows:
using FluentApi.Builder.Dtos;
using FluentApi.Builder.Dtos.Descriptors;
using FluentApi.Builder.Interfaces;
namespace FluentApi.Builder.Implementations
{
internal class HasRole : IHasRole
{
private readonly MemberDescriptor m_Descriptor;
public HasRole(MemberDescriptor descriptor)
{
m_Descriptor = descriptor;
}
public IAmTeaching AsTeacher =>
new AmTeaching(new TeacherDescriptor(m_Descriptor) { Role = MemberRole.Teacher });
public IAmStudying AsStudent =>
new AmStudying(new StudentDescriptor(m_Descriptor) { Role = MemberRole.Student });
}
}
Moving on to define the AmStudying
class implementing the IAmStudying
interface. So, create a 05.AmStudying.cs file, and define the class as follows:
using System.Linq;
using FluentApi.Builder.Dtos;
using FluentApi.Builder.Dtos.Descriptors;
using FluentApi.Builder.Interfaces;
namespace FluentApi.Builder.Implementations
{
internal class AmStudying : IAmStudying
{
private readonly StudentDescriptor m_Descriptor;
public AmStudying(StudentDescriptor descriptor)
{
m_Descriptor = descriptor;
}
public IHasStudyingSchedule Studying(params Subject[] subjects)
{
var clone = new StudentDescriptor(m_Descriptor) { Subjects = subjects.AsEnumerable().Clone().ToList() };
return new HasStudyingSchedule(clone);
}
}
}
What to notice here is that the constructor is expecting a StudentDescriptor
not a MemberDescriptor
and that’s because at the moment of constructing AmStudying
it is clear.
Also, notice that we even cloned the passed in array of Subject
using the extension method we created before. This way we make sure that any changes to be applied by the end user to the passed in array of Subject
would not affect our builders state.
Moving on to define the AmTeaching
class implementing the IAmTeaching
interface. So, create a 05.AmTeaching.cs file, and define the class as follows:
using FluentApi.Builder.Dtos;
using FluentApi.Builder.Dtos.Descriptors;
using FluentApi.Builder.Interfaces;
namespace FluentApi.Builder.Implementations
{
internal class AmTeaching : IAmTeaching
{
private readonly TeacherDescriptor m_Descriptor;
public AmTeaching(TeacherDescriptor descriptor)
{
m_Descriptor = descriptor;
}
public IHasTeachingSchedule Teaching(Subject subject)
{
var clone = new TeacherDescriptor(m_Descriptor) { Subject = new Subject(subject) };
return new HasTeachingSchedule(clone);
}
}
}
What to notice here is that the constructor is expecting a TeacherDescriptor
not a MemberDescriptor
and that’s because at the moment of constructing AmTeaching
it is clear.
Also, here we are not passing the same Subject
passed in by the end user, we are passing a clone.
Moving on to define the HasStudyingSchedule
class implementing the IHasStudyingSchedule
interface. So, create a 06.HasStudyingSchedule.cs file, and define the class as follows:
using System;
using System.Linq;
using FluentApi.Builder.Dtos;
using FluentApi.Builder.Dtos.Descriptors;
using FluentApi.Builder.Interfaces;
namespace FluentApi.Builder.Implementations
{
internal class HasStudyingSchedule : IHasStudyingSchedule
{
private readonly StudentDescriptor m_Descriptor;
public HasStudyingSchedule(StudentDescriptor descriptor)
{
m_Descriptor = descriptor;
}
public ICanBeBuilt WithSchedule(params SubjectSchedule[] subjectsSchedules)
{
if (m_Descriptor.Subjects.Any(s => !subjectsSchedules.Select(ss => ss.Subject).Contains(s)))
{
throw new ArgumentException("Some of the registered subjects are not scheduled.");
}
if (subjectsSchedules.Select(ss => ss.Subject).Any(s => !m_Descriptor.Subjects.Contains(s)))
{
throw new ArgumentException("Some of the scheduled subjects are not registered.");
}
var clone = new StudentDescriptor(m_Descriptor)
{
SubjectsSchedules = subjectsSchedules.AsEnumerable().Clone().ToList()
};
return new CanBeBuilt(clone);
}
}
}
Moving on to define the HasTeachingSchedule
class implementing the IHasTeachingSchedule
interface. So, create a 06.HasTeachingSchedule.cs file, and define the class as follows:
using System.Linq;
using FluentApi.Builder.Dtos;
using FluentApi.Builder.Dtos.Descriptors;
using FluentApi.Builder.Interfaces;
namespace FluentApi.Builder.Implementations
{
internal class HasTeachingSchedule : IHasTeachingSchedule
{
private readonly TeacherDescriptor m_Descriptor;
public HasTeachingSchedule(TeacherDescriptor descriptor)
{
m_Descriptor = descriptor;
}
public ICanBeBuilt WithSchedule(params Schedule[] schedules)
{
var clone = new TeacherDescriptor(m_Descriptor)
{
Schedules = schedules.AsEnumerable().Clone().ToList()
};
return new CanBeBuilt(clone);
}
}
}
Moving on to define the CanBeBuilt
class implementing the ICanBeBuilt
interface. So, create a 07.CanBeBuilt.cs file, and define the class as follows:
using FluentApi.Builder.Dtos.Descriptors;
using FluentApi.Builder.Interfaces;
namespace FluentApi.Builder.Implementations
{
internal class CanBeBuilt : ICanBeBuilt
{
private readonly MemberDescriptor m_Descriptor;
public CanBeBuilt(MemberDescriptor descriptor)
{
m_Descriptor = descriptor;
}
public MemberDescriptor Build()
{
return m_Descriptor.Clone();
}
}
}
What to notice here is that the constructor is expecting a MemberDescriptor
as at this point the passed in MemberDescriptor
could be a TeacherDescriptor
or a StudentDescriptor
.
Also, on the Build
method we are returning a clone of the descriptor but this time we can’t use the cloning constructor as if you use the cloning constructor of the MemberDescriptor
class, you would finally return an instance of MemberDescriptor
, neither a TeacherDescriptor
nor a StudentDescriptor
which is not right. Instead, we use the Clone
method which would return the right instance at run-time.
using System;
using FluentApi.Builder.Dtos;
using FluentApi.Builder.Implementations;
namespace FluentApi
{
class Program
{
static void Main(string[] args)
{
var memberBuilder = new MemberBuilder();
var ahmed =
memberBuilder
.New
.WithName("Ahmed")
.WithAge(36)
.AsTeacher
.Teaching(new Subject("Software Engineering"))
.WithSchedule(
new Schedule
(
new DateTime(2021, 11, 20),
new DateTime(2021, 12, 20)
), new Schedule
(
new DateTime(2022, 1, 5),
new DateTime(2021, 3, 5)
))
.Build();
var subjectsToStudy = new Subject[]
{
new Subject("Software Engineering"),
new Subject("Physics")
};
var mohamed =
memberBuilder
.New
.WithAge(15)
.WithName("Mohamed")
.AsStudent
.Studying(subjectsToStudy)
.WithSchedule
(
new SubjectSchedule
(
subjectsToStudy[0],
new Schedule
(
new DateTime(2021, 11, 20),
new DateTime(2021, 12, 20)
)
),
new SubjectSchedule
(
subjectsToStudy[1],
new Schedule
(
new DateTime(2021, 11, 20),
new DateTime(2021, 12, 20)
)
)
)
.Build();
Console.ReadLine();
}
}
}
Also Published Here