Perché il concatenamento dei setter non è convenzionale?


46

Avere il concatenamento implementato su bean è molto utile: non è necessario sovraccaricare costruttori, mega costruttori, fabbriche e offre una maggiore leggibilità. Non riesco a pensare a nessun aspetto negativo, a meno che tu non voglia che il tuo oggetto sia immutabile , nel qual caso non avrebbe comunque setter. Quindi c'è un motivo per cui questa non è una convenzione OOP?

public class DTO {

    private String foo;
    private String bar;

    public String getFoo() {
         return foo;
    }

    public String getBar() {
        return bar;
    }

    public DTO setFoo(String foo) {
        this.foo = foo;
        return this;
    }

    public DTO setBar(String bar) {
        this.bar = bar;
        return this;
    }

}

//...//

DTO dto = new DTO().setFoo("foo").setBar("bar");

32
Perché Java potrebbe essere l'unica lingua in cui i setter non sono un abominio per l'uomo ...
Telastyn,

11
È un cattivo stile perché il valore restituito non è semanticamente significativo. È fuorviante. L'unico lato positivo è salvare pochissimi tasti.
usr

58
Questo modello non è affatto raro. C'è persino un nome per questo. Si chiama interfaccia fluente .
Philipp,

9
Puoi anche astrarre questa creazione in un builder per un risultato più leggibile. myCustomDTO = DTOBuilder.defaultDTO().withFoo("foo").withBar("bar").Build();Lo farei, in modo da non essere in conflitto con l'idea generale che i setter siano vuoti.

9
@Philipp, anche se tecnicamente hai ragione, non direi che new Foo().setBar('bar').setBaz('baz')sembra molto "fluente". Voglio dire, certo che potrebbe essere implementato esattamente allo stesso modo, ma mi aspetterei davvero di leggere qualcosa di più simileFoo().barsThe('bar').withThe('baz').andQuuxes('the quux')
Wayne Werner,

Risposte:


50

Quindi c'è un motivo per cui questa non è una convenzione OOP?

La mia ipotesi migliore: perché viola CQS

Hai un comando (che modifica lo stato dell'oggetto) e una query (che restituisce una copia dello stato - in questo caso, l'oggetto stesso) miscelati nello stesso metodo. Questo non è necessariamente un problema, ma viola alcune delle linee guida di base.

Ad esempio, in C ++, std :: stack :: pop () è un comando che restituisce void e std :: stack :: top () è una query che restituisce un riferimento all'elemento superiore nello stack. Classicamente, ti piacerebbe combinare i due, ma non puoi farlo ed essere al sicuro. (Non è un problema in Java, perché l'operatore di assegnazione in Java non genera).

Se DTO fosse un tipo di valore, potresti ottenere un fine simile con

public DTO setFoo(String foo) {
    return new DTO(foo, this.bar);
}

public DTO setBar(String bar) {
    return new DTO(this.foo, bar);
}

Inoltre, concatenare i valori di ritorno è un colossale dolore quando si ha a che fare con l'eredità. Vedi il "Modello di modello curiosamente ricorrente"

Infine, c'è il problema che il costruttore predefinito dovrebbe lasciarti con un oggetto che si trova in uno stato valido. Se è necessario eseguire un gruppo di comandi per ripristinare l'oggetto a uno stato valido, qualcosa è andato molto male.


4
Finalmente un vero catain qui. L'ereditarietà è il vero problema. Penso che il concatenamento potrebbe essere un setter convenzionale per oggetti di rappresentazione dei dati utilizzati nella composizione.
Ben

6
"restituire una copia dello stato da un oggetto" - non lo fa.
user253751

2
Direi che il motivo principale per seguire queste linee guida (con alcune notevoli eccezioni come gli iteratori) è che rende il codice molto più facile da ragionare.
jpmc26,

1
@MatthieuM. no. Parlo principalmente Java, ed è prevalente lì nelle API "fluide" avanzate - Sono sicuro che esiste anche in altre lingue. In sostanza, rendi generico il tuo tipo su un tipo che si estende da solo. Quindi aggiungere un abstract T getSelf()metodo con restituisce il tipo generico. Ora, invece di tornare thisdai tuoi setter, tu return getSelf()e poi qualsiasi classe dominante semplicemente rendi il tipo generico in sé e ritorni thisda getSelf. In questo modo i setter restituiscono il tipo effettivo e non il tipo dichiarante.
Boris the Spider

1
@MatthieuM. veramente? Sembra simile a CRTP per C ++ ...
Inutile

33
  1. Il salvataggio di alcuni tasti non è convincente. Potrebbe essere carino, ma le convenzioni OOP si preoccupano più di concetti e strutture, non di battiture.

  2. Il valore restituito non ha significato.

  3. Ancor più che insignificante, il valore di ritorno è fuorviante, poiché gli utenti possono aspettarsi che il valore di ritorno abbia un significato. Potrebbero aspettarsi che si tratti di un "setter immutabile"

    public FooHolder {
        public FooHolder withFoo(int foo) {
            /* return a modified COPY of this FooHolder instance */
        }
    }
    

    In realtà il tuo setter muta l'oggetto.

  4. Non funziona bene con l'eredità.

    public FooHolder {
        public FooHolder setFoo(int foo) {
            ...
        }
    }
    
    public BarHolder extends FooHolder {
        public FooHolder setBar(int bar) {
            ...
        }
    } 
    

    posso scrivere

    new BarHolder().setBar(2).setFoo(1)

    ma no

    new BarHolder().setFoo(1).setBar(2)

Per me, da 1 a 3 sono quelli importanti. Il codice ben scritto non riguarda il testo piacevolmente organizzato. Un codice ben scritto riguarda concetti, relazioni e struttura fondamentali. Il testo è solo un riflesso esteriore del vero significato del codice.


2
Tuttavia, la maggior parte di questi argomenti si applica a qualsiasi interfaccia fluida / concatenabile.
Casey,

@ Casey, si. I costruttori (cioè con i setter) sono il caso più frequente di concatenamento.
Paul Draper,

10
Se hai familiarità con l'idea di un'interfaccia fluida, # 3 non si applica, dal momento che probabilmente sospetti un'interfaccia fluida dal tipo restituito. Devo anche essere in disaccordo con "Il codice ben scritto non riguarda il testo piacevolmente organizzato". Non è tutto, ma il testo ben organizzato è piuttosto importante, poiché consente al lettore umano del programma di capirlo con meno sforzo.
Michael Shaw,

1
Il concatenamento setter non consiste nel disporre piacevolmente il testo o nel salvare i tasti premuti. Si tratta di ridurre il numero di identificatori. Con il concatenamento dei setter è possibile creare e configurare l'oggetto in una singola espressione, il che significa che potrebbe non essere necessario salvarlo in una variabile: una variabile che dovrai nominare e che rimarrà lì fino alla fine dell'ambito.
Idan Arye,

3
Riguardo al punto 4, è qualcosa che può essere risolto in Java come segue: public class Foo<T extends Foo> {...}con il setter che ritorna Foo<T>. Anche quei metodi "setter", preferisco chiamarli "con" metodi. Se il sovraccarico funziona bene, quindi Foo<T> with(Bar b) {...}, altrimenti Foo<T> withBar(Bar b).
YoYo

12

Non penso che questa sia una convenzione OOP, è più legata al design del linguaggio e alle sue convenzioni.

Sembra che ti piaccia usare Java. Java ha una specifica JavaBeans che specifica che il tipo di ritorno del setter deve essere nullo, cioè è in conflitto con il concatenamento dei setter. Questa specifica è ampiamente accettata e implementata in una varietà di strumenti.

Naturalmente potresti chiederti, perché non concatenare parte delle specifiche. Non conosco la risposta, forse questo modello non era conosciuto / popolare in quel momento.


Sì, JavaBeans è esattamente quello che avevo in mente.
Ben

Non penso che la maggior parte delle applicazioni che usano JavaBeans si preoccupi, ma potrebbero (usando la riflessione per afferrare i metodi che sono chiamati 'set ...', hanno un singolo parametro e sono nulli ). Molti programmi di analisi statica lamentano di non controllare un valore di ritorno da un metodo che restituisce qualcosa e questo potrebbe essere molto fastidioso.

Le chiamate riflettenti @MichaelT per ottenere metodi in Java non specificano il tipo restituito. La chiamata di un metodo in questo modo restituisce Object, il che può comportare la restituzione del singleton di tipo Voidse il metodo specifica voidcome tipo di ritorno. In altre notizie, apparentemente "lasciare un commento ma non votare" è motivo di fallimento di un audit di "primo post" anche se è un buon commento. Incolpo Shog per questo.

7

Come altri hanno già detto, questa è spesso definita un'interfaccia fluida .

Normalmente i setter sono chiamate che passano in variabili in risposta al codice logico in un'applicazione; la tua classe DTO ne è un esempio. Il codice convenzionale quando i setter non restituiscono nulla è normale per questo. Altre risposte hanno spiegato il modo.

Tuttavia, ci sono alcuni casi in cui un'interfaccia fluida può essere una buona soluzione, questi hanno in comune.

  • Le costanti sono per lo più passate agli incastonatori
  • La logica del programma non cambia ciò che viene passato ai setter.

Impostazione della configurazione, ad esempio fluidificante

Id(x => x.Id);
Map(x => x.Name)
   .Length(16)
   .Not.Nullable();
HasMany(x => x.Staff)
   .Inverse()
   .Cascade.All();
HasManyToMany(x => x.Products)
   .Cascade.All()
   .Table("StoreProduct");

Impostazione dei dati di test nei test unitari, utilizzando TestDataBulderClasses (Object Mothers) speciali

members = MemberBuilder.CreateList(4)
    .TheFirst(1).With(b => b.WithFirstName("Rob"))
    .TheNext(2).With(b => b.WithFirstName("Poya"))
    .TheNext(1).With(b => b.WithFirstName("Matt"))
    .BuildList(); // Note the "build" method sets everything else to
                  // senible default values so a test only need to define 
                  // what it care about, even if for example a member 
                  // MUST have MembershipId  set

Tuttavia, creare una buona interfaccia fluida è molto difficile , quindi ne vale la pena solo quando si dispone di molte impostazioni "statiche". Inoltre, l'interfaccia fluida non deve essere confusa con classi "normali"; quindi viene spesso utilizzato il modello di costruzione.


5

Penso che gran parte del motivo non sia una convenzione per concatenare un setter dopo l'altro perché in questi casi è più tipico vedere un oggetto opzioni o parametri in un costruttore. C # ha anche una sintassi di inizializzazione.

Invece di:

DTO dto = new DTO().setFoo("foo").setBar("bar");

Si potrebbe scrivere:

(in JS)

var dto = new DTO({foo: "foo", bar: "bar"});

(in C #)

DTO dto = new DTO{Foo = "foo", Bar = "bar"};

(in Java)

DTO dto = new DTO("foo", "bar");

setFooe non setBarsono più necessari per l'inizializzazione e possono essere utilizzati in seguito per la mutazione.

Mentre la catenabilità è utile in alcune circostanze, è importante non provare a riempire tutto su una sola riga solo per ridurre i caratteri di nuova riga.

Per esempio

dto.setFoo("foo").setBar("fizz").setFizz("bar").setBuzz("buzz");

rende più difficile leggere e capire cosa sta succedendo. Riformattazione in:

dto.setFoo("foo")
    .setBar("fizz")
    .setFizz("bar")
    .setBuzz("buzz");

È molto più facile da capire e rende più evidente "l'errore" nella prima versione. Una volta eseguito il refactoring del codice in quel formato, non c'è alcun vantaggio reale rispetto a:

dto.setFoo("foo");
dto.setBar("bar");
dto.setFizz("fizz");
dto.setBuzz("buzz");

3
1. Non posso davvero essere d'accordo con il problema della leggibilità, l'aggiunta di istanza prima di ogni chiamata non lo rende affatto più chiaro. 2. I pattern di inizializzazione che hai mostrato con js e C # non hanno nulla a che fare con i costruttori: ciò che hai fatto in js è passato un singolo argomento, e quello che hai fatto in C # è uno shugar di sintassi che chiama geter e setter dietro le quinte, e java non ha lo shugar geter-setter come fa C #.
Ben

1
@Benedictus, stavo mostrando vari modi in cui le lingue OOP gestiscono questo problema senza concatenare. Il punto non era fornire un codice identico, il punto era mostrare alternative che rendessero inutili il concatenamento.
zzzzBov,

2
"L'aggiunta dell'istanza prima di ogni chiamata non rende affatto più chiara" Non ho mai sostenuto che l'aggiunta dell'istanza prima di ogni chiamata rendesse tutto più chiaro, ho solo detto che era relativamente equivalente.
zzzzBov,

1
VB.NET ha anche una parola chiave "With" utilizzabile che crea un riferimento temporaneo al compilatore in modo che ad esempio [usare /per rappresentare l'interruzione di riga] With Foo(1234) / .x = 23 / .y = 47sia equivalente Dim temp=Foo(1234) / temp.x = 23 / temp.y = 47. Tale sintassi non crea ambiguità poiché di .xper sé non può avere altro significato se non quello di legarsi all'affermazione "Con" immediatamente circostante [se non ce n'è, o l'oggetto non ha membro x, allora non ha .xsenso]. Oracle non ha incluso nulla di simile in Java, ma un tale costrutto si adatterebbe perfettamente al linguaggio.
supercat

5

Tale tecnica viene effettivamente utilizzata nel modello Builder.

x = ObjectBuilder()
        .foo(5)
        .bar(6);

Tuttavia, in generale è evitato perché è ambiguo. Non è ovvio se il valore restituito sia l'oggetto (quindi è possibile chiamare altri setter) o se l'oggetto restituito è il valore che è stato appena assegnato (anche un modello comune). Di conseguenza, il Principio di Least Surprise suggerisce che non dovresti provare a supporre che l'utente voglia vedere una soluzione o l'altra, a meno che non sia fondamentale per il design dell'oggetto.


6
La differenza è che, quando si utilizza il modello builder, di solito si scrive un oggetto di sola scrittura (il Builder) che alla fine costruisce un oggetto di sola lettura (immutabile) (qualunque classe si stia costruendo). A tale proposito, è auspicabile disporre di una lunga catena di chiamate di metodo poiché può essere utilizzata come singola espressione.
Darkhogg,

"o se l'oggetto return è il valore che è stato appena assegnato" Odio che, se voglio mantenere il valore che ho appena passato, lo inserirò prima in una variabile. Tuttavia, può essere utile ottenere il valore assegnato in precedenza (credo che alcune interfacce contenitore lo facciano).
JAB

@JAB Sono d'accordo, non sono un grande fan di quella notazione, ma ha i suoi posti. Quello che mi viene in mente è Obj* x = doSomethingToObjAndReturnIt(new Obj(1, 2, 3)); che penso abbia anche guadagnato popolarità perché rispecchia a = b = c = d, anche se non sono convinto che la popolarità sia fondata. Ho visto quello che hai citato, restituendo il valore precedente, in alcune librerie di operazioni atomiche. Morale della storia? È ancora più confuso di quanto abbia fatto sembrare =)
Cort Ammon,

Credo che gli accademici diventino un po 'pedanti: non c'è nulla di intrinsecamente sbagliato nel linguaggio. È straordinariamente utile per aggiungere chiarezza in alcuni casi. Prendiamo ad esempio la seguente classe di formattazione del testo che fa rientrare ogni riga di una stringa e disegna attorno ad essa una casella di arte ascii new BetterText(string).indent(4).box().print();. In quel caso, ho bypassato un mucchio di gobbledygook e preso una stringa, la ho rientrata, la ho inscatolata e l'ho prodotta. Potresti persino desiderare un metodo di duplicazione all'interno della cascata (come, diciamo, .copy()) per consentire a tutte le seguenti azioni di non modificare più l'originale.
tgm1024,

2

Questo è più un commento che una risposta, ma non posso commentare, quindi ...

volevo solo ricordare che questa domanda mi ha sorpreso perché non lo vedo per niente insolito . In realtà, nel mio ambiente di lavoro (sviluppatore web) è molto molto comune.

Ad esempio, è così che la dottrina di Symfony: generate: entity comando genera automaticamente tutto setters , per impostazione predefinita .

jQuery Kinda catene maggior parte dei suoi metodi in un modo molto simile.


Js è un abominio
Ben

@Benedictus Direi che PHP è l'abominio più grande. JavaScript è un linguaggio perfettamente perfetto ed è diventato abbastanza carino con l'inclusione delle funzionalità ES6 (anche se sono sicuro che alcune persone preferiscono ancora CoffeeScript o varianti; personalmente, non sono un fan di come CoffeeScript gestisce l'ambito variabile mentre controlla l'ambito esterno prima piuttosto che trattare le variabili assegnate nell'ambito locale come locali a meno che non sia esplicitamente indicato come non locale / globale come Python lo fa).
JAB

@Benedictus Concordo con JAB. Nei miei anni del college ho usato per guardare in basso JS come un abominio wanna-be linguaggio di scripting, ma dopo aver lavorato un paio di anni con esso, e imparare la DESTRA modo di utilizzarlo, ho imparato ad ... in realtà l'amore è . E penso che con ES6 diventerà un linguaggio davvero maturo . Ma ciò che mi fa girare la testa è ... cosa c'entra la mia risposta con JS? ^^ U
xDaizu

In realtà ho lavorato un paio d'anni con js e posso dire di aver imparato i suoi modi. Tuttavia vedo questa lingua come nient'altro che un errore. Ma sono d'accordo, Php è molto peggio.
Ben

@Benedictus Capisco la tua posizione e la rispetto. Mi piace ancora però. Certo, ha le sue stranezze e vantaggi (che lingua non lo fa?) Ma sta costantemente avanzando nella giusta direzione. Ma ... in realtà non mi piace così tanto che devo difenderlo. Non ottengo nulla dalle persone che lo amano o odiano ... ahahah Inoltre, questo non è un posto per quello, la mia risposta non era nemmeno su JS.
xDaizu
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.