Extensible fluent API with builder pattern and extension methods

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:

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
}
@ 2020-04-18 10:58

Comments:

Sharing your thoughts: