Fluent interfaces and inheritance in C#
Asked Answered
H

7

66

I'll show a problem by example. There is a base class with fluent interface:

class FluentPerson
{
    private string _FirstName = String.Empty;
    private string _LastName = String.Empty;

    public FluentPerson WithFirstName(string firstName)
    {
        _FirstName = firstName;
        return this;
    }

    public FluentPerson WithLastName(string lastName)
    {
        _LastName = lastName;
        return this;
    }

    public override string ToString()
    {
        return String.Format("First name: {0} last name: {1}", _FirstName, _LastName);
    }
}

and a child class:

class FluentCustomer : FluentPerson
{
    private long _Id;

    private string _AccountNumber = String.Empty;

    public FluentCustomer WithAccountNumber(string accountNumber)
    {
        _AccountNumber = accountNumber;
        return this;
    }
    public FluentCustomer WithId(long id)
    {
        _Id = id;
        return this;
    }

    public override string ToString()
    {
        return base.ToString() + String.Format(" account number: {0} id: {1}", _AccountNumber, _Id);
    }
}

The problem is that when you call customer.WithAccountNumber("000").WithFirstName("John").WithLastName("Smith") you can't add .WithId(123) in the end because return type of the WithLastName() method is FluentPerson (not FluentCustomer).

How this problem usually solved?

Halfbeak answered 17/2, 2010 at 6:36 Comment(3)
Interesting thread ! The question, and answers given, ranging from disabling inheritance (Ramesh), creating an extension method that type-converts Person to Customer (Dzimtry), Yann's interesting use of Generics for the "base" class, RichardTallent's breaking it out into two separate classes with duplicate fields, and Gorpik's comments : have all increased the conviction I have that I should stay away from programming with fluent interfaces. For me, the "beauty" of method-chaining would not justify the "yogic code contortions" proposed here. But I am prepared to "eat my words," as always :)Nobie
@BillW: you're right. Actually I asked that question because I already have a class with fluent interface and I need to implement another class that uses a lot of functionality of the first class. One more requirement is to not break the old code.I think fluent interface is not a universal thing and you can live without it, but in some cases they are really handy (not necessary). BTW I was surprised by the range of approaches suggested in answers, too.Halfbeak
I've just explored the solutions below, and I think it comes down to these tradeoffs: 1) Generics allows to have inheritance (and use protected members), but only allows 2 levels of inheritance. If your classes already use generics it may become very confusing. 2) Extensions allows to have multiple levels of inheritance (and is very clean) but do not allow using protected members. You may have to expose some things. 3) Composition (instead of Inheritance) offers greater flexibility but might become too verbose (each derived class will have to delegate all methods which it inherits).Thighbone
P
46

You can use generics to achieve that.

public class FluentPerson<T>
    where T : FluentPerson<T>
{
    public T WithFirstName(string firstName)
    {
        // ...
        return (T)this;
    }

    public T WithLastName(string lastName)
    {
        // ...
        return (T)this;
    }
}

public class FluentCustomer : FluentPerson<FluentCustomer>
{
    public FluentCustomer WithAccountNumber(string accountNumber)
    {
        // ...
        return this;
    }
}

And now:

var customer = new FluentCustomer()
  .WithAccountNumber("123")
  .WithFirstName("Abc")
  .WithLastName("Def")
  .ToString();
Pachton answered 17/2, 2010 at 7:16 Comment(5)
Looks nice, but you cannot inherit further from FluentCustomer.Brutify
How do you propose to create an instance of FluentPerson? =)Chengteh
@Gorpik; Good point. In fact, I think it should be possible to inherit further, but the syntax would be probably ugly (with nested generics). Anyway, it works fine for simple scenarios; for example when you need to build several specialized builders on top of a common abstract one.Pachton
@Steck. Should probably need to define a default non-generic type that inherits from the generic version of the class. Something like that: public class FluentPerson : FluentPerson<FluentPerson> { }. And now you can use: var person = new FluentPerson();.Pachton
@Yann Trevin: Have you figured out for sure whether you can inherit further? I can't figure it out. If so, might you post an update to this answer? (I'd promise an up-vote, but I've already done that.)Outspoken
C
52

Try to use some Extension methods.

static class FluentManager
{
    public static T WithFirstName<T>(this T person, string firstName) where T : FluentPerson
    {
        person.FirstName = firstName;
        return person;
    }

    public static T WithId<T>(this T customer, long id) where T : FluentCustomer
    {
        customer.ID = id;
        return customer;
    }
}

class FluentPerson
{
    public string FirstName { private get; set; }
    public string LastName { private get; set; }

    public override string ToString()
    {
        return string.Format("First name: {0} last name: {1}", FirstName, LastName);
    }
}

class FluentCustomer : FluentPerson
{
    public long ID { private get; set; }
    public long AccountNumber { private get; set; }

    public override string ToString()
    {
        return base.ToString() + string.Format(" account number: {0} id: {1}", AccountNumber, ID);
    }
}

after you can use like

new FluentCustomer().WithId(22).WithFirstName("dfd").WithId(32);
Chengteh answered 17/2, 2010 at 7:39 Comment(4)
I like this approach better than the other proposed ideas simply because it would offer the greatest flexibility in terms of extensibility. Of course, the drawback would be if the OP's target .NET version doesn't support it!Spearmint
Upvoted, I like this better as well, if fluent methods are required. Of course, the down side is that extension methods can only access the object's public members, so the fluent extension method may need to call a traditional method on the object (so the private work can be done) and then return the object.Northey
Using fluent Extension Method is NOT an option if you have two independent class hierarchies with same property.Antipode
Extension is not an option, since you have only access to public properties and also does not solves the problem with inheritance /for each new class inherited you'd had to create new extension methods - it's almost the same as wrapping each member class/.Lur
P
46

You can use generics to achieve that.

public class FluentPerson<T>
    where T : FluentPerson<T>
{
    public T WithFirstName(string firstName)
    {
        // ...
        return (T)this;
    }

    public T WithLastName(string lastName)
    {
        // ...
        return (T)this;
    }
}

public class FluentCustomer : FluentPerson<FluentCustomer>
{
    public FluentCustomer WithAccountNumber(string accountNumber)
    {
        // ...
        return this;
    }
}

And now:

var customer = new FluentCustomer()
  .WithAccountNumber("123")
  .WithFirstName("Abc")
  .WithLastName("Def")
  .ToString();
Pachton answered 17/2, 2010 at 7:16 Comment(5)
Looks nice, but you cannot inherit further from FluentCustomer.Brutify
How do you propose to create an instance of FluentPerson? =)Chengteh
@Gorpik; Good point. In fact, I think it should be possible to inherit further, but the syntax would be probably ugly (with nested generics). Anyway, it works fine for simple scenarios; for example when you need to build several specialized builders on top of a common abstract one.Pachton
@Steck. Should probably need to define a default non-generic type that inherits from the generic version of the class. Something like that: public class FluentPerson : FluentPerson<FluentPerson> { }. And now you can use: var person = new FluentPerson();.Pachton
@Yann Trevin: Have you figured out for sure whether you can inherit further? I can't figure it out. If so, might you post an update to this answer? (I'd promise an up-vote, but I've already done that.)Outspoken
L
5

A solution where you need fluent interface, inheritance and also some generics...

Anyhow as I stated before: this is the only option if you want to use inheritance and access also protected members...

public class GridEx<TC, T> where TC : GridEx<TC, T>
{
    public TC Build(T type)
    {
        return (TC) this;
    }
}

public class GridExEx : GridEx<GridExEx, int>
{

}

class Program
{
    static void Main(string[] args)
    {
        new GridExEx().Build(1);
    }
}
Lur answered 15/10, 2015 at 5:32 Comment(1)
Best solution if you don't want to or can't make changes to the classes that are the subject of your fluent builders. Thanks, you got my pointBurne
I
4

Logically you need to configure stuff from most specific (customer) to least specific (person) or otherwise it is even hard to read it despite the fluent interface. Following this rule in most cases you won't need get into trouble. If however for any reason you still need to mix it you can use intermediate emphasizing statements like

static class Customers
{
   public static Customer AsCustomer(this Person person)
   {
       return (Customer)person;
   }
}

customer.WIthLastName("Bob").AsCustomer().WithId(10);
Infectious answered 17/2, 2010 at 6:47 Comment(0)
A
3
 public class FluentPerson
 {
    private string _FirstName = String.Empty;
    private string _LastName = String.Empty;

    public FluentPerson WithFirstName(string firstName)
    {
        _FirstName = firstName;
        return this;
    }

    public FluentPerson WithLastName(string lastName)
    {
        _LastName = lastName;
        return this;
    }

    public override string ToString()
    {
        return String.Format("First name: {0} last name: {1}", _FirstName, _LastName);
    }
}


   public class FluentCustomer 
   {
       private string _AccountNumber = String.Empty;
       private string _id = String.Empty;
       FluentPerson objPers=new FluentPerson();



       public FluentCustomer WithAccountNumber(string accountNumber)
       {
           _AccountNumber = accountNumber;
           return this;
       }

       public FluentCustomer WithId(string id)
       {
           _id = id;
           return this;
       }

       public FluentCustomer WithFirstName(string firstName)
       {
           objPers.WithFirstName(firstName);
           return this;
       }

       public FluentCustomer WithLastName(string lastName)
       {
           objPers.WithLastName(lastName);
           return this;
       }


       public override string ToString()
       {
           return objPers.ToString() + String.Format(" account number: {0}",  _AccountNumber);
       }
   }

And invoke it using

  var ss = new FluentCustomer().WithAccountNumber("111").WithFirstName("ram").WithLastName("v").WithId("444").ToString();
Arango answered 17/2, 2010 at 6:48 Comment(4)
Interesting : you have now eliminated the inheritance of class FluentCustomer from class FluentPerson.Nobie
@BillW, its always good implement the inheritance using interface than the class inheritance (though i havent tried that in my answer, but its simple to do that).Arango
I am fascinated by your solution in the sense that instead of looking from the perspective of "Customer" "isA" "Person," (inheritance) it seems to me to look from a perspective in which "Customer" "has-a" Person (too bad I'm not bright enough to know if that's "composition" :). Namaste, or Vannakum, if more appropriate.Nobie
@BillW, thanks mate.. yes its a form of composition, And actually its "Vanakkam" :) ..Arango
N
3

Is a fluent interface really the best call here, or would an initializer be better?

 var p = new Person{
      LastName = "Smith",
      FirstName = "John"
      };

 var c = new Customer{
      LastName = "Smith",
      FirstName = "John",
      AccountNumber = "000",
      ID = "123"
      };

Unlike a fluent interface, this works fine without inherited methods giving back the base class and messing up the chain. When you inherit a property, the caller really shouldn't care whether FirstName was first implemented in Person or Customer or Object.

I find this more readable as well, whether on one line or multiple, and you don't have to go through the trouble of providing fluent self-decorating functions that correspond with each property.

Northey answered 17/2, 2010 at 7:10 Comment(4)
I think OP was just using a simple example to illustrate the problem.Brutify
"Horses for courses" I think. Adding to Gorpik's comment above, the use of a fluent interface would allow you to combine setters with other behavioural elements if they were implemented, whereas initializers would limit you only to setting a fixed collection of values, and would require overloads or some other construct for optional elements. :-)Spearmint
Initializer elements are all optional and any public writable property can be initialized. No need for overloads. But for setting more complex behaviors rather than just a short combination of primitive setters), you're right--fluent functions are handy.Northey
I stand corrected! That's what I get for glancing at a code example, rather than actually reading it! ;-) BTW, Re: readability, I find fluent interfaces tend to be more readable when used and implemented "sensibly", but less readable when the implementor tries to tie too many things together. A fluent interface is a great way to spaghettify your API implementation if you try to be too clever with them. My experience OTOH has been that they can also simplify the design if a little thought is put into it.Spearmint
G
1

I know this is now an old question, but I wanted to share my thoughts about this with you.

What about separating fluency, which is a kind of mechanism, and your classes, when you can ? This would leave your classes pure.

What about something like this ?

The classes

    public class Person
    {
        public string FirstName { get; set; }
        public string LastName {get; set;}

        public override string ToString()
        {
            return $"First name: {FirstName} last name: {LastName}";
        }
    }

    public class Customer : Person
    {
        public string AccountNumber { get; set; }
        public long Id { get; set; }

        public override string ToString()
        {
            return base.ToString() + $" account number: {AccountNumber} id: {Id}");
        }
    }

A class that adds some fluent mechanism

    public class FluentCustomer 
    {
        private Customer Customer { get; }

        public FluentCustomer() : this(new Customer())
        {
        }

        private FluentCustomer(Customer customer)
        {
            Customer = customer;
        }

        public FluentCustomer WithAccountNumber(string accountNumber)
        {
            Customer.AccountNumber = accountNumber;
            return this;
        }

        public FluentCustomer WithId(long id)
        {
            Customer.Id = id;
            return this;
        }

        public FluentCustomer WithFirstName(string firstName)
        {
            Customer.FirstName = firstName;
            return this;
        }

        public FluentCustomer WithLastName(string lastName)
        {
            Customer.LastName = lastName;
            return this;
        }

        public static implicit operator Customer(FluentCustomer fc)
        {
            return fc.Customer;
        }

        public static implicit operator FluentCustomer(Customer customer)
        {
            return new FluentCustomer(customer);
        }
    }

An extension method to switch to fluent mode

    public static class CustomerExtensions 
    {

        public static FluentCustomer Fluent(this Customer customer)
        {
            return customer;
        }
    }

The same example as in question


        Customer customer = new Customer().Fluent()
                            .WithAccountNumber("000")
                            .WithFirstName("John")
                            .WithLastName("Smith")
                            .WithId(123);

Gabby answered 10/1, 2019 at 12:49 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.