使用接口编写流畅风格 API 时避免代码重复

Avoiding code duplication when writing fluent-style API using interfaces

我正在尝试为在我的 C# 程序中构建实体编写流畅的 API。

我有一个基本实体,它有一些属性和多态实体的集合:它们都来自一个公共基本类型,但有些会有额外的属性。

这是一个例子:

public interface IGarageBuilder
{
  public IGarageBuilder Label(string label);
  public IGarageBuilder AddCar(Action<ICarBuilder> configure);
  public IGarageBuilder AddBike(Action<IBikeBuilder > configure);
}

public interface IVehicleBuilder
{
  public IVehicleBuilder Model(string model); // <- That is the member that is causing me trouble
}

public interface IBikeBuilder : IVehicleBuilder
{
  
}

public interface ICarBuilder : IVehicleBuilder
{
  public ICarBuiler HorsePoser(int horsePower);
}

(我写的软件有很多IVehicleBuilder共有的属性,我简化了很多)

我遇到了 IVehicleBuilder 接口的问题:如上定义,以下代码将无法编译:

public void BuildGarage(IGarageBuilder builder)
{
  builder.Label("My dream garage")
    .AddCar(opt => opt.Model("Ford").HorsePower(120)); // <- this fails because .Model() returns a IVehicleBuilder interface, not an ICarBuilder 
}

现在,我可以在没有 IVehicleBuilder 和 ICarBuilder 之间的依赖关系的情况下将“Model()”API 复制到 ICarBuilder 和 IBikeBuilder,但是,在我的真实应用程序中,我有几十个这样的 API,其中几个被覆盖以允许不同的方式来配置实体。此外,我还必须复制这些实现,因为它们现在 return 不同的类型。

有没有办法避免所有这些代码重复?

减少重复的一种方法是将界面分为两层。一个级别描述了可用的不同种类的方法(或方法组),更高级别将这些组合在一起成为 API 使用的流畅接口,使用接口继承:

public interface IHasModel<T>
{
    T Model(string model);
}

public interface ICarBuilder : IHasModel<ICarBuilder>
{
    // Things which are specific to just cars can go in here if you want, rather than
    // taking up a separate IHasHorsePower<T>
    ICarBuilder HorsePower(int horsePower);
}

public interface IBikeBuilder : IHasModel<IBikeBuilder> { }

此外,尝试为所有接口保持相同的底层构建器实现。这可以具有仅由单个构建器使用的方法和状态位,这很好。

internal class Builder : ICarBuilder, IBikeBuilder
{
    // May or may not be used, depending on what we're building...
    private string model;
    private int horsePower;
    
    public Builder Model(string model)
    {
        this.model = model;
        return this;
    }
    
    // Annoyingly return type covariance isn't yet supported for implicit interface
    // implementations, so we need this boilerplate
    ICarBuilder IHasModel<ICarBuilder>.Model(string model) => Model(model);
    IBikeBuilder IHasModel<IBikeBuilder>.Model(string model) => Model(model);
    
    // You can avoid the song-and-dance for simple things, which are just referenced
    // by a single interface
    public ICarBuilder HorsePower(int horsePower)
    {
        this.horsePower = horsePower;
        return this;
    }

    // I assume you'll have something like this as well...
    public Car BuildCar() => new Car(model, horsePower);
    public Bike BuildBike() => new Bike(model);
}