Conditional Builder Method Chaining Fluent Interface

戏子无情 提交于 2019-11-28 02:46:23

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.

I have a solution to interface chaining; the only problem with my solution is that it grows in complexity (scale) with every new method you want to support. But, it makes for a really awesome API for the user.

Let us consider that you have 3 methods, A, B, and C, and you want to use them in a chain.

Let us also consider that you don't want to be able to call any method more than once.

e.g.

new Builder().A().B().C(); // OK
new Builder().A().B().A(); // Not OK

This can be accomplished with some serious awesomeness:

public class Builder : A<Not_A>, B<Not_B>, C<Not_C>, Not_A, Not_B, Not_C, Not_AB, Not_BC, Not_AC, Empty
{
  Not_AB A<Not_AB>.A() { return (Not_AB)A(); }
  Not_AC A<Not_AC>.A() { return (Not_AC)A(); }
  Empty A<Empty>.A() { return (Empty)A(); }
  public Not_A A()
  {
    return (Not_A)this;
  }

  Not_AB B<Not_AB>.B() { return (Not_AB)B(); }
  Not_BC B<Not_BC>.B() { return (Not_BC)B(); }
  Empty B<Empty>.B() { return (Empty)B(); }
  public Not_B B()
  {
    return (Not_B)this;
  }

  Not_AC C<Not_AC>.C() { return (Not_AC)C(); }
  Not_BC C<Not_BC>.C() { return (Not_BC)C(); }
  Empty C<Empty>.C() { return (Empty)C(); }
  public Not_C C()
  {
    return (Not_C)this;
  }
}

public interface Empty { }

public interface A<TRemainder> { TRemainder A(); }
public interface B<TRemainder> { TRemainder B(); }
public interface C<TRemainder> { TRemainder C(); }

public interface Not_A : B<Not_AB>, C<Not_AC> { }
public interface Not_B : A<Not_AB>, C<Not_BC> { }
public interface Not_C : A<Not_AC>, B<Not_BC> { }

public interface Not_AB : C<Empty> { }
public interface Not_BC : A<Empty> { }
public interface Not_AC : B<Empty> { }

And then, mix this with Chris Shain's awesomeness to use a stack of actions!

I decided to implement it. Note that you can't call any method twice now with this chaining solution. I put your When method as an extension method.

Here is the calling code:

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

Here is my Ninja and Skill classes:

public class Ninja
{
  public string Name { get; set; }
  public int Level { get; set; }
  public int Shurikens { get; set; }
  public Skill Skill { get; set; }
}

public enum Skill
{
  None = 1,
  HideInShadows
}

This is the NinjaBuilder class:

public class NinjaBuilder : NinjaBuilder_Sans_Named
{
  public static NinjaBuilder CreateNinja() { return new NinjaBuilder(); }
  public Stack<Action<Ninja>> _buildActions;

  public NinjaBuilder()
  {
    _buildActions = new Stack<Action<Ninja>>();
  }

  public override Ninja Build()
  {
    var ninja = new Ninja();
    while (_buildActions.Count > 0)
    {
      _buildActions.Pop()(ninja);
    }

    return ninja;
  }

  public override void AddCondition(Func<Ninja, bool> condition)
  {
    if (_buildActions.Count == 0)
      return;

    var top = _buildActions.Pop();
    _buildActions.Push(n => { if (condition(n)) { top(n); } });
  }

  public override Sans_Named_NinjaBuilder Named(string name)
  {
    _buildActions.Push(n => n.Name = name);
    return this;
  }

  public override Sans_AtLevel_NinjaBuilder AtLevel(int level)
  {
    _buildActions.Push(n => n.Level = level);
    return this;
  }

  public override Sans_WithShurikens_NinjaBuilder WithShurikens(int shurikenCount)
  {
    _buildActions.Push(n => n.Shurikens = shurikenCount);
    return this;
  }

  public override Sans_WithSkill_NinjaBuilder WithSkill(Skill skillType)
  {
    _buildActions.Push(n => n.Skill = skillType);
    return this;
  }
}

And the rest of this code is just overhead to make the conversions and calls work:

public abstract class NinjaBuilderBase :
  EmptyNinjaBuilder,
  Named_NinjaBuilder<Sans_Named_NinjaBuilder>,
  AtLevel_NinjaBuilder<Sans_AtLevel_NinjaBuilder>,
  WithShurikens_NinjaBuilder<Sans_WithShurikens_NinjaBuilder>,
  WithSkill_NinjaBuilder<Sans_WithSkill_NinjaBuilder>
{
  public abstract void AddCondition(Func<Ninja, bool> condition);
  public abstract Ninja Build();

  public abstract Sans_WithSkill_NinjaBuilder WithSkill(Skill skillType);
  public abstract Sans_WithShurikens_NinjaBuilder WithShurikens(int shurikenCount);
  public abstract Sans_AtLevel_NinjaBuilder AtLevel(int level);
  public abstract Sans_Named_NinjaBuilder Named(string name);
}

public abstract class NinjaBuilder_Sans_WithSkill : NinjaBuilderBase,
  Sans_WithSkill_NinjaBuilder
{
  Sans_Named_WithSkill_NinjaBuilder Named_NinjaBuilder<Sans_Named_WithSkill_NinjaBuilder>.Named(string name) { return (Sans_Named_WithSkill_NinjaBuilder)Named(name); }
  Sans_AtLevel_WithSkill_NinjaBuilder AtLevel_NinjaBuilder<Sans_AtLevel_WithSkill_NinjaBuilder>.AtLevel(int level) { return (Sans_AtLevel_WithSkill_NinjaBuilder)AtLevel(level); }
  Sans_WithShurikens_WithSkill_NinjaBuilder WithShurikens_NinjaBuilder<Sans_WithShurikens_WithSkill_NinjaBuilder>.WithShurikens(int shurikenCount) { return (Sans_WithShurikens_WithSkill_NinjaBuilder)WithShurikens(shurikenCount); }
}

public abstract class NinjaBuilder_Sans_WithShurikens : NinjaBuilder_Sans_WithSkill,
  Sans_WithShurikens_NinjaBuilder,
  Sans_WithShurikens_WithSkill_NinjaBuilder
{
  Sans_Named_WithShurikens_WithSkill_NinjaBuilder Named_NinjaBuilder<Sans_Named_WithShurikens_WithSkill_NinjaBuilder>.Named(string name) { return (Sans_Named_WithShurikens_WithSkill_NinjaBuilder)Named(name); }
  Sans_AtLevel_WithShurikens_WithSkill_NinjaBuilder AtLevel_NinjaBuilder<Sans_AtLevel_WithShurikens_WithSkill_NinjaBuilder>.AtLevel(int level) { return (Sans_AtLevel_WithShurikens_WithSkill_NinjaBuilder)AtLevel(level); }
  Sans_Named_WithSkill_NinjaBuilder Named_NinjaBuilder<Sans_Named_WithSkill_NinjaBuilder>.Named(string name) { return (Sans_Named_WithSkill_NinjaBuilder)Named(name); }
  Sans_AtLevel_WithShurikens_NinjaBuilder AtLevel_NinjaBuilder<Sans_AtLevel_WithShurikens_NinjaBuilder>.AtLevel(int level) { return (Sans_AtLevel_WithShurikens_NinjaBuilder)AtLevel(level); }
  Sans_WithShurikens_WithSkill_NinjaBuilder WithSkill_NinjaBuilder<Sans_WithShurikens_WithSkill_NinjaBuilder>.WithSkill(Skill skillType) { return (Sans_WithShurikens_WithSkill_NinjaBuilder)WithSkill(skillType); }
}

public abstract class NinjaBuilder_Sans_AtLevel : NinjaBuilder_Sans_WithShurikens,
  Sans_AtLevel_NinjaBuilder,
  Sans_AtLevel_WithShurikens_NinjaBuilder,
  Sans_AtLevel_WithSkill_NinjaBuilder,
  Sans_AtLevel_WithShurikens_WithSkill_NinjaBuilder
{
  EmptyNinjaBuilder Named_NinjaBuilder<EmptyNinjaBuilder>.Named(string name) { return Named(name); }
  Sans_Named_AtLevel_WithSkill_NinjaBuilder Named_NinjaBuilder<Sans_Named_AtLevel_WithSkill_NinjaBuilder>.Named(string name) { return (Sans_Named_AtLevel_WithSkill_NinjaBuilder)Named(name); }
  Sans_AtLevel_WithShurikens_WithSkill_NinjaBuilder WithShurikens_NinjaBuilder<Sans_AtLevel_WithShurikens_WithSkill_NinjaBuilder>.WithShurikens(int shurikenCount) { return (Sans_AtLevel_WithShurikens_WithSkill_NinjaBuilder)WithShurikens(shurikenCount); }
  Sans_Named_AtLevel_WithShurikens_NinjaBuilder Named_NinjaBuilder<Sans_Named_AtLevel_WithShurikens_NinjaBuilder>.Named(string name) { return (Sans_Named_AtLevel_WithShurikens_NinjaBuilder)Named(name); }
  Sans_AtLevel_WithShurikens_WithSkill_NinjaBuilder WithSkill_NinjaBuilder<Sans_AtLevel_WithShurikens_WithSkill_NinjaBuilder>.WithSkill(Skill skillType) { return (Sans_AtLevel_WithShurikens_WithSkill_NinjaBuilder)WithSkill(skillType); }
  Sans_Named_AtLevel_NinjaBuilder Named_NinjaBuilder<Sans_Named_AtLevel_NinjaBuilder>.Named(string name) { return (Sans_Named_AtLevel_NinjaBuilder)Named(name); }
  Sans_AtLevel_WithShurikens_NinjaBuilder WithShurikens_NinjaBuilder<Sans_AtLevel_WithShurikens_NinjaBuilder>.WithShurikens(int shurikenCount) { return (Sans_AtLevel_WithShurikens_NinjaBuilder)WithShurikens(shurikenCount); }
  Sans_AtLevel_WithSkill_NinjaBuilder WithSkill_NinjaBuilder<Sans_AtLevel_WithSkill_NinjaBuilder>.WithSkill(Skill skillType) { return (Sans_AtLevel_WithSkill_NinjaBuilder)WithSkill(skillType); }
}

public abstract class NinjaBuilder_Sans_Named : NinjaBuilder_Sans_AtLevel,
  Sans_Named_NinjaBuilder,
  Sans_Named_AtLevel_NinjaBuilder,
  Sans_Named_WithShurikens_NinjaBuilder,
  Sans_Named_WithSkill_NinjaBuilder,
  Sans_Named_WithShurikens_WithSkill_NinjaBuilder,
  Sans_Named_AtLevel_WithSkill_NinjaBuilder,
  Sans_Named_AtLevel_WithShurikens_NinjaBuilder
{
  EmptyNinjaBuilder WithSkill_NinjaBuilder<EmptyNinjaBuilder>.WithSkill(Skill skillType) { return (EmptyNinjaBuilder)WithSkill(skillType); }
  EmptyNinjaBuilder WithShurikens_NinjaBuilder<EmptyNinjaBuilder>.WithShurikens(int shurikenCount) { return (EmptyNinjaBuilder)WithShurikens(shurikenCount); }
  EmptyNinjaBuilder AtLevel_NinjaBuilder<EmptyNinjaBuilder>.AtLevel(int level) { return (EmptyNinjaBuilder)AtLevel(level); }
  Sans_Named_AtLevel_WithShurikens_NinjaBuilder AtLevel_NinjaBuilder<Sans_Named_AtLevel_WithShurikens_NinjaBuilder>.AtLevel(int level) { return (Sans_Named_AtLevel_WithShurikens_NinjaBuilder)AtLevel(level); }
  Sans_Named_WithShurikens_WithSkill_NinjaBuilder WithShurikens_NinjaBuilder<Sans_Named_WithShurikens_WithSkill_NinjaBuilder>.WithShurikens(int shurikenCount) { return (Sans_Named_WithShurikens_WithSkill_NinjaBuilder)WithShurikens(shurikenCount); }
  Sans_Named_WithShurikens_WithSkill_NinjaBuilder WithSkill_NinjaBuilder<Sans_Named_WithShurikens_WithSkill_NinjaBuilder>.WithSkill(Skill skillType) { return (Sans_Named_WithShurikens_WithSkill_NinjaBuilder)WithSkill(skillType); }
  Sans_Named_AtLevel_WithShurikens_NinjaBuilder WithShurikens_NinjaBuilder<Sans_Named_AtLevel_WithShurikens_NinjaBuilder>.WithShurikens(int shurikenCount) { return (Sans_Named_AtLevel_WithShurikens_NinjaBuilder)WithShurikens(shurikenCount); }
  Sans_Named_AtLevel_WithSkill_NinjaBuilder WithSkill_NinjaBuilder<Sans_Named_AtLevel_WithSkill_NinjaBuilder>.WithSkill(Skill skillType) { return (Sans_Named_AtLevel_WithSkill_NinjaBuilder)WithSkill(skillType); }
  Sans_Named_AtLevel_NinjaBuilder AtLevel_NinjaBuilder<Sans_Named_AtLevel_NinjaBuilder>.AtLevel(int level) { return (Sans_Named_AtLevel_NinjaBuilder)AtLevel(level); }
  Sans_Named_WithShurikens_NinjaBuilder WithShurikens_NinjaBuilder<Sans_Named_WithShurikens_NinjaBuilder>.WithShurikens(int shurikenCount) { return (Sans_Named_WithShurikens_NinjaBuilder)WithShurikens(shurikenCount); }
  Sans_Named_WithSkill_NinjaBuilder WithSkill_NinjaBuilder<Sans_Named_WithSkill_NinjaBuilder>.WithSkill(Skill skillType) { return (Sans_Named_WithSkill_NinjaBuilder)WithSkill(skillType); }
}

public static class NinjaBuilderExtension
{
  public static TBuilderLevel When<TBuilderLevel>(this TBuilderLevel ths, Func<Ninja, bool> condition) where TBuilderLevel : EmptyNinjaBuilder
  {
    ths.AddCondition(condition);
    return ths;
  }
}

public interface EmptyNinjaBuilder { void AddCondition(Func<Ninja, bool> condition); Ninja Build(); }

public interface Named_NinjaBuilder<TRemainder> { TRemainder Named(string name); }
public interface AtLevel_NinjaBuilder<TRemainder> { TRemainder AtLevel(int level);}
public interface WithShurikens_NinjaBuilder<TRemainder> { TRemainder WithShurikens(int shurikenCount); }
public interface WithSkill_NinjaBuilder<TRemainder> { TRemainder WithSkill(Skill skillType); }

// level one reductions
public interface Sans_Named_NinjaBuilder :
  AtLevel_NinjaBuilder<Sans_Named_AtLevel_NinjaBuilder>,
  WithShurikens_NinjaBuilder<Sans_Named_WithShurikens_NinjaBuilder>,
  WithSkill_NinjaBuilder<Sans_Named_WithSkill_NinjaBuilder>,
  EmptyNinjaBuilder { }
public interface Sans_AtLevel_NinjaBuilder :
  Named_NinjaBuilder<Sans_Named_AtLevel_NinjaBuilder>,
  WithShurikens_NinjaBuilder<Sans_AtLevel_WithShurikens_NinjaBuilder>,
  WithSkill_NinjaBuilder<Sans_AtLevel_WithSkill_NinjaBuilder>,
  EmptyNinjaBuilder { }
public interface Sans_WithShurikens_NinjaBuilder :
  Named_NinjaBuilder<Sans_Named_WithSkill_NinjaBuilder>,
  AtLevel_NinjaBuilder<Sans_AtLevel_WithShurikens_NinjaBuilder>,
  WithSkill_NinjaBuilder<Sans_WithShurikens_WithSkill_NinjaBuilder>,
  EmptyNinjaBuilder { }
public interface Sans_WithSkill_NinjaBuilder :
  Named_NinjaBuilder<Sans_Named_WithSkill_NinjaBuilder>,
  AtLevel_NinjaBuilder<Sans_AtLevel_WithSkill_NinjaBuilder>,
  WithShurikens_NinjaBuilder<Sans_WithShurikens_WithSkill_NinjaBuilder>,
  EmptyNinjaBuilder { }

// level two reductions
// Named
public interface Sans_Named_AtLevel_NinjaBuilder :
  WithShurikens_NinjaBuilder<Sans_Named_AtLevel_WithShurikens_NinjaBuilder>,
  WithSkill_NinjaBuilder<Sans_Named_AtLevel_WithSkill_NinjaBuilder>,
  EmptyNinjaBuilder { }
public interface Sans_Named_WithShurikens_NinjaBuilder :
  AtLevel_NinjaBuilder<Sans_Named_AtLevel_WithShurikens_NinjaBuilder>,
  WithSkill_NinjaBuilder<Sans_Named_WithShurikens_WithSkill_NinjaBuilder>,
  EmptyNinjaBuilder { }
public interface Sans_Named_WithSkill_NinjaBuilder :
  AtLevel_NinjaBuilder<Sans_Named_AtLevel_WithShurikens_NinjaBuilder>,
  WithShurikens_NinjaBuilder<Sans_Named_WithShurikens_WithSkill_NinjaBuilder>,
  EmptyNinjaBuilder { }
// AtLevel
public interface Sans_AtLevel_WithShurikens_NinjaBuilder :
  Named_NinjaBuilder<Sans_Named_AtLevel_WithShurikens_NinjaBuilder>,
  WithSkill_NinjaBuilder<Sans_AtLevel_WithShurikens_WithSkill_NinjaBuilder>,
  EmptyNinjaBuilder { }
public interface Sans_AtLevel_WithSkill_NinjaBuilder :
  Named_NinjaBuilder<Sans_Named_AtLevel_WithSkill_NinjaBuilder>,
  WithShurikens_NinjaBuilder<Sans_AtLevel_WithShurikens_WithSkill_NinjaBuilder>,
  EmptyNinjaBuilder { }
// WithShurikens
public interface Sans_WithShurikens_WithSkill_NinjaBuilder :
  Named_NinjaBuilder<Sans_Named_WithShurikens_WithSkill_NinjaBuilder>,
  AtLevel_NinjaBuilder<Sans_AtLevel_WithShurikens_WithSkill_NinjaBuilder>,
  EmptyNinjaBuilder { }

// level three reductions
// Named
public interface Sans_AtLevel_WithShurikens_WithSkill_NinjaBuilder :
  Named_NinjaBuilder<EmptyNinjaBuilder>,
  EmptyNinjaBuilder { }
// AtLevel
public interface Sans_Named_WithShurikens_WithSkill_NinjaBuilder :
  AtLevel_NinjaBuilder<EmptyNinjaBuilder>,
  EmptyNinjaBuilder { }
// WithShurikens
public interface Sans_Named_AtLevel_WithSkill_NinjaBuilder :
  WithShurikens_NinjaBuilder<EmptyNinjaBuilder>,
  EmptyNinjaBuilder { }
// WithSkill
public interface Sans_Named_AtLevel_WithShurikens_NinjaBuilder :
  WithSkill_NinjaBuilder<EmptyNinjaBuilder>,
  EmptyNinjaBuilder { }

You could consider writing overloaded versions of With, and in the second, take a Where as an argument:

var level = 5;  
var ninja = NinjaBuilder     
     .CreateNinja()
     .Named("Ninja Boy")
     .AtLevel(level)
     .WithShurikens(10)
     .WithSkill(Skill.HideInShadows, Where.Level(l => l > 3))
     .Build() 

Of course, this is predicated on the notion that you're going to write Where as a separate object entirely, that essentially looks like this:

public sealed static class Where
{
    public bool Defense (Func<int, bool> predicate) { return predicate(); }
    public bool Dodge (Func<int, bool> predicate) { return predicate(); }
    public bool Level (Func<int, bool> predicate) { return predicate(); }

}
Jordão

You could have a conditional optional parameter in your method that is true by default:

.WithSkill(Skill.HideInShadows, when: level > 3)

This will of course be very specific to the WithSkill method:

public NinjaBuilder WithSkill(Skill skill, bool when = true) {
  if (!when) return this;
  // ...
}

You could add it to other methods that you want to be conditional too.

Another option is to have a method that nests the conditional parts of the builder:

public NinjaBuilder When(bool condition, Action<NinjaBuilder> then) {
  if (condition) then(this);
  return this;
}

Then you can write it like this:

.When(level > 3, 
  then: _ => _.WithSkill(Skill.HideInShadows))

Or like this:

.When(level > 3, _=>_
  .WithSkill(Skill.HideInShadows)
)

This is more generic and can be used with any methods of the builder.

You can even add an optional "else":

public NinjaBuilder When(bool condition, Action<NinjaBuilder> then, Action<NinjaBuilder> otherwise = null) {
  if (condition) {
    then(this);
  }
  else if (otherwise != null) {
    otherwise(this);
  }
  return this;
}

Or, as a "mixin":

public interface MBuilder {}
public static class BuilderExtensions {
  public static TBuilder When<TBuilder>(this TBuilder self, bool condition, Action<TBuilder> then, Action<TBuilder> otherwise = null)
  where TBuilder : MBuilder
  {
    if (condition) {
      then(self);
    }
    else if (otherwise != null) {
      otherwise(self);
    }
    return self;
  }
}

public class NinjaBuilder : MBuilder ...

This is of course a way to create "if" statements as method calls. Other ways could also work:

.When(level > 3) // enter "conditional" context
  .WithSkill(Skill.HideInShadows)
.End() // exit "conditional" context

In this case, the builder keeps track whether it should ignore any method calls that are done in a "conditional" context if the condition is false. When would enter the context, End would exit it. You could also have an Otherwise() call to mark the "else" context. Interestingly enough, you could also cover other statements like this, like loops:

.Do(times: 10) // add 10 shurikens
  .AddShuriken()
.End()

In this case, the calls made in "loop" context must be recorded and played-back the desired number of times when End is called.

So, contexts are a kind of state the builder can be at; they change how it operates. You could also nest contexts, using a stack to keep track of them. And you should check if certain call are valid at certain states and maybe throw exceptions if they aren't.

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!