首页 > 解决方案 > 具有继承的专业化和专门的方法返回的流利的构建器(奇怪的重复模板)

问题描述

我正在尝试编写一个其他开发人员可以流畅使用的数据管道构建器。如果我从预期结果的示例开始,这可能是最简单的:

var b = new Builder();
b.Batch().BatchedOperation().Unbatch().UnbatchedOperation().etc.

我已经能够创建一个抽象构建器,它可以由派生构建器使用奇怪的重复模板模式进行专门化。

我正在努力解决的关键点是,某些操作只能在某些其他操作之后才能进行。具体来说,管道中有两种主要类型的操作:适用于实例集的操作和适用于单个实例的操作。因此,仅对已为BatchedOperation的管道执行 aBatchedUnbatchedOperation已为 的管道执行 a 才有效Unbatched

为了下面的示例代码,我将管道在任何时候都视为两种形式之一:Foo形式或Bar形式。这基本上等同于批处理或非批处理,但它将代码削减到核心问题,而不会挂断批处理或非批处理的确切含义,并消除了混乱。

首先,假设我从这样的东西开始,基本的 CRTP:

public abstract class Builder<TBuilder> where TBuilder : Builder<TBuilder>
{
    protected TBuilder builder;
    internal Builder() => builder = (TBuilder)this;
    public TBuilder Bar() => builder;
    public TBuilder Foo() => builder;
}

我可以创建这个构建器的一些专业化,如下所示:

public class SpecialBuilder : Builder<SpecialBuilder>
{
    public SpecialBuilder() : base()    { }
    public SpecialBuilder Special() => builder;
}

但是,问题在于它允许我执行以下操作:

var b = new SpecialBuilder();
b.Foo().Foo().etc

这不好,因为一旦管道被Foo()d,它应该不可能Foo()再次,因为它现在处于Bar()能够状态。需要明确的是,这会引发运行时错误,但不会在编译时捕获(或者特别是智能感知)。

我可以使用接口限制管道操作的结果:

public interface IBar<T> { IFoo<T> Bar(); }
public interface IFoo<T> { IBar<T> Foo(); }

public abstract class Builder<TBuilder> : IFoo<TBuilder>, IBar<TBuilder>
where TBuilder : Builder<TBuilder>
{
    protected TBuilder builder;
    internal Builder() => builder = (TBuilder)this;
    public IFoo<TBuilder> Bar() => builder;
    public IBar<TBuilder> Foo() => builder;
}

但是,这又回到了不可继承的构建器,因为现在我的派生构建器一旦被Foo()d 就无法运行。那时它不能再是Special()d,因为它现在是IBara ,而不是 a SpecialBuilder

看来我需要额外的专门接口:

public interface ISpecialFoo<T> : IFoo<T> { T Special(); }
public interface ISpecialBar<T> : IBar<T> { T Special(); }

但是当然,摘要Builder仍然不能指定Bar()返回 in IFoo<TBuilder>,因为那仍然不是 a SpecialBuilder,因此不能是Special()d。所以看起来接口返回类型本身需要遵循奇怪的重复模板模式。

这是我的大脑开始受伤的地方。我想也许是这样的:

public abstract class Builder<TBuilder, TFoo, TBar> 
    : IFoo<TBuilder>, IBar<TBuilder> // errors for both of these
    where TBuilder : Builder<TBuilder, TFoo, TBar>, IFoo<TBuilder>, IBar<TBuilder>, TFoo, TBar
    where TFoo : IFoo<TBuilder>
    where TBar : IBar<TBuilder>
{
    protected TBuilder builder;
    internal Builder() => builder = (TBuilder)this;
    public TFoo Bar() => builder;
    public TBar Foo() => builder;
}

但这给了我两个对称的错误,因为尝试从上面指出的接口继承:

'Builder<TBuilder, TFoo, TBar>' 没有实现接口成员 IFoo.Foo()'。'Builder<TBuilder, TFoo, TBar>.Foo()' 无法实现 'IFoo.Foo()' 因为它没有匹配的返回类型 'IBar'

“Builder<TBuilder, TFoo, TBar>”没有实现接口成员“IBar.Bar()”。'Builder<TBuilder, TFoo, TBar>.Bar()' 无法实现 'IBar.Bar()' 因为它没有匹配的返回类型 'IFoo'。

这甚至可能吗?我真的很想给这个代码的用户在编译时提供帮助,例如,智能感知只显示构建器状态的有效操作。但他们的收获显然是我在这里的痛苦。

这是一个完整的控制台应用程序,它演示了我得到的最接近的应用程序:

public interface ISpecial { SpecialBuilder Special(); }
public interface ISpecialFoo : IFoo<SpecialBuilder>, ISpecial { }
public interface ISpecialBar : IBar<SpecialBuilder>, ISpecial { }
public interface IBar<T> { IFoo<T> Bar(); }
public interface IFoo<T> { IBar<T> Foo(); }

internal class Program
{
    private static void Main(string[] args)
    {
        var b = new SpecialBuilder();
        b.Special().Foo().Special().Bar();
    }
}

public abstract class Builder<TBuilder, TFoo, TBar>
    where TBuilder : Builder<TBuilder, TFoo, TBar>, TFoo, TBar
    where TFoo : IFoo<TBuilder>
    where TBar : IBar<TBuilder>
{
    protected TBuilder builder;
    internal Builder() => builder = (TBuilder)this;
    public TFoo Bar() => builder;
    public TBar Foo() => builder;
}

public class SpecialBuilder : Builder<SpecialBuilder, ISpecialFoo, ISpecialBar>, ISpecialFoo, ISpecialBar
{
    public SpecialBuilder() : base() { }
    public SpecialBuilder Special() => builder;
}

标签: c#genericsfluent

解决方案


只需将类型信息挂在各处即可。

// I moved the generic interfaces inside the CRTP base
// to save some typing for the CRTP consumers.
// You can also move IState1 and IState2 outside, and
// they will need generic parameters TIState1, TIState2.
public abstract class Builder<TBuilder, TIState1, TIState2>
  : Builder<TBuilder, TIState1, TIState2>.IState1,
    Builder<TBuilder, TIState1, TIState2>.IState2
  // This constraint surprisingly (or not) compiles.
  where TBuilder : Builder<TBuilder, TIState1, TIState2>, TIState1, TIState2
  where TIState1 : Builder<TBuilder, TIState1, TIState2>.IState1
  where TIState2 : Builder<TBuilder, TIState1, TIState2>.IState2
{
  public interface IState1 { TIState2 OpState1(); }
  public interface IState2 { TIState1 OpState2(); }

  private TBuilder that;
  protected Builder() { that = (TBuilder)this; }

  public TIState2 OpState1() { return that; }
  public TIState1 OpState2() { return that; }
}

public sealed class MyBuilder
  : Builder<MyBuilder, MyBuilder.IMyState1, MyBuilder.IMyState2>,
    MyBuilder.IMyState1, MyBuilder.IMyState2
{
  public interface IMySpecial { MyBuilder OpSpecial(); }
  public interface IMyState1 : IState1, IMySpecial { }
  public interface IMyState2 : IState2, IMySpecial { }

  public MyBuilder OpSpecial() { return this; }
}

现在如果你尝试new MyBuilder().OpState1().,由于返回类型是IMyState2,你只会看到OpSpecialOpState2object方法。

然而,使用接口限制方法可见性的方法对性能有可怕的影响,因为接口调度非常慢。


推荐阅读