Motivation
Having the object in a properly state was a huge and important part of object oriented programming. Meanwhile, it's hard to create a object instance which have a lot of properties with some business restrictions. Object initialization was result in clutter code like:
var car = new Car();
car.Make = "Honda";
if(make != "Honda"){
car.Make = "Not Honda";
}
if(year < 2000) {
throw NotSupportYearException(year);
}
car.year = year;
//-- ommitted for short --
car.Save();
We can make life easier with builder pattern. Even more, we can make it extensible with C# extension method.
How fluent API looks like
var car = new CarBuilder()
.AddMake("Honda")
.AddYear("2020")
.AddMileage(20000)
.AddPrice(20000m)
.Build();
Console.WriteLine(car);
How to add more APIs as need
We can add more fluent APIs to initialize the Car
by attaching C# extension methods to ICarBuilder
. For instance, after we shipped CarBuilder
, we realized that it's better to have AddModel
to help initializing the Car
. We can add extension method like:
public static class CustomizedCarBuilderExts
{
public static ICarBuilder AddModel(this ICarBuilder builder, string model)
{
builder.Car.Model = model;
return builder;
}
}
Code
Key parts:
ICarBuilder
serve following purposes:
- Return refered
Car
which we can operate it. - We can attach extension method to
ICarBuilder
to extend existing API later. Build
method to add extra actions and return fully built Car. It's a convention method. we can use referred Car directly.
CarBuilder
serve as container to hold the Car.
The ASP.NET Core havily used this way to properly initialize the objects at startup. eg: CreateWebHostBuilder
;
using System;
namespace Zjy.Utils
{
public class Car {
public string Make {get; set;}
public string Model {get; set;}
public string Year {get; set;}
public int Mileage {get; set;}
public decimal Price {get; set;}
public void Save() {
Console.WriteLine("Saved!");
}
public override string ToString() {
return $"{Make} {Year} with mileage {Mileage} at Price: ${Price}";
}
}
# region Builder Pattern
public interface ICarBuilder
{
Car Car { get; }
Car Build();
}
public class CarBuilder : ICarBuilder
{
private Car _car = null;
public CarBuilder()
{
_car = new Car();
}
public Car Car => this._car;
public Car Build()
{
this._car.Save();
return this._car;
}
}
# endregion
# region Extension Methods
/// <summary>
/// We can add customized extensions to ICarBuilder.
/// Even if we shipped following piece of code.
/// </summary>
public static class CarBuilderExtentions
{
public static ICarBuilder AddPrice(this ICarBuilder builder, decimal price)
{
builder.Car.Price = price;
return builder;
}
public static ICarBuilder AddMileage(this ICarBuilder builder, int mileage)
{
builder.Car.Mileage = mileage;
return builder;
}
public static ICarBuilder AddYear(this ICarBuilder builder, string year)
{
builder.Car.Year = year;
return builder;
}
public static ICarBuilder AddMake(this ICarBuilder builder, string make)
{
builder.Car.Make = make;
return builder;
}
}
# endregion
# region Usage
public class Program {
public static void Main(string[] args) {
var car = new CarBuilder()
.AddMake("Honda")
.AddYear("2020")
.AddMileage(20000)
.AddPrice(20000m)
.Build();
Console.WriteLine(car);
}
}
# endregion
}
Comments: