Quando diventare fluente in C #?


78

Per molti aspetti mi piace molto l'idea delle interfacce Fluent, ma con tutte le funzionalità moderne di C # (inizializzatori, lambda, parametri nominati) mi trovo a pensare "ne vale la pena?" E "È questo lo schema giusto per uso?". Qualcuno potrebbe darmi, se non una pratica accettata, almeno la propria esperienza o matrice decisionale su quando usare il modello Fluent?

Conclusione:

Alcune buone regole empiriche dalle risposte finora:

  • Le interfacce fluide sono di grande aiuto quando si hanno più azioni dei setter, poiché le chiamate beneficiano maggiormente del pass-through del contesto.
  • Le interfacce fluide dovrebbero essere pensate come uno strato sopra una api, non l'unico mezzo di utilizzo.
  • Le funzionalità moderne come lambda, inizializzatori e parametri nominati, possono lavorare mano nella mano per rendere un'interfaccia fluida ancora più amichevole.

Ecco un esempio di cosa intendo per caratteristiche moderne che lo rendono meno necessario. Prendi ad esempio un'interfaccia fluida (forse scarso) che mi consente di creare un dipendente come:

Employees.CreateNew().WithFirstName("Peter")
                     .WithLastName("Gibbons")
                     .WithManager()
                          .WithFirstName("Bill")
                          .WithLastName("Lumbergh")
                          .WithTitle("Manager")
                          .WithDepartment("Y2K");

Potrebbe essere facilmente scritto con inizializzatori come:

Employees.Add(new Employee()
              {
                  FirstName = "Peter",
                  LastName = "Gibbons",
                  Manager = new Employee()
                            {
                                 FirstName = "Bill",
                                 LastName = "Lumbergh",
                                 Title = "Manager",
                                 Department = "Y2K"
                            }
              });

In questo esempio avrei anche potuto usare parametri nominati nei costruttori.


1
Bella domanda, ma penso che sia più una domanda wiki
Ivo

La tua domanda è taggata "fluente-proibita". Quindi stai cercando di decidere se creare un'interfaccia fluida o se utilizzare la configurazione nhibernate vs. XML fluente?
Ilya Kogan,

1
Votato per la migrazione a Programmers.SE
Matt Ellen,

@Ilya Kogan, penso che in realtà sia taggato "interfaccia fluente" che è un tag generico per il modello di interfaccia fluente. Questa domanda non riguarda la proibizione, ma come hai detto solo se creare un'interfaccia fluida. Grazie.

1
Questo post mi ha ispirato a pensare a un modo per utilizzare questo modello in C. Il mio tentativo può essere trovato sul sito gemello di Code Review .
otto

Risposte:


28

Scrivere un'interfaccia fluida (mi sono dilettato con esso) richiede più impegno, ma ha un vantaggio perché se lo fai nel modo giusto, l'intento del codice utente risultante è più ovvio. È essenzialmente una forma di lingua specifica del dominio.

In altre parole, se il tuo codice viene letto molto più di quanto sia scritto (e quale codice non lo è?), Dovresti considerare di creare un'interfaccia fluida.

Le interfacce fluide riguardano più il contesto e sono molto più che semplici modi per configurare gli oggetti. Come puoi vedere nel link sopra, ho usato un'API fluente per ottenere:

  1. Contesto (quindi quando in genere esegui molte azioni in una sequenza con la stessa cosa, puoi concatenare le azioni senza dover dichiarare il tuo contesto più e più volte).
  2. Rilevabilità (quando vai a objectA.allora intellisense ti dà molti suggerimenti. Nel mio caso sopra, plm.Led.ti offre tutte le opzioni per controllare il LED incorporato e plm.Network.ti dà le cose che puoi fare con l'interfaccia di rete. plm.Network.X10.Ti dà il sottoinsieme di azioni di rete per dispositivi X10. Non lo otterrai con gli inizializzatori del costruttore (a meno che tu non voglia costruire un oggetto per ogni diverso tipo di azione, che non è idiomatico).
  3. Riflessione (non utilizzata nell'esempio sopra): la capacità di prendere un'espressione LINQ passata e manipolarla è uno strumento molto potente, in particolare in alcune API di supporto che ho creato per i test unitari. Posso trasmettere un'espressione getter di proprietà, creare un mucchio di espressioni utili, compilare ed eseguire quelle o persino usare il getter di proprietà per impostare il mio contesto.

Una cosa che faccio in genere è:

test.Property(t => t.SomeProperty)
    .InitializedTo(string.Empty)
    .CantBeNull() // tries to set to null and Asserts ArgumentNullException
    .YaddaYadda();

Non vedo come puoi fare qualcosa del genere anche senza un'interfaccia fluida.

Modifica 2 : puoi anche apportare miglioramenti di leggibilità davvero interessanti, come:

test.ListProperty(t => t.MyList)
    .ShouldHave(18).Items()
    .AndThenAfter(t => testAddingItemToList(t))
    .ShouldHave(19).Items();

Grazie per la risposta, tuttavia sono consapevole del motivo per usare Fluent, ma sto cercando un motivo più concreto per usarlo su qualcosa come il mio nuovo esempio sopra.
Andrew Hanlon,

Grazie per la risposta estesa. Penso che tu abbia delineato due buone regole empiriche: 1) Usa Fluent quando hai molte chiamate che beneficiano del "pass-through" del contesto. 2) Pensa a Fluent quando hai più chiamate che setter.
Andrew Hanlon,

2
@ach, non vedo nulla in questa risposta su "più chiamate che setter". Sei confuso dalla sua affermazione sul "codice [che] è letto molto più di quanto sia scritto"? Non si tratta di getter / setter di proprietà, si tratta di umani che leggono il codice contro umani che scrivono il codice - di rendere il codice facile da leggere per gli umani , perché in genere leggiamo una determinata riga di codice molto più spesso di quanto lo modifichiamo.
Joe White,

@Joe White, forse dovrei riformulare il mio termine 'call' per 'action'. Ma l'idea è ancora valida. Grazie.
Andrew Hanlon,

La riflessione per il test è malvagia!
Adronius

24

Scott Hanselman ne parla nell'episodio 260 del suo podcast Hanselminutes con Jonathan Carter. Spiegano che un'interfaccia fluida è più simile a un'interfaccia utente su un'API. Non dovresti fornire un'interfaccia fluida come unico punto di accesso, ma piuttosto fornirla come una sorta di interfaccia utente del codice sopra la "normale interfaccia API".

Jonathan Carter parla anche un po 'della progettazione delle API sul suo blog .


Grazie mille per i collegamenti informativi e l'interfaccia utente in cima all'API è un bel modo di vederlo.
Andrew Hanlon,

14

Le interfacce fluide sono funzionalità molto potenti da fornire nel contesto del tuo codice, quando non hai il ragionamento "giusto".

Se il tuo obiettivo è semplicemente quello di creare enormi catene di codici a una riga come una specie di pseudo-black-box, probabilmente stai abbaiando sull'albero sbagliato. Se invece lo stai usando per aggiungere valore alla tua interfaccia API fornendo un mezzo per concatenare chiamate di metodo e migliorare la leggibilità del codice, allora con molta buona pianificazione e sforzo penso che lo sforzo valga la pena.

Eviterei di seguire quello che sembra diventare un "modello" comune quando si creano interfacce fluide, in cui si nominano tutti i metodi fluenti "con" -qualcosa, poiché ruba un'interfaccia API potenzialmente buona del suo contesto, e quindi il suo valore intrinseco .

La chiave è pensare alla sintassi fluente come un'implementazione specifica di un linguaggio specifico del dominio. Come un ottimo esempio di ciò di cui sto parlando, dai un'occhiata a StoryQ, che utilizza la fluidità come mezzo per esprimere un DSL in un modo molto prezioso e flessibile.


Grazie per la risposta, non è mai troppo tardi per dare una risposta ponderata.
Andrew Hanlon,

Non mi dispiace il prefisso 'with' per i metodi. Li distingue da altri metodi che non restituiscono un oggetto per il concatenamento. Ad esempio position.withX(5)controposition.getDistanceToOrigin()
LegendLength,

5

Nota iniziale: sto contestando un'ipotesi nella domanda e traendo le mie conclusioni specifiche (alla fine di questo post). Poiché questo probabilmente non costituisce una risposta completa e comprensiva, lo segnerò come CW.

Employees.CreateNew().WithFirstName("Peter")…

Potrebbe essere facilmente scritto con inizializzatori come:

Employees.Add(new Employee() { FirstName = "Peter",  });

Ai miei occhi, queste due versioni dovrebbero significare e fare cose diverse.

  • A differenza della versione non fluente, la versione fluente nasconde il fatto che anche il nuovo Employeeè Addedito nella collezione Employees- suggerisce solo che un nuovo oggetto è Created.

  • Il significato di ….WithX(…)è ambiguo, in particolare per le persone provenienti da F #, che ha una withparola chiave per le espressioni degli oggetti : potrebbero interpretare obj.WithX(x)come un nuovo oggetto da objcui deriva che è identico ad objeccezione della sua Xproprietà, il cui valore è x. D'altra parte, con la seconda versione, è chiaro che non vengono creati oggetti derivati ​​e che tutte le proprietà sono specificate per l'oggetto originale.

….WithManager().With
  • Questo ….With…ha ancora un altro significato: cambiare il "focus" dell'inizializzazione della proprietà in un oggetto diverso. Il fatto che l'API fluente abbia due diversi significati per Withsta rendendo difficile interpretare correttamente ciò che sta accadendo ... ed è forse per questo che hai usato il rientro nel tuo esempio per dimostrare il significato previsto di quel codice. Sarebbe più chiaro in questo modo:

    (employee).WithManager(Managers.CreateNew().WithFirstName("Bill").…)
    //                     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    //                     value of the `Manager` property appears inside the parentheses,
    //                     like with `WithName`, where the `Name` is also inside the parentheses 

Conclusioni: "Nascondere" una funzionalità di linguaggio abbastanza semplice new T { X = x }, con un'API fluente ( Ts.CreateNew().WithX(x)) può ovviamente essere fatto, ma:

  1. Bisogna fare attenzione che i lettori del risultante codice fluente capiscano ancora esattamente cosa fa. Cioè, l'API fluente dovrebbe essere trasparente nel significato e inequivocabile. La progettazione di tale API potrebbe richiedere più lavoro di quanto previsto (potrebbe essere necessario verificarne la facilità d'uso e l'accettazione) e / o ...

  2. progettarlo potrebbe richiedere più lavoro del necessario: in questo esempio, l'API fluente aggiunge pochissimo "comfort dell'utente" rispetto all'API sottostante (una funzione del linguaggio). Si potrebbe dire che un'API fluente dovrebbe rendere la funzione API / linguaggio sottostante "più facile da usare"; cioè, dovrebbe risparmiare al programmatore un notevole sforzo. Se è solo un altro modo di scrivere la stessa cosa, probabilmente non ne vale la pena, perché non semplifica la vita del programmatore, ma rende solo più difficile il lavoro del designer (vedi la conclusione n. 1 in alto a destra).

  3. Entrambi i punti di cui sopra presuppongono silenziosamente che l'API fluente sia un livello su un'API o su una funzione del linguaggio esistente. Questa ipotesi può essere un'altra buona linea guida: un'API fluente può essere un modo extra di fare qualcosa, non l'unico modo. Cioè, potrebbe essere una buona idea offrire un'API fluente come scelta di "opt-in".


1
Grazie per aver dedicato del tempo per aggiungere alla mia domanda. Devo ammettere che il mio esempio scelto è stato mal pensato. All'epoca stavo effettivamente cercando di utilizzare un'interfaccia fluida per un'API di query che stavo sviluppando. Ho semplificato troppo. Grazie per aver segnalato i difetti e per i buoni punti di conclusione.
Andrew Hanlon,

2

Mi piace lo stile fluente, esprime l'intento in modo molto chiaro. Con l'esempio di initalizzatore oggetto che hai dopo, devi avere setter di proprietà pubbliche per usare quella sintassi, non con lo stile fluente. Detto questo, con il tuo esempio non guadagni molto rispetto ai setter pubblici perché sei quasi andato per un set java-esque / ottieni uno stile di metodo.

Il che mi porta al secondo punto, non sono sicuro che userei lo stile fluente come hai fatto tu, con molti setter di proprietà, probabilmente userei la seconda versione per quello, lo trovo meglio quando tu hanno molti verbi da concatenare, o almeno molte cose piuttosto che impostazioni.


Grazie per la tua risposta, penso che tu abbia espresso una buona regola empirica: parlare bene è meglio con molte chiamate su molti setter.
Andrew Hanlon,

1

Non avevo familiarità con il termine interfaccia fluente , ma mi ricorda un paio di API che ho usato tra cui LINQ .

Personalmente non vedo come le funzionalità moderne di C # possano impedire l'utilità di un tale approccio. Preferirei dire che vanno di pari passo. Ad esempio, è ancora più semplice ottenere una tale interfaccia usando i metodi di estensione .

Forse chiarisci la tua risposta con un esempio concreto di come un'interfaccia fluida può essere sostituita utilizzando una delle funzionalità moderne che hai citato.


1
Grazie per la risposta: ho aggiunto un esempio di base per chiarire la mia domanda.
Andrew Hanlon,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.