Search code examples
c#fluentfluent-interface

Fluent interface building different concrete types


I need some suggestions on how to build a fluent interface acting as a Builder, responsible for returning different concrete types depending on the methods called.

Imagine that I need to create one of the following types using my ProductBuilder (fluently): Product, ServiceProduct, PackagedProduct (both derived from Product).

I'm thinking of using a fluent syntax like this (other suggestions are more than welcome):

To create a Product:

var product = new ProductBuilder()
   .Create("Simple product")
   .WithPrice(12.5)

To create a ServiceProduct

var product = new ProductBuilder()
   .Create("Service product")
   .WithPrice(12.5)
   .AsServiceProduct()
       .ServiceProductSpecificMethods...()

And PackagedProduct with a call to AsPackagedProduct() instead of AsServiceProduct() etc. You get the idea.

I haven't found a sample that shows best practices on this. Only samples where the final build returns the same type.

Any suggestions?


Solution

  • I see two options here.

    If there are a finite number of products that are fixed, and not designed to be extended, then just create a Create method for each product:

    var product = new ProductBuilder()
       .CreateSimple()
       .WithPrice(12.5);
    
    var product = new ProductBuilder()
       .CreateService()
       .WithPrice(12.5)
       .ServiceProductSpecificMethods...();
    

    If you don't want (or can't have) ProductBuilder to know all of the types of products, then I would use generics:

    public class Product {}
    public class SimpleProduct : Product {}
    public class ServiceProduct : Product {}
    
    var product = new ProductBuilder<SimpleProduct>()
       .WithPrice(12.5);
    

    Here's a starting place for the design to follow:

    public class Product
    {
        public decimal Price { get; set; }
    }
    public class SimpleProduct : Product { }
    public class ServiceProduct : Product
    {
        public string Service { get; set; }
    }
    
    public class ProductBuilder<T> where T : Product, new()
    {
        private List<Action<T>> actions = new List<Action<T>>();
    
        public T Build()
        {
            T product = new T();
            foreach (var action in actions)
            {
                action(product);
            }
    
            return product;
        }
        public void Configure(Action<T> action)
        {
            actions.Add(action);
        }
    }
    
    public static class ProductExtensions
    {
        public static ProductBuilder<T> WithPrice<T>(this ProductBuilder<T> builder, decimal price)
            where T : Product
        {
            builder.Configure(product => product.Price = price);
            return builder;
        }
    
        public static ProductBuilder<T> WithService<T>(this ProductBuilder<T> builder, string service)
                where T : ServiceProduct
        {
            builder.Configure(product => product.Service = service);
            return builder;
        }
    }