What fluent interfaces have you made or seen in C# that were very valuable? What was so great about them?

折月煮酒 提交于 2020-01-11 15:29:10

问题


"Fluent interfaces" is a fairly hot topic these days. C# 3.0 has some nice features (particularly extension methods) that help you make them.

FYI, a fluent API means that each method call returns something useful, often the same object you called the method on, so you can keep chaining things. Martin Fowler discusses it with a Java example here. The concept kooks something like this:

var myListOfPeople = new List<Person>();

var person = new Person();
person.SetFirstName("Douglas").SetLastName("Adams").SetAge(42).AddToList(myListOfPeople);

I have seen some incredibly useful fluent interfaces in C# (one example is the fluent approach for validating parameters found in an earlier StackOverflow question I had asked. It blew me away. It was able to give highly readable syntax for expressing parameter validation rules, and also, if there were no exceptions, it was able to avoid instantiating any objects! So for the "normal case", there was very little overhead. This one tidbit taught me a huge amount in a short time. I want to find more things like that).

So, I'd like to learn more by looking at and discussing some excellent examples. So, what are some excellent fluent interfaces you've made or seen in C#, and what made them so valuable?

Thanks.


回答1:


Kudos for the method parameter validation, you've given me a new idea for our fluent APIs. I've hated our precondition checks anyways...

I've built a extensibility system for a new product in development, where you can fluently describe the commands available, the user interface elements and more. This runs on top of StructureMap and FluentNHibernate, which are nice APIs too.

MenuBarController mb;
// ...
mb.Add(Resources.FileMenu, x =>
{
  x.Executes(CommandNames.File);
  x.Menu
    .AddButton(Resources.FileNewCommandImage, Resources.FileNew, Resources.FileNewTip, y => y.Executes(CommandNames.FileNew))
    .AddButton(null, Resources.FileOpen, Resources.FileOpenTip, y => 
    {
      y.Executes(CommandNames.FileOpen);
      y.Menu
        .AddButton(Resources.FileOpenFileCommandImage, Resources.OpenFromFile, Resources.OpenFromFileTop, z => z.Executes(CommandNames.FileOpenFile))
        .AddButton(Resources.FileOpenRecordCommandImage, Resources.OpenRecord, Resources.OpenRecordTip, z => z.Executes(CommandNames.FileOpenRecord));
     })
     .AddSeperator()
     .AddButton(null, Resources.FileClose, Resources.FileCloseTip, y => y.Executes(CommandNames.FileClose))
     .AddSeperator();
     // ...
});

And you can configure all commands available like this:

Command(CommandNames.File)
  .Is<DummyCommand>()
  .AlwaysEnabled();

Command(CommandNames.FileNew)
  .Bind(Shortcut.CtrlN)
  .Is<FileNewCommand>()
  .Enable(WorkspaceStatusProviderNames.DocumentFactoryRegistered);

Command(CommandNames.FileSave)
  .Bind(Shortcut.CtrlS)
  .Enable(WorkspaceStatusProviderNames.DocumentOpen)
  .Is<FileSaveCommand>();

Command(CommandNames.FileSaveAs)
  .Bind(Shortcut.CtrlShiftS)
  .Enable(WorkspaceStatusProviderNames.DocumentOpen)
  .Is<FileSaveAsCommand>();

Command(CommandNames.FileOpen)
  .Is<FileOpenCommand>()
  .Enable(WorkspaceStatusProviderNames.DocumentFactoryRegistered);

Command(CommandNames.FileOpenFile)
  .Bind(Shortcut.CtrlO)
  .Is<FileOpenFileCommand>()
  .Enable(WorkspaceStatusProviderNames.DocumentFactoryRegistered);

Command(CommandNames.FileOpenRecord)
  .Bind(Shortcut.CtrlShiftO)
  .Is<FileOpenRecordCommand>()
  .Enable(WorkspaceStatusProviderNames.DocumentFactoryRegistered);

Our view configure their controls for the standard edit menu commands using a service given to them by the workspace, where they just tell it to observe them:

Workspace
  .Observe(control1)
  .Observe(control2)

If the user tabs to the controls, the workspace automatically gets an appropriate adapter for the control and provides undo/redo and clipboard operations.

It has helped us reduce the setup code dramatically and make it even more readable.


I forgot to tell about a library we're using in our WinForms MVP model presenters to validate the views: FluentValidation. Really easy, really testable, really nice!




回答2:


This is actually the first time I've heard the term "fluent interface." But the two examples that come to mind are LINQ and immutable collections.

Under the covers LINQ is a series of methods, most of which are extension methods, which take at least one IEnumerable and return another IEnumerable. This allows for very powerful method chaining

var query = someCollection.Where(x => !x.IsBad).Select(x => x.Property1);

Immutable types, and more specifically collections have a very similar pattern. Immutable Collections return a new collection for what would be normally a mutating operation. So building up a collection often turns into a series of chained method calls.

var array = ImmutableCollection<int>.Empty.Add(42).Add(13).Add(12);



回答3:


I love the fluent interface in CuttingEdge.Conditions.

From their sample:

 // Check all preconditions:
 id.Requires("id")
    .IsNotNull()          // throws ArgumentNullException on failure 
    .IsInRange(1, 999)    // ArgumentOutOfRangeException on failure 
    .IsNotEqualTo(128);   // throws ArgumentException on failure 
 

I've found that it is a lot easier to read, and makes me much more effective at checking my preconditions (and post conditions) in methods than when I have 50 if statements to handle the same checks.




回答4:


Here's one I made just yesterday. Further thought may lead me to change the approach, but even if so, the "fluent" approach let me accomplish something I otherwise could not have.

First, some background. I recently learned (here on StackOverflow) a way to pass a value to a method such that the method would be able to determine both the name and the value. For example, one common use is for parameter validation. For example:

public void SomeMethod(Invoice lastMonthsInvoice)
{
     Helper.MustNotBeNull( ()=> lastMonthsInvoice);
}

Note there's no string containing "lastMonthsInvoice", which is good because strings suck for refactoring. However, the error message can say something like "The parameter 'lastMonthsInvoice' must not be null." Here's the post that explains why this works and points to the guy's blog post.

But that is just background. I'm using the same concept, but in a different way. I am writing some unit tests, and I want to dump certain property values out to the console so they show up in the unit test output. I got tired of writing this:

Console.WriteLine("The property 'lastMonthsInvoice' has the value: " + lastMonthsInvoice.ToString());

... because I have to name the property as a string and then refer to it. So I made it where I could type this:

ConsoleHelper.WriteProperty( ()=> lastMonthsInvoice );

And get this output:

Property [lastMonthsInvoice] is: <whatever ToString from Invoice

produces>

Now, here's where a fluent approach allowed me to do something I otherwise couldn't do.

I wanted to make ConsoleHelper.WriteProperty take a params array, so it could dump many such property values to the console. To do that, its signature would look like this:

public static void WriteProperty<T>(params Expression<Func<T>>[] expr)

So I could do this:

ConsoleHelper.WriteProperty( ()=> lastMonthsInvoice, ()=> firstName, ()=> lastName );

However, that doesn't work due to type inference. In other words, all of these expressions do not return the same type. lastMonthsInvoice is an Invoice. firstName and lastName are strings. They cannot be used in the same call to WriteProperty, because T is not the same across all of them.

This is where the fluent approach came to the rescue. I made WriteProperty() return something. The type it returned is something I can call And() on. This gives me this syntax:

ConsoleHelper.WriteProperty( ()=> lastMonthsInvoice)
     .And( ()=> firstName)
     .And( ()=> lastName);

This is a case where the fluent approach allowed something that otherwise would not have been possible (or at least not convenient).

Here's the full implementation. As I said, I wrote it yesterday. You'll probably see room for improvement or maybe even better approaches. I welcome that.

public static class ConsoleHelper
{
    // code where idea came from ...
    //public static void IsNotNull<T>(Expression<Func<T>> expr)
    //{
    // // expression value != default of T
    // if (!expr.Compile()().Equals(default(T)))
    // return;

    // var param = (MemberExpression)expr.Body;
    // throw new ArgumentNullException(param.Member.Name);
    //}

    public static PropertyWriter WriteProperty<T>(Expression<Func<T>> expr)
    {
        var param = (MemberExpression)expr.Body;
        Console.WriteLine("Property [" + param.Member.Name + "] = " + expr.Compile()());
        return null;
    }

    public static PropertyWriter And<T>(this PropertyWriter ignored, Expression<Func<T>> expr)
    {
        ConsoleHelper.WriteProperty(expr);
        return null;
    }

    public static void Blank(this PropertyWriter ignored)
    {
        Console.WriteLine();
    }
}

public class PropertyWriter
{
    /// <summary>
    /// It is not even possible to instantiate this class. It exists solely for hanging extension methods off.
    /// </summary>
    private PropertyWriter() { }
}



回答5:


In addition to the ones specified here, the popuplar RhinoMocks unit test mock framework uses fluent syntax to specify expectations on mock objects:

// Expect mock.FooBar method to be called with any paramter and have it invoke some method
Expect.Call(() => mock.FooBar(null))
    .IgnoreArguments()
    .WhenCalled(someCallbackHere);

// Tell mock.Baz property to return 5:
SetupResult.For(mock.Baz).Return(5);



回答6:


Method Naming

Fluent interfaces lend themselves to readability as long as the method names are chosen sensibly.

With that in mind, I'd like to nominate this particular API as "anti-fluent":

System.Type.IsInstanceOfType

It's a member of System.Type and takes an object, and returns true if the object is an instance of the type. Unfortunately, you naturally tend to read it from left to right like this:

o.IsInstanceOfType(t);  // wrong

When it's actually the other way:

t.IsInstanceOfType(o);  // right, but counter-intuitive

But not all methods could possibly be named (or positioned in the BCL) to anticipate how they might appear in "pseudo-English" code, so this isn't really a criticism. I'm just pointing out another aspect of fluent interfaces - the choosing of method names in order to cause the least surprise.

Object Initializers

With many of the examples given here, the only reason a fluent interface is being used is so that several properties of a newly allocated object can be initialized within a single expression.

But C# has a language feature that very often makes this unnecessary - object initializer syntax:

var myObj = new MyClass
            {
                SomeProperty = 5,
                Another = true,
                Complain = str => MessageBox.Show(str),
            };

This perhaps would explain why expert C# users are less familiar with the term "fluent interface" for chaining calls on the same object - it isn't needed quite so often in C#.

As properties can have hand-coded setters, this is an opportunity to call several methods on the newly constructed object, without having to make each method return the same object.

The limitations are:

  • A property setter can only accept one argument
  • A property setter cannot be generic

I would like it if we could call methods and enlist in events, as well as assign to properties, inside an object initializer block.

var myObj = new MyClass
            {
                SomeProperty = 5,
                Another = true,
                Complain = str => MessageBox.Show(str),
                DoSomething()
                Click += (se, ev) => MessageBox.Show("Clicked!"),
            };

And why should such a block of modifications only be applicable immediately after construction? We could have:

myObj with
{
    SomeProperty = 5,
    Another = true,
    Complain = str => MessageBox.Show(str),
    DoSomething(),
    Click += (se, ev) => MessageBox.Show("Clicked!"),
}

The with would be a new keyword that operates on an object of some type and produces the same object and type - note that this would be an expression, not a statement. So it would exactly capture the idea of chaining in a "fluent interface".

So you could use initializer-style syntax regardless of whether you'd got the object from a new expression or from an IOC or factory method, etc.

In fact you could use with after a complete new and it would be equivalent to the current style of object initializer:

var myObj = new MyClass() with
            {
                SomeProperty = 5,
                Another = true,
                Complain = str => MessageBox.Show(str),
                DoSomething(),
                Click += (se, ev) => MessageBox.Show("Clicked!"),
            };

And as Charlie points out in the comments:

public static T With(this T with, Action<T> action)
{
    if (with != null)
        action(with);
    return with;
}

The above wrapper simply forces a non-returning action to return something, and hey presto - anything can be "fluent" in that sense.

Equivalent of initializer, but with event enlisting:

var myObj = new MyClass().With(w =>
            {
                w.SomeProperty = 5;
                w.Another = true;
                w.Click += (se, ev) => MessageBox.Show("Clicked!");
            };

And on a factory method instead of a new:

var myObj = Factory.Alloc().With(w =>
            {
                w.SomeProperty = 5;
                w.Another = true;
                w.Click += (se, ev) => MessageBox.Show("Clicked!");
            };

I couldn't resist giving it the "maybe monad"-style check for null as well, so if you have something that might return null, you can still apply With to it and then check it for null-ness.




回答7:


SubSonic 2.1 has a decent one for the query API:

DB.Select()
  .From<User>()
  .Where(User.UserIdColumn).IsEqualTo(1)
  .ExecuteSingle<User>();

tweetsharp makes extensive use of a fluent API too:

var twitter = FluentTwitter.CreateRequest()
              .Configuration.CacheUntil(2.Minutes().FromNow())
              .Statuses().OnPublicTimeline().AsJson();

And Fluent NHibernate is all the rage lately:

public class CatMap : ClassMap<Cat>  
{  
  public CatMap()  
  {  
    Id(x => x.Id);  
    Map(x => x.Name)  
      .WithLengthOf(16)  
      .Not.Nullable();  
    Map(x => x.Sex);  
    References(x => x.Mate);  
    HasMany(x => x.Kittens);  
  }  
}  

Ninject uses them too, but I couldn't find an example quickly.




回答8:


I wrote a little fluent wrapper for System.Net.Mail which I find makes it email code much more readable (and easier to remember the syntax).

var email = Email
            .From("john@email.com")
            .To("bob@email.com", "bob")
            .Subject("hows it going bob")
            .Body("yo dawg, sup?");

//send normally
email.Send();

//send asynchronously
email.SendAsync(MailDeliveredCallback);

http://lukencode.com/2010/04/11/fluent-email-in-net/




回答9:


The Criteria API in NHibernate has a nice fluent interface which allows you to do cool stuff like this:

Session.CreateCriteria(typeof(Entity))
    .Add(Restrictions.Eq("EntityId", entityId))
    .CreateAlias("Address", "Address")
    .Add(Restrictions.Le("Address.StartDate", effectiveDate))
    .Add(Restrictions.Disjunction()
        .Add(Restrictions.IsNull("Address.EndDate"))
        .Add(Restrictions.Ge("Address.EndDate", effectiveDate)))
    .UniqueResult<Entity>();



回答10:


The new HttpClient of the WCF REST Starter Kit Preview 2 is a great fluent API. see my blog post for a sample http://bendewey.wordpress.com/2009/03/14/connecting-to-live-search-using-the-httpclient/




回答11:


As @John Sheehan mentioned, Ninject uses this type of API to specify bindings. Here are some example code from their user guide:

Bind<IWeapon>().To<Sword>();
Bind<Samurai>().ToSelf();
Bind<Shogun>().ToSelf().Using<SingletonBehavior>();


来源:https://stackoverflow.com/questions/688418/what-fluent-interfaces-have-you-made-or-seen-in-c-sharp-that-were-very-valuable

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!