10

Hi i'm trying to understand how i could build a readable and also error preventing Fluent-API without to much restriction for the User

to hold it simple let's say we want to change the following class to be fluent

public class Car
{
    public int Gallons { get; private set; }
    public int Tons { get; private set; }
    public int Bhp { get; private set; }
    public string Make { get; private set; }
    public string Model { get; private set; }

    public Car(string make, string model)
    {
        Make = make;
        Model = model;
    }

    public void WithHorsePower(int bhp)
    {
        Bhp = bhp;
        return this;
    }

    public void WithFuel(int gallons)
    {
        Gallons = gallons;
    }

    public void WithWeight(int tons)
    {
        Tons = tons;
    }

    public int Mpg()
    {
        return Gallons/Tons;
    }
}

the problem in this case the user should only be able to access Mpg() if Weight() and Fuel() got called first also the position of HorsePower() is irrelevant.

Samples:

int mpg =Car.Create().HorsePower().Fuel().Weight().Mpg();
int mpg =Car.Create().Fuel().HorsePower().Weight().Mpg();
int mpg =Car.Create().HorsePower().Fuel().HorsePower().Weight().Mpg();// <- no typo
int mpg =Car.Create().Fuel().Weight().HorsePower().Mpg();
int mpg =Car.Create().Weight().HorsePower().Fuel().Mpg();
int mpg =Car.Create().Weight().Fuel().Mpg();

Is there a easy way to do this without a big bunch of interfaces?
I also doesn't how to implement this nested interfaces in the right way

Here are the interfaces i currently created

interface Start
{
    IFuelWeight1 HorsePower();

    IHorsePowerWeight1 Fuel();

    IHorsePowerFuel1 Weight();
}

interface IFuelWeight1 // Start.HorsePower()
{
    IHorsePowerWeight1 Fuel();

    IHorsePowerFuel1 Weight();
}

interface IHorsePowerWeight1 // Start.Fuel()
{
    IHorsePowerWeight1 HorsePower();

    IHorsePowerFuelMpg Weight();
}

interface IHorsePowerFuel1 // Start.Weight()
{
    IHorsePowerFuel1 HorsePower();

    IHorsePowerWeightMpg Fuel();
}

#region End

interface IHorsePowerFuelMpg
{
    IFuelWeightMpg HorsePower();

    IHorsePowerWeightMpg Fuel();

    int Mpg();
}

interface IHorsePowerWeightMpg
{
    IFuelWeightMpg HorsePower();

    IHorsePowerFuelMpg Weight();

    int Mpg();
}

interface IFuelWeightMpg
{
    IHorsePowerWeightMpg Fuel();

    IHorsePowerFuelMpg Weight();

    int Mpg();
}

#endregion

EDIT for Adam Houldsworth :-)

  • Is the Interface above a good one or is there a easier way to do this but hold the restriction for Mpg()?
  • How to Implement the interface above to do this?:

        var k = myMiracle as Start;
        k.Fuel().Weight();
        k.Weight().Fuel();
        k.HorsePower().Fuel().Weight();
        k.HorsePower().Weight().Fuel();
        k.Fuel().HorsePower().Weight();
        k.Weight().HorsePower().Fuel();
    
4

2 回答 2

9

一种替代方法是调用 Mpg() 上的所有操作,这将允许其他操作是有条件的。

这已经在 SO 中通过代码示例得到了回答。请参考Conditional Builder Method Chaining Fluent Interface

该帖子指出,可以使用构造函数来实现相同的功能,而不是使用接口,并使用使所有其他操作成为条件的调用方法。

于 2013-12-19T16:03:47.700 回答
8

Fluent API 是一件好事,但在你的情况下我会采取不同的方式。建造汽车让我更倾向于建造者模式。这样,您将隐藏在工厂(不是工厂方法模式)中组成的汽车,该工厂接受您现在拥有的命令,但不接受问题。

除非新车已完成并准备公布,否则任何制造商都不会让您了解有关新车的详细信息。所以你必须发送一个命令,比如先释放一辆汽车,如果你调用未完成的汽车,你会得到一个例外GetMyCar(),这是完全有道理的。Mpg如果您使用这种流畅的模式,它仍然看起来不错。

var builder = new CarBuilder();
// each building method returns `CarBuilder`
builder.BuildFrames(size).BuildChassis().AppendWheels(4)...

好吧,这就是我的看法。但是,如果您不喜欢构建器,则可以选择针对您当前情况的另外两个建议。

Mpg1) 如果用户在之前调用WeightFuel设置了,则抛出异常并带有解释情况的消息。还要添加该方法的适当文档Mpg

2)使构造函数获取其他属性的所有必需参数。在我看来,这是一个比第一个更好的解决方案,因为从一开始就是你可以期待的状态。

于 2013-12-19T09:13:25.970 回答