Metodo di concatenamento: perché è una buona pratica o no?

Il concatenamento dei metodi è la pratica dei metodi object che restituiscono l’object stesso in modo che il risultato sia chiamato per un altro metodo. Come questo:

participant.addSchedule(events[1]).addSchedule(events[2]).setStatus('attending').save() 

Questo sembra essere considerato una buona pratica, dal momento che produce codice leggibile, o una “interfaccia fluente”. Tuttavia, a me sembra invece che si interrompa la notazione di chiamata dell’object implicita dall’orientamento dell’object stesso – il codice risultante non rappresenta le azioni che eseguono al risultato di un metodo precedente, il che è il modo in cui il codice orientato agli oggetti dovrebbe funzionare:

 participant.getSchedule('monday').saveTo('monnday.file') 

Questa differenza riesce a creare due significati diversi per la notazione a punti di “chiamare l’object risultante”: nel contesto del concatenamento, l’esempio precedente verrebbe letto come il salvataggio dell’object partecipante , anche se l’esempio è in realtà destinato a salvare la pianificazione object ricevuto da getSchedule.

Capisco che la differenza qui è se ci si aspetta che il metodo chiamato restituisca qualcosa o meno (nel qual caso restituirebbe l’object chiamato stesso per il concatenamento). Ma questi due casi non sono distinguibili dalla notazione stessa, ma solo dalla semantica dei metodi che vengono chiamati. Quando non si usa il concatenamento del metodo, posso sempre sapere che una chiamata al metodo funziona su qualcosa relativo al risultato della chiamata precedente – con il concatenamento, questa interruzione dell’assunzione, e devo semanticamente elaborare l’intera catena per capire quale sia l’object reale chiamato è veramente. Per esempio:

 participant.attend(event).setNotifications('silent').getSocialStream('twitter').postStatus('Joining '+event.name).follow(event.getSocialId('twitter')) 

Le ultime due chiamate al metodo si riferiscono al risultato di getSocialStream, mentre quelle precedenti si riferiscono al partecipante. Forse è una ctriggers pratica scrivere in realtà catene in cui il contesto cambia (è vero?), Ma anche in questo caso dovrai controllare costantemente se le catene di punti che sembrano simili siano di fatto mantenute nello stesso contesto o funzionino solo sul risultato .

A me sembra che mentre il metodo di concatenazione superficiale produce codice leggibile, sovraccaricare il significato della notazione a punti produce solo più confusione. Poiché non mi considero un guru della programmazione, presumo che la colpa sia mia. Quindi: cosa mi sto perdendo? Capisco che il metodo concatenando in qualche modo sia sbagliato? Ci sono dei casi in cui il metodo di concatenamento è particolarmente buono, o alcuni in cui è particolarmente cattivo?

Sidenote: Capisco che questa domanda possa essere letta come una dichiarazione di opinione mascherata come una domanda. Tuttavia, non lo è – voglio sinceramente capire perché il concatenamento è considerato una buona pratica, e dove sbaglio nel pensare che rompa la notazione inerente all’object.

Sono d’accordo che questo è soggettivo. Per la maggior parte evito il concatenamento di metodi, ma recentemente ho trovato anche un caso in cui era proprio la cosa giusta: avevo un metodo che accettava qualcosa come 10 parametri, e ne avevo bisogno di più, ma per la maggior parte del tempo dovevi solo specificare un pochi. Con l’override questo è diventato molto ingombrante molto velocemente. Invece ho optato per l’approccio concatenante:

 MyObject.Start() .SpecifySomeParameter(asdasd) .SpecifySomeOtherParameter(asdasd) .Execute(); 

Questo è qualcosa come un modello di fabbrica. L’approccio del concatenamento del metodo era facoltativo, ma rendeva più facile la scrittura del codice (specialmente con IntelliSense). Tenete presente che questo è un caso isolato, e non è una pratica generale nel mio codice.

Il punto è che nel 99% dei casi si può probabilmente fare altrettanto bene o addirittura meglio senza il concatenamento di metodi. Ma c’è l’1% dove questo è l’approccio migliore.

Solo i miei 2 centesimi;

Il concatenamento dei metodi rende complicato il debug: – Non è ansible inserire il punto di interruzione in un punto conciso in modo da poter sospendere il programma esattamente dove lo si desidera – Se uno di questi metodi genera un’eccezione e si ottiene un numero di riga, non si ha idea quale metodo nella “catena” ha causato il problema.

Penso che sia generalmente buona norma scrivere sempre linee brevi e concise. Ogni linea dovrebbe semplicemente effettuare una chiamata al metodo. Preferisci più linee a linee più lunghe.

EDIT: il commento indica che il metodo di concatenamento e interruzione di riga sono separati. Questo è vero. Tuttavia, a seconda del debugger, potrebbe essere ansible o meno collocare un punto di interruzione nel mezzo di un’istruzione. Anche se è ansible, l’utilizzo di righe separate con variabili intermedie offre molta più flessibilità e un sacco di valori che è ansible esaminare nella finestra di controllo che aiuta il processo di debug.

Personalmente, preferisco concatenare metodi che agiscono solo sull’object originale, ad esempio impostando più proprietà o chiamando metodi di tipo utility.

 foo.setHeight(100).setWidth(50).setColor('#ffffff'); foo.moveTo(100,100).highlight(); 

Non lo uso quando uno o più metodi concatenati restituirebbero qualsiasi object diverso da foo nel mio esempio. Mentre sintatticamente puoi concatenare qualsiasi cosa purché tu stia usando l’API corretta per quell’object nella catena, cambiando gli oggetti, IMHO rende le cose meno leggibili e può essere davvero confuso se le API per i diversi oggetti hanno qualche somiglianza. Se fai qualche chiamata di metodo molto comune alla fine ( .toString() , .print() , a prescindere) su quale object stai agendo? Qualcuno che legge casualmente il codice potrebbe non capire che sarebbe un object implicitamente restituito nella catena piuttosto che il riferimento originale.

Anche concatenare oggetti diversi può portare a errori nulli imprevisti. Nei miei esempi, supponendo che fo sia valido, tutte le chiamate al metodo sono “sicure” (ad es., Valide per foo). Nell’esempio dell’OP:

 participant.getSchedule('monday').saveTo('monnday.file') 

… non c’è alcuna garanzia (come uno sviluppatore esterno che osserva il codice) che getSchedule restituirà effettivamente un object di pianificazione valido, non nullo. Inoltre, il debug di questo stile di codice è spesso molto più difficile poiché molti IDE non valuteranno la chiamata al metodo in fase di debug come un object che è ansible esaminare. IMO, ogni volta che potresti avere bisogno di un object da ispezionare a scopo di debug, preferisco averlo in una variabile esplicita.

Martin Fowler ha una buona discussione qui:

Metodo di concatenamento

Quando usarlo

Il concatenamento di metodi può aggiungere molto alla leggibilità di un DSL interno e, di conseguenza, è diventato quasi un sinonimo di DSL interni in alcune menti. Il metodo Chaining è il migliore, tuttavia, quando viene utilizzato in combinazione con altre combinazioni di funzioni.

Il metodo Chaining è particolarmente efficace con grammatiche come parent :: = (this | that) *. L’uso di metodi diversi fornisce un modo leggibile di vedere quale argomento verrà dopo. Analogamente, gli argomenti opzionali possono essere facilmente saltati con Method Chaining. Un elenco di clausole obbligatorie, come genitore :: = primo secondo, non funziona così bene con il modulo di base, sebbene possa essere supportato utilizzando le interfacce progressive. La maggior parte delle volte preferirei la funzione annidata per quel caso.

Il problema più grande per Method Chaining è il problema di finitura. Mentre ci sono soluzioni alternative, di solito se ci si imbatte in questo è meglio usare una funzione annidata. La funzione annidata è anche una scelta migliore se ti trovi in ​​difficoltà con le variabili di contesto.

Secondo me, il metodo di concatenamento è un po ‘una novità. Certo, sembra bello ma non vedo alcun reale vantaggio.

Com’è:

 someList.addObject("str1").addObject("str2").addObject("str3") 

meglio di:

 someList.addObject("str1") someList.addObject("str2") someList.addObject("str3") 

L’eccezione potrebbe essere quando addObject () restituisce un nuovo object, nel qual caso il codice non legato potrebbe essere un po ‘più ingombrante come:

 someList = someList.addObject("str1") someList = someList.addObject("str2") someList = someList.addObject("str3") 

È pericoloso perché potresti dipendere da più oggetti del previsto, come se la tua chiamata restituisse un’istanza di un’altra class:

Darò un esempio:

foodStore è un object che è composto da molti negozi di alimentari che possiedi. foodstore.getLocalStore () restituisce un object che contiene informazioni sul negozio più vicino al parametro. getPriceforProduct (qualsiasi cosa) è un metodo di quell’object.

Quindi quando chiami foodStore.getLocalStore (parametri) .getPriceforProduct (qualsiasi cosa)

non dipendi solo da FoodStore come te, ma anche da LocalStore.

Se getPriceforProduct (qualcosa) dovesse mai cambiare, è necessario modificare non solo FoodStore ma anche la class che ha chiamato il metodo concatenato.

Dovresti sempre cercare un accoppiamento libero tra le classi.

Detto questo, personalmente mi piace incatenarli durante la programmazione di Ruby.

Benefici del concatenamento
cioè, dove mi piace usarlo

Un vantaggio del concatenamento che non ho visto menzionato era la capacità di usarlo durante l’iniziazione variabile, o quando si passa un nuovo object a un metodo, non si è sicuri se questo è un comportamento scorretto o meno.

So che questo è un esempio forzato, ma di avere le seguenti classi

 Public Class Location Private _x As Integer = 15 Private _y As Integer = 421513 Public Function X() As Integer Return _x End Function Public Function X(ByVal value As Integer) As Location _x = value Return Me End Function Public Function Y() As Integer Return _y End Function Public Function Y(ByVal value As Integer) As Location _y = value Return Me End Function Public Overrides Function toString() As String Return String.Format("{0},{1}", _x, _y) End Function End Class Public Class HomeLocation Inherits Location Public Overrides Function toString() As String Return String.Format("Home Is at: {0},{1}", X(), Y()) End Function End Class 

E dici di non avere accesso alla class base, o di dire che i valori predefiniti sono dinamici, in base al tempo, ecc. Sì, è ansible creare un’istanza quindi, quindi modificare i valori, ma ciò può diventare ingombrante, specialmente se stai passando i valori di un metodo:

  Dim loc As New HomeLocation() loc.X(1337) PrintLocation(loc) 

Ma questo non è molto più facile da leggere:

  PrintLocation(New HomeLocation().X(1337)) 

O che ne dici di un membro della class?

 Public Class Dummy Private _locA As New Location() Public Sub New() _locA.X(1337) End Sub End Class 

vs

 Public Class Dummy Private _locC As Location = New Location().X(1337) End Class 

Questo è il modo in cui ho usato il concatenamento, e in genere i miei metodi sono solo per la configurazione, quindi sono lunghi solo 2 righe, impostano un valore, quindi Return Me . Per noi ha ripulito enormi righe molto difficili da leggere e capire il codice in una riga che legge come una frase. qualcosa di simile a

 New Dealer.CarPicker().Subaru.WRX.SixSpeed.TurboCharged.BlueExterior.GrayInterior.Leather.HeatedSeats 

Vs Qualcosa come

 New Dealer.CarPicker(Dealer.CarPicker.Makes.Subaru , Dealer.CarPicker.Models.WRX , Dealer.CarPicker.Transmissions.SixSpeed , Dealer.CarPicker.Engine.Options.TurboCharged , Dealer.CarPicker.Exterior.Color.Blue , Dealer.CarPicker.Interior.Color.Gray , Dealer.CarPicker.Interior.Options.Leather , Dealer.CarPicker.Interior.Seats.Heated) 

Detrosity Of Chaining
cioè, dove non mi piace usarlo

Non uso il concatenamento quando ci sono molti parametri da passare alle routine, principalmente perché le linee diventano molto lunghe, e come l’OP citato può diventare confuso quando chiamate le routine ad altre classi per passare ad una delle i metodi di concatenamento.

C’è anche la preoccupazione che una routine restituisca dati non validi, quindi finora ho usato il concatenamento solo quando sto richiamando la stessa istanza chiamata. Come è stato sottolineato, se si fa una catena tra le classi si rende più difficile il debugging (quale restituisce null?) E si possono aumentare le dipendenze che si accoppiano tra le classi.

Conclusione

Come tutto nella vita e nella programmazione, Chaining non è né buono né cattivo se puoi evitare il male, quindi il concatenamento può essere un grande vantaggio.

Provo a seguire queste regole.

  1. Cerca di non mettere in catena tra le classi
  2. Crea routine specifiche per il concatenamento
  3. Fai solo UNA cosa in una routine di concatenamento
  4. Usalo quando migliora la leggibilità
  5. Usalo quando rende il codice più semplice

Il concatenamento dei metodi può consentire la progettazione di DSL avanzati direttamente in Java. In sostanza, puoi modellare almeno questi tipi di regole DSL:

 1. SINGLE-WORD 2. PARAMETERISED-WORD parameter 3. WORD1 [ OPTIONAL-WORD] 4. WORD2 { WORD-CHOICE-A | WORD-CHOICE-B } 5. WORD3 [ , WORD3 ... ] 

Queste regole possono essere implementate usando queste interfacce

 // Initial interface, entry point of the DSL interface Start { End singleWord(); End parameterisedWord(String parameter); Intermediate1 word1(); Intermediate2 word2(); Intermediate3 word3(); } // Terminating interface, might also contain methods like execute(); interface End {} // Intermediate DSL "step" extending the interface that is returned // by optionalWord(), to make that method "optional" interface Intermediate1 extends End { End optionalWord(); } // Intermediate DSL "step" providing several choices (similar to Start) interface Intermediate2 { End wordChoiceA(); End wordChoiceB(); } // Intermediate interface returning itself on word3(), in order to allow for // repetitions. Repetitions can be ended any time because this interface // extends End interface Intermediate3 extends End { Intermediate3 word3(); } 

Con queste semplici regole, è ansible implementare DSL complessi come SQL direttamente in Java, come fatto da jOOQ , una libreria che ho creato. Vedi un esempio SQL piuttosto complesso tratto dal mio blog qui:

 create().select( r1.ROUTINE_NAME, r1.SPECIFIC_NAME, decode() .when(exists(create() .selectOne() .from(PARAMETERS) .where(PARAMETERS.SPECIFIC_SCHEMA.equal(r1.SPECIFIC_SCHEMA)) .and(PARAMETERS.SPECIFIC_NAME.equal(r1.SPECIFIC_NAME)) .and(upper(PARAMETERS.PARAMETER_MODE).notEqual("IN"))), val("void")) .otherwise(r1.DATA_TYPE).as("data_type"), r1.NUMERIC_PRECISION, r1.NUMERIC_SCALE, r1.TYPE_UDT_NAME, decode().when( exists( create().selectOne() .from(r2) .where(r2.ROUTINE_SCHEMA.equal(getSchemaName())) .and(r2.ROUTINE_NAME.equal(r1.ROUTINE_NAME)) .and(r2.SPECIFIC_NAME.notEqual(r1.SPECIFIC_NAME))), create().select(count()) .from(r2) .where(r2.ROUTINE_SCHEMA.equal(getSchemaName())) .and(r2.ROUTINE_NAME.equal(r1.ROUTINE_NAME)) .and(r2.SPECIFIC_NAME.lessOrEqual(r1.SPECIFIC_NAME)).asField()) .as("overload")) .from(r1) .where(r1.ROUTINE_SCHEMA.equal(getSchemaName())) .orderBy(r1.ROUTINE_NAME.asc()) .fetch() 

Un altro bell’esempio è jRTF , un piccolo DSL progettato per certificare i documenti RTF direttamente in Java. Un esempio:

 rtf() .header( color( 0xff, 0, 0 ).at( 0 ), color( 0, 0xff, 0 ).at( 1 ), color( 0, 0, 0xff ).at( 2 ), font( "Calibri" ).at( 0 ) ) .section( p( font( 1, "Second paragraph" ) ), p( color( 1, "green" ) ) ) ).out( out ); 

Questo sembra piuttosto soggettivo.

Il concatenamento del metodo non è qualcosa che è intrinsecamente cattivo o buono.

La leggibilità è la cosa più importante.

(Considera anche che avere un gran numero di metodi concatenati renderà le cose molto fragili se qualcosa cambia)

Molti usano il concatenamento di metodi come una forma di convenienza piuttosto che avere in mente qualche problema di leggibilità. Il concatenamento dei metodi è accettabile se comporta l’esecuzione della stessa azione sullo stesso object, ma solo se migliora effettivamente la leggibilità e non solo per scrivere meno codice.

Sfortunatamente molti usano il concatenamento del metodo secondo gli esempi forniti nella domanda. Mentre possono ancora essere resi leggibili, sfortunatamente causano un elevato accoppiamento tra più classi, quindi non è desiderabile.

Il metodo di concatenamento può essere semplicemente una novità per la maggior parte dei casi, ma penso che abbia il suo posto. Un esempio potrebbe essere trovato nell’uso di Active Record di CodeIgniter :

 $this->db->select('something')->from('table')->where('id', $id); 

Sembra molto più pulito (e ha più senso, a mio parere) di:

 $this->db->select('something'); $this->db->from('table'); $this->db->where('id', $id); 

È davvero soggettivo; Ognuno ha la propria opinione.

Il punto assolutamente mancato qui è che il concatenamento del metodo consente di asciugare . È un efficace supporto per il “con” (che è mal implementato in alcune lingue).

 A.method1().method2().method3(); // one A A.method1(); A.method2(); A.method3(); // repeating A 3 times 

Ciò è importante per la stessa ragione per cui l’ASCIUTTO è sempre importante; se A si rivela essere un errore e queste operazioni devono essere eseguite su B, è necessario aggiornare solo in 1 posto, non in 3.

Pragmaticamente, il vantaggio è piccolo in questo caso. Ancora, un po ‘meno di battitura, un litle più robusto (ASCIUTTO), lo prendo io.

Sono d’accordo, quindi ho cambiato il modo in cui un’interfaccia fluente è stata implementata nella mia biblioteca.

Prima:

 collection.orderBy("column").limit(10); 

Dopo:

 collection = collection.orderBy("column").limit(10); 

Nell’implementazione “prima” le funzioni modificano l’object e finiscono in return this . Ho cambiato l’implementazione per restituire un nuovo object dello stesso tipo .

Il mio ragionamento per questo cambiamento :

  1. Il valore di ritorno non aveva nulla a che fare con la funzione, era puramente lì per supportare la parte di concatenamento, avrebbe dovuto essere una funzione di vuoto secondo OOP.

  2. Anche il metodo di concatenamento nelle librerie di sistema lo implementa in questo modo (come linq o stringa):

     myText = myText.trim().toUpperCase(); 
  3. L’object originale rimane intatto, consentendo all’utente API di decidere cosa fare con esso. Permette di:

     page1 = collection.limit(10); page2 = collection.offset(10).limit(10); 
  4. Un’implementazione di copia può anche essere utilizzata per build oggetti:

     painting = canvas.withBackground('white').withPenSize(10); 

    Dove la funzione setBackground(color) cambia l’istanza e non restituisce nulla (come dovrebbe) .

  5. Il comportamento delle funzioni è più prevedibile (vedere i punti 1 e 2).

  6. L’uso di un nome di variabile breve può anche ridurre la confusione di codice, senza forzare una API sul modello.

     var p = participant; // create a reference p.addSchedule(events[1]);p.addSchedule(events[2]);p.setStatus('attending');p.save() 

Conclusione:
Secondo me un’interfaccia fluente che utilizza un return this implementazione è semplicemente sbagliata.

Il bene:

  1. È conciso, ma ti permette di mettere più in una singola riga elegantemente.
  2. A volte puoi evitare l’uso di una variabile, che a volte può essere utile.
  3. Potrebbe funzionare meglio.

Il cattivo:

  1. Stai implementando i ritorni, essenzialmente aggiungendo funzionalità ai metodi sugli oggetti che non fanno realmente parte di ciò che questi metodi dovrebbero fare. Sta restituendo qualcosa che hai già solo per salvare pochi byte.
  2. Nasconde interruttori di contesto quando una catena conduce a un’altra. Puoi ottenerlo con getter, tranne che è piuttosto chiaro quando il contesto cambia.
  3. Il concatenamento su più linee appare brutto, non funziona bene con il rientro e può causare confusione da parte dell’operatore (specialmente nelle lingue con ASI).
  4. Se vuoi iniziare a restituire qualcos’altro che è utile per un metodo concatenato, potresti avere più difficoltà a risolverlo o a colpirlo.
  5. Stai scaricando il controllo su un’entity framework che normalmente non utilizzi a puro scopo di lucro, anche in lingue rigorosamente tipizzate, errori non sempre rilevabili.
  6. Potrebbe peggiorare.

Generale:

Un buon approccio è quello di non utilizzare il concatenamento in generale fino a quando non si presentano situazioni o moduli specifici sarebbero particolarmente adatti ad esso.

Il concatenamento può compromettere la leggibilità in alcuni casi, specialmente quando si pesa ai punti 1 e 2.

Con l’accasazione può essere usato in modo improprio, come invece di un altro approccio (passando un array per esempio) o mescolando metodi in modi bizzarri (parent.setSomething (). GetChild (). SetSomething (). GetParent (). SetSomething ()).

Penso che l’errore principale sia pensare che questo sia un approccio orientato agli oggetti in generale, quando in realtà è più un approccio di programmazione funzionale che altro.

I motivi principali per cui lo utilizzo sono sia per la leggibilità che per impedire che il mio codice venga inondato da variabili.

Non capisco veramente di cosa stiano parlando gli altri quando dicono che danneggia la leggibilità. È una delle forms di programmazione più concisa e coesa che ho usato.

Anche questo:

convertTextToVoice.LoadText ( “source.txt”) ConvertToVoice ( “destination.wav.”);

è come lo userei in genere. Usarlo per concatenare x numero di parametri non è il modo in cui generalmente lo uso. Se volessi inserire il numero x di parametri in una chiamata di metodo, userei la syntax params :

public void foo (oggetti params [] items)

E lancia gli oggetti in base al tipo o usa semplicemente una matrice di dati o una collezione in base al tuo caso d’uso.