In our previous blog, we examined the Stepwise builder, where following steps is a requirement for the pattern and we attempt to drive object creation by following some steps one at a time. In this blog, we will examine the Functional builder, another variant where we avoid class extension through inheritance and try to functionally extend the existing builder class either by using more functional ways like extensions or functional paradigms.
Reusing the existing Employee Class will enable us to create a builder that gives users the ability to create the class's objects.
public class Employee
{
public Guid Id { get; set; }
public decimal Salary { get; set; }
}
Let's create a functional builder right away to generate an employee id, and then we'll talk about how we did it in more detail.
public sealed class EmployeeBuilder
{
public readonly List<Func<Employee, Employee>> actions = new List<Func<Employee, Employee>>();
public EmployeeBuilder GenerateEmployeeId()
{
Perform(e => e.Id = Guid.NewGuid());
return this;
}
public Employee Build()
{
return actions.Aggregate(new Employee(), (e, func) => func(e));
}
public EmployeeBuilder Perform(Action<Employee> action)
{
actions.Add(e =>
{
action(e);
return e;
});
return this;
}
}
Now, in order to allow users to pass Lambda functions to modify Employee object values, we are using List>. We can chain multiple methods on the Employee object in a single action if we follow fluency, which is ensured by the second parameter.
By declaring the Perform method, we are giving the user an API so they can add actions to our read-only list of actions (used to store all actions so we can execute them one at a time on the same object passed). However, since this method is public, we can actually use it later to perform more actions. For example, in the "GenerateEmployeeId()" method, we use the "Perform()" method to create an employee's unique ID.
Let's reuse our builder now, adhering to the Open-Close principle so that we don't have to modify it but rather extend it to prevent inheritance. I purposefully added the "sealed" keyword to the class to mimic the requirement where we are not allowed to extend classes in order to prevent anyone from inheriting the class and to implement a functional way to extend our Builder. Let's look at how we can extend that with the help of an example.
public static class EmployeeBuilderExtensions
{
public static EmployeeBuilder WithSalary(this EmployeeBuilder builder, decimal salary)
{
builder.Perform(b => b.Salary = salary);
return builder;
}
}
Consider how easy it is to add new functionality to an existing builder. We can also perform different actions while creating the objects, and we can construct the final object by aggregating all the specified actions on the same object.
Therefore, if we need to extend an existing builder class but are unable to inherit from it because of some business requirements, we can still benefit from the open-closed principle by using this functional approach.
You'll notice that EmployeeBuilder contains redundant code, and if we later need to create a Builder for another class, we'll have to repeat the same code, which is bad because we should extract the same code and avoid redundancy. In this case, we can use generics to extract the code for multiple types (i.e., for multiple classes), so let's take a look at how we can do that.
public abstract class FunctionalBuilder<TBuilder, TCurrent>
where TCurrent : FunctionalBuilder<TBuilder, TCurrent>
where TBuilder : new()
{
public readonly List<Func<TBuilder, TBuilder>> actions = new List<Func<TBuilder, TBuilder>>();
public TBuilder Build()
{
return actions.Aggregate(new TBuilder(), (b, func) => func(b));
}
public TCurrent Perform(Action<TBuilder> action)
{
actions.Add(e =>
{
action(e);
return e;
});
return (TCurrent)this;
}
}
After putting the functionality into a generic class, we can now create actual builders from this by deriving from the abstract "FunctionalBuilder" class, where “TBuilder” is the type for which we want to create a Builder and “TCurrent” is the instance of a Builder we are creating. By returning the same builder type, this will enable us to achieve fluency. For instance, if we create an "EmployeeBuilder," all methods from "FunctionalBuilder" will return "EmployeeBuilder," enabling fluency.
Let's try to implement a builder for our Employee class using this generic class, where we should be able to create employee objects with EmployeeId and Salary.
public class EmployeeBuilder : FunctionalBuilder<Employee, EmployeeBuilder>
{
public EmployeeBuilder GenerateEmployeeId()
{
Perform(e => e.Id = Guid.NewGuid());
return this;
}
public EmployeeBuilder WithSalary(decimal salary)
{
Perform(e => e.Salary = salary);
return this;
}
}
And in our primary method, we can apply this as follows:
public class FunctionalBuilderMain
{
public static void Main(string[] args)
{
var employee = new EmployeeBuilder()
.GenerateEmployeeId()
.WithSalary(2000)
.Build();
Console.WriteLine($"Employee created with Id: {employee.Id} and Salary : ${employee.Salary}");
}
}
After combining the above code snippets, we'll get something like this, which we can start using to create/extend further by introducing new Builders.
namespace Builder.FunctionalBuilder
{
public class Employee
{
public Guid Id { get; set; }
public decimal Salary { get; set; }
}
public abstract class FunctionalBuilder<TBuilder, TCurrent>
where TCurrent : FunctionalBuilder<TBuilder, TCurrent>
where TBuilder : new()
{
public readonly List<Func<TBuilder, TBuilder>> actions = new List<Func<TBuilder, TBuilder>>();
public TBuilder Build()
{
return actions.Aggregate(new TBuilder(), (b, func) => func(b));
}
public TCurrent Perform(Action<TBuilder> action)
{
actions.Add(e =>
{
action(e);
return e;
});
return (TCurrent)this;
}
}
public class EmployeeBuilder : FunctionalBuilder<Employee, EmployeeBuilder>
{
public EmployeeBuilder GenerateEmployeeId()
{
Perform(e => e.Id = Guid.NewGuid());
return this;
}
public EmployeeBuilder WithSalary(decimal salary)
{
Perform(e => e.Salary = salary);
return this;
}
}
public class FunctionalBuilderMain
{
public static void Main(string[] args)
{
var employee = new EmployeeBuilder()
.GenerateEmployeeId()
.WithSalary(2000)
.Build();
Console.WriteLine($"Employee created with Id: {employee.Id} and Salary : ${employee.Salary}");
}
}
}
Finally, I just wanted to encourage people to start using functional builders when they want to extend their builders but can't use inheritance. In our next blog, we'll look at how we can add extra abstraction over the builders by introducing some masking patterns.
Happy Coding..!!!
Top comments (0)