Quando un metodo di una classe dovrebbe restituire la stessa istanza dopo aver modificato se stesso?


9

Ho una classe che ha tre metodi A(), B()e C(). Tali metodi modificano la propria istanza.

Mentre i metodi devono restituire un'istanza quando l'istanza è una copia separata (proprio come Clone()), ho avuto una libera scelta di restituire voido la stessa istanza ( return this;) quando si modifica la stessa istanza nel metodo e non si restituisce alcun altro valore.

Quando decido di restituire la stessa istanza modificata, posso fare catene di metodi ordinate come obj.A().B().C();.

Questo sarebbe l'unico motivo per farlo?

Va bene anche modificare la propria istanza e restituirla anche? O dovrebbe restituire solo una copia e lasciare l'oggetto originale come prima? Perché quando restituisce la stessa istanza modificata l'utente potrebbe presumere che il valore restituito sia una copia, altrimenti non verrebbe restituito? Se va bene, qual è il modo migliore per chiarire queste cose sul metodo?

Risposte:


7

Quando usare il concatenamento

Il concatenamento di funzioni è molto popolare tra le lingue in cui un IDE con il completamento automatico è un luogo comune. Ad esempio, quasi tutti gli sviluppatori C # utilizzano Visual Studio. Pertanto, se stai sviluppando con C # l'aggiunta di concatenamento ai tuoi metodi può essere un risparmio di tempo per gli utenti di quella classe perché Visual Studio ti aiuterà a costruire la catena.

D'altra parte, linguaggi come PHP che sono altamente dinamici in natura e spesso non hanno il supporto di auto-completamento negli IDE vedranno meno classi che supportano il concatenamento. Il concatenamento sarà appropriato solo quando si impiegano phpDocs corretti per esporre i metodi concatenabili.

Che cos'è il concatenamento?

Data una classe denominata, Fooi due metodi seguenti sono entrambi concatenabili.

function what() { return this; }
function when() { return new Foo(this); }

Il fatto che uno sia un riferimento all'istanza corrente e che crei una nuova istanza non cambia il fatto che si tratti di metodi concatenabili.

Non esiste una regola d'oro secondo cui un metodo concatenabile deve fare riferimento solo all'oggetto corrente. Infatti, i metodi concatenabili possono essere suddivisi in due classi diverse. Per esempio;

class B { function When() { return true; } };
class A { function What() { return new B(); } };

var a = new A();
var x = a.What().When();

Non c'è alcun riferimento a thisin nessuno degli esempi sopra. Il codice a.What().When()è un esempio di concatenamento. La cosa interessante è che il tipo di classe Bnon è mai assegnato a una variabile.

Un metodo viene concatenato quando il valore restituito viene utilizzato come componente successivo di un'espressione.

Ecco qualche altro esempio

 // return value never assigned.
 myFile.Open("something.txt").Write("stuff").Close();

// two chains used in expression
int x = a.X().Y() * b.X().Y();

// a chain that creates new strings
string name = str.Substring(1,10).Trim().ToUpperCase();

Quando usare thisenew(this)

Le stringhe nella maggior parte delle lingue sono immutabili. Quindi il concatenamento delle chiamate dei metodi comporta sempre la creazione di nuove stringhe. Dove come oggetto come StringBuilder può essere modificato.

La coerenza è la migliore pratica.

Se disponi di metodi che modificano lo stato di un oggetto e restituiscono this, non mescolare metodi che restituiscono nuove istanze. Invece, crea un metodo specifico chiamato Clone()che lo farà esplicitamente.

 var x  = a.Foo().Boo().Clone().Foo();

Questo è molto più chiaro su ciò che sta accadendo all'interno a.

Il passo fuori e trucco indietro

Io chiamo questo il trucco di andata e ritorno , perché risolve molti problemi comuni legati al concatenamento. Fondamentalmente significa che esci dalla classe originale in una nuova classe temporanea e poi torna alla classe originale.

La classe temporanea esiste solo per fornire funzionalità speciali alla classe originale, ma solo a condizioni speciali.

Ci sono volte in cui una catena deve cambiare stato , ma la classe Anon può rappresentare tutti quei possibili stati . Quindi durante una catena viene introdotta una nuova classe che contiene un riferimento a A. Ciò consente al programmatore di entrare in uno stato e tornare a A.

Ecco il mio esempio, sia noto lo stato speciale B.

class A {
    function Foo() { return this; }
    function Boo() { return this; }
    function Change() return new B(this); }
}

class B {
    var _a;
    function (A) { _a = A; }
    function What() { return this; }
    function When() { return this; }
    function End() { return _a; }
}

var a = new A();
a.Foo().Change().What().When().End().Boo();

Questo è un esempio molto semplice. Se si desidera avere un maggiore controllo, è Bpossibile tornare a un nuovo super-tipo Acon metodi diversi.


4
Davvero non capisco perché si consiglia il concatenamento dei metodi in base esclusivamente al supporto del completamento automatico simile a Intellisense. Le catene di metodi o le interfacce fluide sono un modello di progettazione API per qualsiasi linguaggio OOP; l'unica cosa che fa il completamento automatico è prevenire errori di battitura e errori di digitazione.
amon,

@amon hai letto male quello che ho detto. Ho detto che è più popolare quando l'intellisense è comune per una lingua. Non ho mai detto che dipendesse dalla funzione.
Reactgular

3
Mentre questa risposta mi aiuta molto a capire le possibilità del concatenamento, mi manca ancora la decisione su quando usare il concatenamento. Certo, la coerenza è la migliore . Tuttavia, mi riferisco al caso quando modifico il mio oggetto nella funzione e return this. Come posso aiutare gli utenti delle mie librerie a gestire e comprendere questa situazione? (Consentire loro di concatenare i metodi, anche quando non è necessario, sarebbe davvero a posto? O dovrei attenermi a un solo modo, richiedere di cambiare affatto?)
Martin Braun

@modiX se la mia risposta è corretta, accettala come risposta. Le altre cose che stai cercando sono opinioni su come usare il concatenamento e non esiste una risposta giusta / sbagliata per questo. Dipende da te decidere. Forse ti stai preoccupando troppo dei piccoli dettagli?
Reactgular,

3

Tali metodi modificano la propria istanza.

A seconda della lingua, avere metodi che restituiscono void/ unite modificano la loro istanza o parametri non è idiomatico. Anche nei linguaggi che lo facevano di più (C #, C ++), sta andando fuori moda con lo spostamento verso una programmazione di stile più funzionale (oggetti immutabili, funzioni pure). Ma supponiamo che ci sia una buona ragione per ora.

Questo sarebbe l'unico motivo per farlo?

Per alcuni comportamenti (si pensi x++) si prevede che l'operazione restituisca il risultato, anche se modifica la variabile. Ma questa è in gran parte l'unica ragione per farlo da soli.

Va bene anche modificare la propria istanza e restituirla anche? O dovrebbe restituire solo una copia e lasciare l'oggetto originale come prima? Perché quando restituisce la stessa istanza modificata l'utente potrebbe ammettere che il valore restituito è una copia, altrimenti non verrebbe restituito? Se va bene, qual è il modo migliore per chiarire queste cose sul metodo?

Dipende.

Nelle lingue in cui copia / nuovo e ritorno sono comuni (C # LINQ e stringhe), restituire lo stesso riferimento sarebbe confuso. Nelle lingue in cui modifica e restituzione sono comuni (alcune librerie C ++), la copia sarebbe confusa.

Rendere inequivocabile la firma (restituendo voido usando costrutti di linguaggio come proprietà) sarebbe il modo migliore per chiarire. Dopodiché, rendere chiaro il nome SetFooper mostrare che stai modificando l'istanza esistente è buono. Ma la chiave è mantenere i modi di dire della lingua / libreria con cui stai lavorando.


Grazie per la tua risposta. Sto lavorando principalmente con il framework .NET, e dalla tua risposta è pieno di cose non idiomatiche. Mentre cose come i metodi LINQ restituiscono una nuova copia dell'originale dopo aver aggiunto operazioni ecc. Cose come Clear()o Add()di qualsiasi tipo di raccolta modificheranno la stessa istanza e restituiranno void. In entrambi i casi dici che sarebbe confuso ...
Martin Braun,

@modix - LINQ non restituisce una copia durante l'aggiunta delle operazioni.
Telastyn,

Perché posso fare obj = myCollection.Where(x => x.MyProperty > 0).OrderBy(x => x.MyProperty);allora? Where()restituisce un nuovo oggetto raccolta. OrderBy()restituisce anche un oggetto di raccolta diverso, poiché il contenuto è ordinato.
Martin Braun,

1
@modix - Tornano nuovi oggetti che implementano IEnumerable, ma per essere chiari, non sono copie, e non sono raccolte (ad eccezione di ToList, ToArraye così via).
Telastyn,

Esatto, non sono copie, inoltre sono nuovi oggetti, anzi. Inoltre, dicendo raccolta, mi riferivo agli oggetti che puoi contare (oggetti che contengono più di un oggetto in un qualsiasi tipo di array), non alle classi da cui ereditano ICollection. Colpa mia.
Martin Braun,

0

(Ho assunto C ++ come linguaggio di programmazione)

Per me questo è principalmente un aspetto di leggibilità. Se A, B, C sono modificatori, specialmente se si tratta di un oggetto temporaneo passato come parametro a una funzione, ad es

do_something(
      CPerson('name').age(24).height(160).weight(70)
      );

rispetto a

{
   CPerson person('name');
   person.set_age(24);
   person.set_height(160);
   person.set_weight(70);
   do_something(person);
}

Regradando se è giusto restituire un riferimento all'istanza modificata, direi di sì e ti indirizzerei ad esempio agli operatori di flusso '>>' e '<<' ( http://www.cprogramming.com/tutorial/ operator_overloading.html )


1
Hai pensato male, è C #. Tuttavia, voglio che la mia domanda sia più comune.
Martin Braun,

Certo, avrebbe potuto essere anche Java, ma quello era un punto secondario solo per mettere il mio esempio con gli operatori nel contesto. L'essenza era che si riduce alla leggibilità, che è influenzata dalle aspettative e dal background del lettore, che a loro volta sono influenzati dalle consuete pratiche nel particolare linguaggio / framework di cui ci si occupa - come hanno anche sottolineato le altre risposte.
AdrianI,

0

Puoi anche fare il metodo concatenando cosa con return return piuttosto che modificare return.

Un buon esempio di C # è string.Replace (a, b) che non cambia la stringa su cui è stata invocata ma restituisce invece una nuova stringa che consente di concatenarsi allegramente.


Si, lo so. Ma non volevo fare riferimento a questo caso, poiché devo usare questo caso quando non voglio modificare la propria istanza, comunque. Tuttavia, grazie per averlo sottolineato.
Martin Braun,

Per altre risposte, la coerenza della semantica è importante. Dal momento che puoi essere coerente con il ritorno di copia e non puoi essere coerente con il ritorno di modifica (perché non puoi farlo con oggetti immutabili), direi che la risposta alla tua domanda è non usare il ritorno di copia.
Peter finito il
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.