Perché incatenare setter non convenzionali?

46

Avere il chaining implementato su bean è molto utile: non c'è bisogno di sovraccaricare costruttori, mega costruttori, fabbriche e ti dà una maggiore leggibilità. Non riesco a pensare ad alcun aspetto negativo, a meno che tu non voglia che il tuo oggetto sia immutabile , nel qual caso non avrebbe comunque alcun setter. Quindi c'è una ragione 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");
    
posta Ben 02.02.2016 - 18:06
fonte

7 risposte

48

So is there a reason why isn't this a OOP convention?

La mia ipotesi migliore: perché viola CQS

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

Per 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 fare in modo che e siano eccezionalmente sicuri. (Non è un problema in Java, perché l'operatore di assegnazione in Java non gira).

Se DTO fosse un tipo di valore, potresti raggiungere una 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 dolore colossale quando si ha a che fare con l'ereditarietà. Vedi "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 deve eseguire un gruppo di comandi per ripristinare l'oggetto in uno stato valido, qualcosa è andato molto male.

    
risposta data 02.02.2016 - 18:51
fonte
32
  1. Il salvataggio di alcune sequenze di tasti non è convincente. Potrebbe essere bello, ma le convenzioni OOP si preoccupano di più su concetti e strutture, non su battute.

  2. Il valore restituito non ha significato.

  3. Ancor più che essere privo di significato, il valore restituito è fuorviante, poiché gli utenti potrebbero aspettarsi che il valore restituito abbia un significato. Potrebbero aspettarsi che sia un "immerable setter"

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

    In realtà il setter cambia l'oggetto.

  4. Non funziona bene con l'ereditarietà.

    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 non

    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. Il codice ben scritto riguarda i concetti, le relazioni e la struttura fondamentali. Il testo è solo un riflesso esteriore del vero significato del codice.

    
risposta data 02.02.2016 - 21:37
fonte
12

Non penso che questa sia una convenzione OOP, è più legata alla progettazione della lingua e alle sue convenzioni.

Sembra che tu voglia usare Java. Java ha una specifica JavaBeans che specifica il tipo di reso del setter da invalidare , 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 noto / popolare in quel momento.

    
risposta data 02.02.2016 - 18:19
fonte
7

Come altre persone hanno detto, questa è spesso chiamata interfaccia fluente .

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

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

  • Le costanti passano principalmente ai setter
  • La logica del programma non cambia ciò che viene trasmesso ai setter.

Configurazione della configurazione, ad esempio fluent-nhibernate

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 speciali TestDataBulderClasses (Object Mothers)

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 un'interfaccia fluente è molto difficile , quindi vale la pena solo quando si dispone di molte impostazioni "statiche". Anche l'interfaccia fluente non deve essere mescolata con le classi "normali"; quindi spesso viene utilizzato lo schema di costruzione.

    
risposta data 03.02.2016 - 11:32
fonte
5

Penso che molto del motivo per cui non è una convenzione mettere in catena un settatore dopo l'altro è perché in quei casi è più tipico vedere un oggetto o un parametro di opzioni 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");

setFoo e setBar non sono più necessari per l'inizializzazione e possono essere utilizzati successivamente per la mutazione.

Sebbene la concatenabilità sia utile in alcune circostanze, è importante non provare a riempire tutto su un'unica linea solo per ridurre i caratteri di nuova riga.

Ad esempio

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

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

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 effettuato il refactoring del codice in quel formato, non c'è alcun vantaggio reale su:

dto.setFoo("foo");
dto.setBar("bar");
dto.setFizz("fizz");
dto.setBuzz("buzz");
    
risposta data 02.02.2016 - 18:40
fonte
5

Questa tecnica è effettivamente utilizzata nel modello Builder.

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

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

    
risposta data 03.02.2016 - 04:46
fonte
2

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

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

Ad esempio, questo è il modo in cui la dottrina di Symfony: generate: comando di entità genera automaticamente tutti setters , di default .

jQuery kinda raggruppa la maggior parte dei suoi metodi in un modo molto simile.

    
risposta data 04.02.2016 - 11:27
fonte

Leggi altre domande sui tag