Quali interfacce fluenti hai fatto o visto in C # che sono state molto preziose? Cosa c’era di così bello in loro?

“Fluent interface” è un argomento piuttosto caldo in questi giorni. C # 3.0 ha alcune caratteristiche interessanti (in particolare i metodi di estensione) che ti aiutano a realizzarle.

Per tua informazione, un’API fluente significa che ogni chiamata al metodo restituisce qualcosa di utile, spesso lo stesso object su cui hai chiamato il metodo, così puoi continuare a incatenare le cose. Martin Fowler lo discute con un esempio di Java qui . Il concetto assomiglia a qualcosa del genere:

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

Ho visto alcune interfacce fluenti incredibilmente utili in C # (un esempio è l’approccio fluente per convalidare i parametri trovati in una precedente domanda StackOverflow che avevo posto . Mi ha lasciato senza parole. Era in grado di fornire una syntax altamente leggibile per esprimere le regole di validazione dei parametri, e inoltre, se non ci fossero eccezioni, è stato in grado di evitare l’istanziazione di qualsiasi object! Quindi per il “caso normale”, c’era poco overhead.Questo bocconcino mi ha insegnato una quantità enorme in breve tempo.Voglio trovare più cose come quello).

Quindi, mi piacerebbe saperne di più guardando e discutendo alcuni esempi eccellenti. Quindi, quali sono alcune eccellenti interfacce fluenti che hai creato o visto in C # e cosa le ha rese così preziose?

Grazie.

Complimenti per la validazione dei parametri del metodo, mi hai dato una nuova idea per le nostre API fluenti. Ho odiato comunque i nostri controlli di precondizione …

Ho sviluppato un sistema di estensibilità per un nuovo prodotto in fase di sviluppo, in cui è ansible descrivere fluentemente i comandi disponibili, gli elementi dell’interfaccia utente e altro ancora. Questo funziona su StructureMap e FluentNHibernate, che sono anche delle belle API.

 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(); // ... }); 

E puoi configurare tutti i comandi disponibili in questo modo:

 Command(CommandNames.File) .Is() .AlwaysEnabled(); Command(CommandNames.FileNew) .Bind(Shortcut.CtrlN) .Is() .Enable(WorkspaceStatusProviderNames.DocumentFactoryRegistered); Command(CommandNames.FileSave) .Bind(Shortcut.CtrlS) .Enable(WorkspaceStatusProviderNames.DocumentOpen) .Is(); Command(CommandNames.FileSaveAs) .Bind(Shortcut.CtrlShiftS) .Enable(WorkspaceStatusProviderNames.DocumentOpen) .Is(); Command(CommandNames.FileOpen) .Is() .Enable(WorkspaceStatusProviderNames.DocumentFactoryRegistered); Command(CommandNames.FileOpenFile) .Bind(Shortcut.CtrlO) .Is() .Enable(WorkspaceStatusProviderNames.DocumentFactoryRegistered); Command(CommandNames.FileOpenRecord) .Bind(Shortcut.CtrlShiftO) .Is() .Enable(WorkspaceStatusProviderNames.DocumentFactoryRegistered); 

La nostra vista configura i loro controlli per i comandi del menu di modifica standard usando un servizio loro assegnato dallo spazio di lavoro, dove gli dicono semplicemente di osservarli:

 Workspace .Observe(control1) .Observe(control2) 

Se le tabs utente ai controlli, lo spazio di lavoro ottiene automaticamente un adattatore appropriato per il controllo e fornisce operazioni di annullamento / ripetizione e appunti.

Ci ha aiutato a ridurre drasticamente il codice di installazione e renderlo ancora più leggibile.


Ho dimenticato di raccontare una libreria che stiamo utilizzando nei nostri modelli di presentazione MVP di WinForm per convalidare le visualizzazioni: FluentValidation . Davvero facile, davvero testabile, davvero bello!

Questa è in realtà la prima volta che ho sentito il termine “interfaccia fluente”. Ma i due esempi che mi vengono in mente sono LINQ e collezioni immutabili.

Sotto le coperte LINQ è una serie di metodi, la maggior parte dei quali sono metodi di estensione, che prendono almeno un IEnumerable e restituiscono un altro IEnumerable. Ciò consente un metodo molto potente di concatenamento

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

Tipi immutabili e in particolare le collezioni hanno uno schema molto simile. Le Collezioni immutabili restituiscono una nuova collezione per ciò che normalmente sarebbe un’operazione mutante. Pertanto, la creazione di una raccolta si trasforma spesso in una serie di chiamate al metodo concatenato.

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

Adoro l’interfaccia fluente in CuttingEdge.Conditions .

Dal loro esempio:

  // Controlla tutte le condizioni preliminari:
  id.Requires ( "id")
     . IsNotNull () // genera ArgumentNullException in caso di errore 
     .IsInRange (1, 999) // ArgumentOutOfRangeException in caso di errore 
     .IsNotEqualTo (128);  // genera ArgumentException in caso di errore 
 

Ho scoperto che è molto più facile da leggere e mi rende molto più efficace nel controllare le mie condizioni preliminari (e condizioni) nei metodi rispetto a quando ho 50 se le istruzioni per gestire gli stessi controlli.

Eccone uno che ho fatto proprio ieri. Un ulteriore pensiero potrebbe portarmi a cambiare approccio, ma anche se così fosse, l’approccio “fluente” mi consente di realizzare qualcosa che altrimenti non avrei potuto avere.

Innanzitutto, un po ‘di background. Recentemente ho appreso (qui su StackOverflow) un modo per passare un valore a un metodo tale che il metodo sia in grado di determinare sia il nome che il valore . Ad esempio, un uso comune è per la convalida dei parametri. Per esempio:

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

Nota che non esiste una stringa contenente “lastMonthsInvoice”, il che è positivo perché le stringhe fanno schifo per il refactoring. Tuttavia, il messaggio di errore può indicare qualcosa come “Il parametro ‘lastMonthsInvoice’ non deve essere nullo.” Ecco il post che spiega perché questo funziona e punta al post del blog del ragazzo.

Ma questo è solo uno sfondo. Sto usando lo stesso concetto, ma in un modo diverso. Sto scrivendo alcuni test unitari e voglio scaricare determinati valori di proprietà nella console in modo che vengano visualizzati nell’output del test dell’unità. Mi sono stancato di scrivere questo:

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

… perché devo nominare la proprietà come una stringa e quindi fare riferimento ad essa. Così l’ho fatto dove potevo scrivere questo:

 ConsoleHelper.WriteProperty( ()=> lastMonthsInvoice ); 

E ottieni questo risultato:

 Property [lastMonthsInvoice] is:  

produce>

Ora, ecco dove un approccio fluente mi ha permesso di fare qualcosa che altrimenti non avrei potuto fare.

Volevo che ConsoleHelper.WriteProperty prendesse un array params, in modo che potesse scaricare molti valori di tali proprietà sulla console. Per fare ciò, la sua firma sarebbe simile a questa:

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

Quindi potrei fare questo:

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

Tuttavia, ciò non funziona a causa dell'inferenza di tipo. In altre parole, tutte queste espressioni non restituiscono lo stesso tipo. lastMonthsInvoice è una fattura. firstName e lastName sono stringhe. Non possono essere utilizzati nella stessa chiamata a WriteProperty, perché T non è la stessa su tutti loro.

È qui che l'approccio fluente è venuto in soccorso. Ho reso WriteProperty () restituire qualcosa. Il tipo restituito è qualcosa che posso chiamare E () su. Questo mi dà questa syntax:

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

Questo è un caso in cui l'approccio fluente consentiva qualcosa che altrimenti non sarebbe stato ansible (o almeno non conveniente).

Ecco la piena implementazione. Come ho detto, l'ho scritto ieri. Probabilmente vedrai margini di miglioramento o forse anche approcci migliori. Lo accetto.

 public static class ConsoleHelper { // code where idea came from ... //public static void IsNotNull(Expression> 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(Expression> expr) { var param = (MemberExpression)expr.Body; Console.WriteLine("Property [" + param.Member.Name + "] = " + expr.Compile()()); return null; } public static PropertyWriter And(this PropertyWriter ignored, Expression> expr) { ConsoleHelper.WriteProperty(expr); return null; } public static void Blank(this PropertyWriter ignored) { Console.WriteLine(); } } public class PropertyWriter { ///  /// It is not even possible to instantiate this class. It exists solely for hanging extension methods off. ///  private PropertyWriter() { } } 

In aggiunta a quelli specificati qui, il framework di test mino dell’unità popup RhinoMocks utilizza una syntax fluente per specificare le aspettative sugli oggetti mock:

 // 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); 

SubSonic 2.1 ha una soluzione decente per l’API di query:

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

tweetsharp fa largo uso anche di un’API fluente:

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

E Fluent NHibernate è di gran moda ultimamente:

 public class CatMap : ClassMap { 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); } } 

Anche Ninject li usa, ma non sono riuscito a trovare un esempio in fretta.

Denominazione del metodo

Le interfacce fluenti si prestano alla leggibilità finché i nomi dei metodi vengono scelti in modo ragionevole.

Con questo in mente, mi piacerebbe nominare questa particolare API come “anti-fluente”:

System.Type.IsInstanceOfType

È un membro di System.Type e accetta un object e restituisce true se l’object è un’istanza del tipo. Sfortunatamente, si tende naturalmente a leggerlo da sinistra a destra in questo modo:

 o.IsInstanceOfType(t); // wrong 

Quando in realtà è il contrario:

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

Ma non tutti i metodi potrebbero essere nominati (o posizionati nel BCL) per prevedere come potrebbero apparire nel codice “pseudo-inglese”, quindi questa non è una vera critica. Sto solo indicando un altro aspetto delle interfacce fluenti – la scelta dei nomi dei metodi per causare la sorpresa minore.

Inizializzatori di oggetti

Con molti degli esempi qui riportati, l’unica ragione per cui viene utilizzata un’interfaccia fluente è che molte proprietà di un object appena assegnato possono essere inizializzate all’interno di una singola espressione.

Ma C # ha una caratteristica linguistica che molto spesso rende superfluo – syntax di inizializzazione dell’object:

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

Questo forse spiegherebbe perché gli utenti esperti di C # hanno meno familiarità con il termine “interfaccia fluente” per concatenare chiamate sullo stesso object – non è necessario abbastanza spesso in C #.

Poiché le proprietà possono avere setter codificati a mano, questa è un’opportunità per chiamare diversi metodi sull’object di nuova costruzione, senza dover fare in modo che ogni metodo restituisca lo stesso object.

Le limitazioni sono:

  • Un setter di proprietà può accettare solo un argomento
  • Un setter di proprietà non può essere generico

Mi piacerebbe se potessimo chiamare metodi ed elencare in eventi, così come assegnare a proprietà, all’interno di un blocco di inizializzatore di oggetti.

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

E perché un tale blocco di modifiche dovrebbe essere applicato immediatamente dopo la costruzione? Potremmo avere:

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

Il with sarebbe una nuova parola chiave che opera su un object di un certo tipo e produce lo stesso object e tipo – nota che questa sarebbe un’espressione , non un’affermazione . Quindi catturerebbe esattamente l’idea di concatenare in una “interfaccia fluente”.

Quindi potresti usare la syntax in stile inizializzatore indipendentemente dal fatto che tu abbia ottenuto l’object da una new espressione o da un IOC o un metodo di fabbrica, ecc.

In effetti si potrebbe usare with una new completa e sarebbe equivalente allo stile corrente di inizializzatore di oggetti:

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

E come sottolinea Charlie nei commenti:

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

L’involucro di cui sopra semplicemente forza un’azione di non ritorno per restituire qualcosa, e presto – tutto ciò che può essere “fluente” in questo senso.

Equivalente di inizializzatore, ma con l’evento che si arruola:

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

E su un metodo di fabbrica invece di un new :

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

Non ho potuto resistere nel dargli il controllo di stile “forse monad” anche per null, quindi se hai qualcosa che potrebbe restituire null , puoi comunque applicare With a e poi controllarlo per null -ness.

L’API Criteria in NHibernate ha un’interfaccia fluida che ti permette di fare cose interessanti come questa:

 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(); 

Il nuovo HttpClient del WCF REST Starter Kit Preview 2 è una grande API fluente. guarda il mio post sul blog per un esempio http://bendewey.wordpress.com/2009/03/14/connecting-to-live-search-using-the-httpclient/

Ho scritto un piccolo involucro fluente per System.Net.Mail che trovo rende molto più leggibile il codice di posta elettronica (e più facile da ricordare la syntax).

 var email = Email .From("[email protected]") .To("[email protected]", "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/

Come menzionato da @ John Sheehan , Ninject utilizza questo tipo di API per specificare i bind. Ecco alcuni esempi di codice dalla loro guida utente :

 Bind().To(); Bind().ToSelf(); Bind().ToSelf().Using();