Search code examples
c#design-patternsbuilderfluent-interfacemethod-chaining

Conditional Builder Method Chaining Fluent Interface


I was wondering what would be the best way to implement a .When condition in a fluent interface using method chaining in a Builder object?

For instance how would I implement the .WithSkill() and .When() methods in the following example:

var level = 5;

var ninja = NinjaBuilder
    .CreateNinja()
    .Named("Ninja Boy")
    .AtLevel(level)
    .WithShurikens(10)
    .WithSkill(Skill.HideInShadows)
        .When(level > 3)
    .Build()

Update - A sample solution can be found here.


Solution

  • What I'd do is have NinjaBuilder keep the operations as a list of delegates, rather than applying them, and only apply them when .Build is called. This would allow you to make them conditional:

    public class NinjaBuilder { 
        List<Action<Ninja>> builderActions = new List<Action<Ninja>>();
    
        public Ninja Build() {
            var ninja = new Ninja();
            builderActions.ForEach(ba => ba(ninja));
            return ninja;
        }
    
        public NinjaBuilder WithShurikens(int numShirukens) {
            builderActions.Add(n=>n.Shirukens = numShirukens);
            return this;
        }
    
        public NinjaBuilder When(Boolean condition) {
            if (!condition) // If the condition is not met, remove the last action
                builderActions.RemoveAt(builderActions.Length - 1);
            return this;
        }
    }
    

    Of course, this assumes that the condition is constant at the time of builder creation. If you want to make it non-constant, you could do something like this instead:

        public NinjaBuilder When(Func<Boolean> condition) {
            var oldAction = builderActions[builderActions.Length - 1];
            builderActions[builderActions.Length - 1] = n => { if (condition()) { oldAction(n); } }
            return this;
        }
    

    If you want When be somewhat more compiler checked, you can make builderActions protected and do something like this:

    public class ConditionalNinjaBuilder : NinjaBuilder {
        public ConditionalNinjaBuilder(NinjaBuilder wrappedBuilder) {            
            // Since someone might call .WithShirukens on the wrapping
            // builder directly, we should make sure that our actions 
            // list is the same instance as the one in our wrapped builder
            builderActions = wrappedBuilder.builderActions;
        }
    
        public ConditionalNinjaBuilder When(Func<Boolean> condition) {
            var oldAction = builderActions[builderActions.Length - 1];
            builderActions[builderActions.Length - 1] = n => { if (condition()) { oldAction(n); } }
            return this;
        }
    }
    

    and have the original operations return a ConditionalNinjaBuilder:

        public ConditionalNinjaBuilder WithShurikens(int numShirukens) {
            builderActions.Add(n=>n.Shirukens = numShirukens);
            return new ConditionalNinjaBuilder(this);
        }
    

    That way you can only call .When after first calling another method. This has the additional advantage/complication of potentially allowing for nested/compounded conditionals, too. Yikes.