Quando andare Fluent in C #?

76

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

Conclusione:

Alcune buone regole pratiche dalle risposte fino ad ora:

  • Le interfacce fluenti aiutano notevolmente quando si hanno più azioni dei setter, dal momento che le chiamate traggono vantaggio dal contesto pass-through.
  • Le interfacce fluenti dovrebbero essere pensate come uno strato sopra l'api, non l'unico mezzo di utilizzo.
  • Le funzionalità moderne come lambda, inizializzatori e parametri denominati, possono funzionare mano nella mano per rendere ancora più amichevole un'interfaccia fluida.

Ecco un esempio di ciò che intendo per le caratteristiche moderne che lo rendono meno necessario. Prendiamo ad esempio un'interfaccia fluente (forse di scarso esempio) che mi consente di creare un Dipendente come:

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

Potrebbe facilmente essere scritto con inizializzatori come:

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

Potrei anche aver usato i parametri con nome nei costruttori in questo esempio.

    
posta Andrew Hanlon 19.04.2011 - 10:44
fonte

6 risposte

26

Scrivere un'interfaccia fluente (ho dilettato con esso) richiede più impegno, ma ha un pay-off perché se lo fai bene, l'intento del codice utente risultante è più ovvio. È essenzialmente una forma di linguaggio specifico del dominio.

In altre parole, se il tuo codice viene letto molto più di quello che è scritto (e quale codice non lo è?), dovresti prendere in considerazione la creazione di un'interfaccia fluente.

Le interfacce fluenti riguardano più il contesto e sono molto più di semplici modi per configurare gli oggetti. Come puoi vedere nel link sopra, ho usato un'API fluent-ish per raggiungere:

  1. Contesto (quindi quando in genere esegui molte azioni in una sequenza con la stessa cosa, puoi concatenare le azioni senza dover dichiarare il contesto più e più volte).
  2. Discoverability (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 può fare con l'interfaccia di rete. plm.Network.X10. fornisce il sottoinsieme di azioni di rete per i dispositivi X10. Non lo otterrete con gli inizializzatori del costruttore (a meno che non vogliate costruire un oggetto per ogni diverso tipo di azione, che non è idiomatica).
  3. Reflection (non usato nell'esempio sopra) - la capacità di prendere un'espressione LINQ passata e manipolarla è uno strumento molto potente, in particolare in alcune API helper che ho creato per i test unitari. Posso passare in un'espressione getter di proprietà, creare un sacco di espressioni utili, compilare ed eseguire quelle, o anche usare il getter di proprietà per configurare il mio contesto.

Una cosa che di solito faccio è:

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

Non vedo come si possa fare qualcosa del genere senza un'interfaccia fluente.

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();
    
risposta data 19.04.2011 - 15:30
fonte
23

Scott Hanselman ne parla in Episodio 260 del suo podcast Hanselminutes con Jonathan Carter. Spiegano che un'interfaccia fluente è più simile a un'interfaccia utente su un'API. Non dovresti fornire un'interfaccia fluente come unico punto di accesso, ma piuttosto fornirlo come una sorta di interfaccia utente di codice in cima alla "normale interfaccia API".

Jonathan Carter parla anche di API design sul suo blog .

    
risposta data 19.04.2011 - 15:28
fonte
14

Sono un po 'in ritardo ma ho pensato di fare un bilancio di questo ...

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

Se il tuo obiettivo è semplicemente creare catene di codici a una riga massicce come una sorta di pseudo-black-box, probabilmente stai abbaiando dall'albero sbagliato. Se invece lo stai utilizzando per aggiungere valore alla tua interfaccia API fornendo un mezzo per concatenare le chiamate ai metodi e migliorare la leggibilità del codice, allora con un sacco di buona pianificazione e impegno penso che ne valga la pena.

Eviterei di seguire ciò che sembra diventare un "pattern" comune quando creo interfacce fluenti, in cui chiami tutti i tuoi fluenti metodi "con" - qualcosa, poiché ruba un'interfaccia API potenzialmente valida del suo contesto, e quindi il suo valore intrinseco.

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

    
risposta data 24.11.2011 - 07:23
fonte
5

Nota iniziale: mi sto occupando di una supposizione nella domanda e traggo le mie conclusioni specifiche (alla fine di questo post) da quella. Poiché questo probabilmente non offre una risposta completa e comprensibile, lo segnalo come CW.

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

Could easily be written with initializers like:

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 il nuovo Employee è anche Add ed alla raccolta Employees - suggerisce solo che un nuovo oggetto è Create d .

  • Il significato di ….WithX(…) è ambiguo, in particolare per le persone che arrivano da F #, che ha una parola chiave with per le espressioni oggetto : potrebbero interpretare obj.WithX(x) come oggetto nuovo derivato da obj identico a obj tranne che per la sua proprietà X , 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 la tua API fluente abbia due significati diversi per With sta rendendo difficile interpretare correttamente ciò che sta accadendo ... che è forse il motivo per cui hai utilizzato il rientro nel tuo esempio per dimostrare il significato inteso di quel codice. Sarebbe più chiaro come questo:

    (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 funzione linguistica abbastanza semplice, new T { X = x } , con un'API fluente ( Ts.CreateNew().WithX(x) ) può ovviamente essere fatta, ma:

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

  2. progettarlo potrebbe essere più lavoro del necessario: in questo esempio, l'API fluente aggiunge pochissimo "comfort dell'utente" all'API sottostante (una funzionalità linguistica). Si potrebbe dire che una API fluente dovrebbe rendere "più facile da usare" la funzione API / lingua sottostante; cioè, dovrebbe salvare il programmatore con 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 (vedere la conclusione n. 1 sopra).

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

risposta data 10.08.2013 - 19:25
fonte
2

Mi piace lo stile fluente, esprime molto chiaramente l'intento. Con l'esempio initaliser dell'oggetto che hai dopo, devi avere setter di proprietà pubblica per usare quella sintassi, non con lo stile fluente. Dicendo che, con il tuo esempio, non guadagni molto rispetto ai public setter perché hai quasi scelto un set java-esque / get style of method.

Il che mi porta al secondo punto, non sono sicuro che se usassi lo stile fluido nel modo in cui tu hai, con molti setter di proprietà, probabilmente userò la seconda versione per quello, la trovo meglio quando ci sono molti verbi da concatenare insieme, o almeno un sacco di azioni piuttosto che di impostazioni.

    
risposta data 19.04.2011 - 16:27
fonte
1

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

Personalmente non vedo come le moderne funzionalità di C # possano impedire l'utilità di tale approccio. Preferirei dire che vanno di pari passo. Per esempio. è ancora più facile ottenere tale interfaccia utilizzando 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.

    
risposta data 19.04.2011 - 15:22
fonte

Leggi altre domande sui tag