【问题标题】:Avoiding code duplication when writing fluent-style API using interfaces使用接口编写流畅风格的 API 时避免代码重复
【发布时间】:2021-02-12 10:23:30
【问题描述】:

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

我有一个基础实体,它具有一些属性和一组多态实体:它们都来自一个通用的基础类型,但有些会有额外的属性。

这是一个例子:

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,其中几个被覆盖以允许配置实体的不同方式。此外,我还必须复制实现,因为它们现在返回不同的类型。

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

【问题讨论】:

  • 您可以将IVehicleBuilder 设为通用,这是一种选择吗?
  • 不应该AddCar 返回ICarBuilder 的类型而不是IGarageBuilder
  • 您正在制造汽车,但未指定您正在制造汽车,您需要一种方法来下拉到更具体的类型(即让IVehicleBuilder返回汽车/自行车)或尽早从更具体的类型开始。
  • @Matin 不,AddCar 接受一个委托,该委托将构建器作为参数并返回 IGarageBuilder(因此可以添加新车辆)

标签: c# fluent c#-9.0


【解决方案1】:

减少重复的一种方法是将界面分成两个级别。一个级别描述了可用的不同类型的方法(或方法组),而更高级别则使用接口继承将这些方法组合到 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);
}

【讨论】:

  • 这样的缺点是如果你有 20 种车辆,构建器现在需要实现 20 个接口。
  • 确实如此,但它比拥有 20 个构建器类更容易,每个构建器类实现 1 个接口
  • 但在这种情况下,OP 正在构建多个东西,因此拥有多个构建器是有意义的。当然,有一个抽象的基础构建器对象来保存共享代码,然后专门针对单个构建器。
  • 他们正在建造的东西有很多共同点:例如,自行车和汽车都有模型。拥有单独的汽车/自行车制造商意味着在两个制造商之间复制该逻辑,而拥有一个可以制造汽车和自行车的制造商可以减少样板。无论如何,这是我编写流畅界面的经验。
  • 当然,但是当您将汽车材料放入通用构建器类时,您已经污染了您的代码。对我来说,将它们放在不同的类中很重要。
猜你喜欢
  • 2014-12-31
  • 1970-01-01
  • 1970-01-01
  • 2015-07-22
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-05-31
  • 1970-01-01
相关资源
最近更新 更多