Understanding of How to Create a Fluent Interface
Asked Answered
S

2

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();
    
Spoonful answered 17/9, 2013 at 10:0 Comment(9)
Have you tried changing all of your WithXXX methods to return Car and simply return this; at the end of each method? Then you can var car = new Car().WithFuel(100).WithMpg(35).WithHorsePower(200);. Alternatively, just make these optional constructor arguments :-)Bankruptcy
sure i know i can just return Car every time :) but unfortunately thats not the Point of my Question, also the optional Constructor will not help because it could also a Method which could do some crazy stuff ;) this Car Class is just an simple example of the problemSpoonful
Sorry I thought the point of the question was to do this more easily than loads of interfaces, returning the item itself is straightforward. I would move away from buzzword implementations such as "fluent" and towards the standard and boring constructor parameters and standard methods.Bankruptcy
@AdamHouldsworth could you explain why you wouldn't use fluent? because i want to refactor my whole MVVM App to use fluent because i thing it will be easier to add stuff and refator it later on, also see my edit :DSpoonful
It's not that I wouldn't use it, but I think it only has limited scenarios where it works well. I've never needed to implement it specifically myself to improve the usage of a class or type.Bankruptcy
mhh but for less experienced programmers this could help to understand the code more faster right? or dies is just add complexity to just look nicer from the outside ?Spoonful
I personally think it can pollute a public interface if it isn't done carefully.Bankruptcy
In this example, property initializers achieve your goal with no extra work and are more legible. I can understand why you would want to do this. LINQ works very nicely with chaining - the difference is, LINQ creates a "nested" (in concept) object graph. This code simply assigns properties. Have you considered the LINQ route of writing extension methods?Dermatogen
I would also add that if it is invalid for a Car to be initialized without Weight or Mpg for example, then you must have those parameters as part of the constructor... otherwise you'll allow users to create a Car that's in an invalid state and there's no amount of "niceness" that a fluent-interface can offer to fix that (it's not worth the trade-off). Fluent interfaces can be useful, but in this particular case you're doing it wrong. Sorry!Drift
B
9

One alternative could be to invoke all operations on Mpg() which will allow the other operations to be conditional.

This is already answered in SO with a code sample. Please refer to Conditional Builder Method Chaining Fluent Interface

The post indicates, instead of Interfaces, the same might be achieved using constructors, with a calling method that makes all others operations conditional.

Bookbindery answered 19/12, 2013 at 16:3 Comment(0)
E
8

Fluent API is a nice thing, but I would go a different way in your case. Building a car draws me more towards the Builder pattern. That way you would hide a car being composed in a factory (not the factory method pattern) which accepts commands like you have now, but do not accept questions.

No manufacturer lets you know details about their new car unless it is completed and prepared to be announced. So you would have to send a command like GetMyCar() for releasing a car first and it would perfectly make sense that if you call Mpg on unfinished car you would get an exception. And it would still look good if you use that fluent pattern.

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

Ok, that's my opinion. However there are two more suggestions for your current situation you can choose from if you don't like the builder.

1) If user calls Mpg before Weight and Fuel are set, thrown an exception with a message explaining the situation. Also add a proper documentation of the Mpg method.

2) Make a constructor take all required parameters for other properties. This is, in my opinon, a better solution than the first one, because is states from the very start what you can expect.

Eustacia answered 19/12, 2013 at 9:13 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.