Creazione di oggetti grandi e immutabili senza utilizzare costruttori con lunghi elenchi di parametri


96

Ho alcuni oggetti grandi (più di 3 campi) che possono e devono essere immutabili. Ogni volta che mi imbatto in quel caso tendo a creare abomini di costruttori con lunghi elenchi di parametri.

Non sembra giusto, è difficile da usare e la leggibilità ne soffre.

È anche peggio se i campi sono una sorta di tipo di raccolta come gli elenchi. Un sempliceaddSibling(S s) faciliterebbe così tanto la creazione dell'oggetto ma rende l'oggetto mutabile.

Cosa usate in questi casi?

Sono su Scala e Java, ma penso che il problema sia indipendente dal linguaggio fintanto che il linguaggio è orientato agli oggetti.

Soluzioni a cui riesco a pensare:

  1. "Abomini costruttori con lunghi elenchi di parametri"
  2. Il modello Builder

5
Penso che il modello builder sia la soluzione migliore e più standard a questo.
Zachary Wright,

@Zachary: Quello che stai sostenendo funziona solo per una forma speciale del pattern builder, come spiegato qui da Joshua Bloch: drdobbs.com/java/208403883?pgno=2 Preferisco chiamare il termine "interfaccia fluente" correlato questa "forma speciale del pattern builder" (vedi la mia risposta).
SyntaxT3rr0r

Risposte:


76

Bene, vuoi sia un oggetto più facile da leggere che immutabile una volta creato?

Penso che un'interfaccia fluente FATTA CORRETTAMENTE ti aiuterebbe.

Sarebbe simile a questo (esempio puramente inventato):

final Foo immutable = FooFactory.create()
    .whereRangeConstraintsAre(100,300)
    .withColor(Color.BLUE)
    .withArea(234)
    .withInterspacing(12)
    .build();

Ho scritto "FATTO CORRETTAMENTE" in grassetto perché la maggior parte dei programmatori Java sbagliano le interfacce fluenti e inquinano i loro oggetti con il metodo necessario per costruire l'oggetto, che ovviamente è completamente sbagliato.

Il trucco è che solo il metodo build () crea effettivamente un Foo (quindi Foo può essere immutabile).

FooFactory.create () , doveXXX (..) e withXXX (..) creano "qualcos'altro".

Quel qualcos'altro potrebbe essere una FooFactory, ecco un modo per farlo ...

You FooFactory sarebbe simile a questo:

// Notice the private FooFactory constructor
private FooFactory() {
}

public static FooFactory create() {
    return new FooFactory();
}

public FooFactory withColor( final Color col ) {
    this.color = color;
    return this;
}

public Foo build() {
    return new FooImpl( color, and, all, the, other, parameters, go, here );
}

11
@ all: per favore, non lamentatevi del suffisso "Impl" in "FooImpl": questa classe è nascosta all'interno di una fabbrica e nessuno la vedrà mai oltre alla persona che scrive l'interfaccia fluente. Tutto ciò che interessa all'utente è che ottiene un "Foo". Avrei potuto chiamare anche "FooImpl" "FooPointlessNitpick";)
SyntaxT3rr0r

5
Ti senti preventivo? ;) Sei stato pignolo in passato su questo. :)
Greg D

3
Credo che l'errore comune che sta riferendosi a è che la gente aggiungere le (ecc) metodi "withXXX" per l' Foooggetto, piuttosto che avere un separato FooFactory.
Dean Harding

3
Hai ancora il FooImplcostruttore con 8 parametri. Qual è il miglioramento?
Mot

4
Non chiamerei questo codice immutablee avrei paura che le persone riutilizzino gli oggetti di fabbrica perché pensano che lo sia. Voglio dire: FooFactory people = FooFactory.create().withType("person"); Foo women = people.withGender("female").build(); Foo talls = people.tallerThan("180m").build();dove tallsora conterrebbe solo donne. Questo non dovrebbe accadere con un'API immutabile.
Thomas Ahle

60

In Scala 2.8, è possibile utilizzare parametri denominati e predefiniti, nonché il copymetodo su una classe case. Ecco un po 'di codice di esempio:

case class Person(name: String, age: Int, children: List[Person] = List()) {
  def addChild(p: Person) = copy(children = p :: this.children)
}

val parent = Person(name = "Bob", age = 55)
  .addChild(Person("Lisa", 23))
  .addChild(Person("Peter", 16))

31
+1 per aver inventato il linguaggio Scala. Sì, è un abuso del sistema di reputazione ma ... aww ... amo così tanto Scala che ho dovuto farlo. :)
Malax

1
Oh, amico ... ho appena risposto a qualcosa di quasi identico! Bene, sono in buona compagnia. :-) Mi chiedo di non aver visto la tua risposta prima, però ... <shrug>
Daniel C. Sobral

20

Bene, considera questo su Scala 2.8:

case class Person(name: String, 
                  married: Boolean = false, 
                  espouse: Option[String] = None, 
                  children: Set[String] = Set.empty) {
  def marriedTo(whom: String) = this.copy(married = true, espouse = Some(whom))
  def addChild(whom: String) = this.copy(children = children + whom)
}

scala> Person("Joseph").marriedTo("Mary").addChild("Jesus")
res1: Person = Person(Joseph,true,Some(Mary),Set(Jesus))

Questo ovviamente ha la sua parte di problemi. Ad esempio, prova a creare espousee Option[Person], quindi a far sposare due persone. Non riesco a pensare a un modo per risolverlo senza ricorrere a un private vare / o un privatecostruttore più una fabbrica.


11

Ecco un paio di altre opzioni:

opzione 1

Rendi l'implementazione stessa mutevole, ma separa le interfacce che espone a mutabili e immutabili. Questo è tratto dal design della libreria Swing.

public interface Foo {
  X getX();
  Y getY();
}

public interface MutableFoo extends Foo {
  void setX(X x);
  void setY(Y y);
}

public class FooImpl implements MutableFoo {...}

public SomeClassThatUsesFoo {
  public Foo makeFoo(...) {
    MutableFoo ret = new MutableFoo...
    ret.setX(...);
    ret.setY(...);
    return ret; // As Foo, not MutableFoo
  }
}

opzione 2

Se la tua applicazione contiene un insieme ampio ma predefinito di oggetti immutabili (ad esempio oggetti di configurazione), potresti prendere in considerazione l'utilizzo del framework Spring .


3
L'opzione 1 è intelligente (ma non troppo intelligente), quindi mi piace.
Timoteo,

4
L'ho fatto prima, ma secondo me è ben lungi dall'essere una buona soluzione perché l'oggetto è ancora mutabile, solo i metodi mutanti sono "nascosti". Forse sono troppo esigente su questo argomento ...
Malax

una variante dell'opzione 1 consiste nell'avere classi separate per le varianti Immutable e Mutable. Ciò fornisce una maggiore sicurezza rispetto all'approccio di interfaccia. Probabilmente stai mentendo al consumatore dell'API ogni volta che gli dai un oggetto modificabile chiamato Immutable che deve semplicemente essere lanciato sulla sua interfaccia mutabile. L'approccio di classe separata richiede metodi per la conversione avanti e indietro. L'API JodaTime utilizza questo modello. Vedere DateTime e MutableDateTime.
toolbear

6

È utile ricordare che esistono diversi tipi di immutabilità . Per il tuo caso, penso che l'immutabilità "ghiacciolo" funzionerà molto bene:

Immutabilità del ghiacciolo: è ciò che chiamo capricciosamente un leggero indebolimento dell'immutabilità di una sola scrittura. Si potrebbe immaginare un oggetto o un campo che è rimasto mutabile per un po 'durante la sua inizializzazione e poi è stato “congelato” per sempre. Questo tipo di immutabilità è particolarmente utile per oggetti immutabili che fanno riferimento circolarmente l'un l'altro, o oggetti immutabili che sono stati serializzati su disco e dopo la deserializzazione devono essere "fluidi" fino a quando non viene completato l'intero processo di deserializzazione, a quel punto tutti gli oggetti possono essere congelato.

Quindi inizializzi il tuo oggetto, quindi imposti un flag "freeze" di qualche tipo che indica che non è più scrivibile. Preferibilmente, nasconderai la mutazione dietro una funzione in modo che la funzione sia ancora pura per i client che consumano la tua API.


1
Voti negativi? Qualcuno vuole lasciare un commento sul perché questa non è una buona soluzione?
Juliet

+1. Forse qualcuno ha svalutato questo perché stai suggerendo l'uso di clone()per derivare nuove istanze.
finnw

O forse era perché può compromettere la sicurezza dei thread in Java: java.sun.com/docs/books/jls/third_edition/html/memory.html#17.5
finnw

Uno svantaggio è che richiede agli sviluppatori futuri di stare attenti a gestire il flag "freeze". Se in seguito viene aggiunto un metodo mutatore che dimentica di affermare che il metodo è scongelato, potrebbero esserci problemi. Allo stesso modo, se viene scritto un nuovo costruttore che dovrebbe chiamare il freeze()metodo, ma non lo fa, le cose potrebbero diventare brutte.
Stalepretzel

5

Puoi anche fare in modo che gli oggetti immutabili espongano metodi che sembrano mutatori (come addSibling) ma lascia che restituiscano una nuova istanza. Questo è ciò che fanno le collezioni Scala immutabili.

Lo svantaggio è che potresti creare più istanze del necessario. È anche applicabile solo quando esistono configurazioni valide intermedie (come un nodo senza fratelli che è ok nella maggior parte dei casi) a meno che non si voglia trattare con oggetti parzialmente costruiti.

Ad esempio, un bordo del grafico che non ha ancora destinazione non è un bordo del grafico valido.


Il presunto svantaggio - creare più istanze del necessario - non è davvero un gran problema. L'allocazione degli oggetti è molto economica, così come la garbage collection di oggetti di breve durata. Quando l'analisi di escape è abilitata per impostazione predefinita, è probabile che questo tipo di "oggetti intermedi" venga allocato nello stack e non costi letteralmente nulla per la creazione.
gustafc

2
@gustafc: Sì. Cliff Click una volta ha raccontato la storia di come hanno confrontato la simulazione Clojure Ant Colony di Rich Hickey su una delle loro grandi scatole (864 core, 768 GB RAM): 700 thread paralleli in esecuzione su 700 core, ciascuno al 100%, generando oltre 20 GB di spazzatura effimera al secondo . Il GC non ha nemmeno sudato.
Jörg W Mittag

5

Considera quattro possibilità:

new Immutable(one, fish, two, fish, red, fish, blue, fish); /*1 */

params = new ImmutableParameters(); /*2 */
params.setType("fowl");
new Immutable(params);

factory = new ImmutableFactory(); /*3 */
factory.setType("fish");
factory.getInstance();

Immutable boringImmutable = new Immutable(); /* 4 */
Immutable lessBoring = boringImmutable.setType("vegetable");

Per me, ciascuno di 2, 3 e 4 è adattato a una situazione diversa. Il primo è difficile da amare, per i motivi citati dall'OP, ed è generalmente sintomo di un design che ha subito qualche creep e necessita di qualche refactoring.

Quello che sto elencando come (2) è buono quando non c'è uno stato dietro la "fabbrica", mentre (3) è il design di scelta quando c'è uno stato. Mi trovo a usare (2) invece di (3) quando non voglio preoccuparmi dei thread e della sincronizzazione, e non devo preoccuparmi di ammortizzare alcune costose impostazioni sulla produzione di molti oggetti. (3), d'altra parte, viene richiamato quando il lavoro reale va nella costruzione della fabbrica (impostazione da uno SPI, lettura dei file di configurazione, ecc.).

Infine, la risposta di qualcun altro ha menzionato l'opzione (4), dove hai molti piccoli oggetti immutabili e lo schema preferibile è quello di ottenere notizie da quelli vecchi.

Nota che non sono un membro del "fan club dei modelli" - certo, alcune cose meritano di essere emulate, ma mi sembra che assumano una vita inutile una volta che le persone danno loro nomi e cappelli divertenti.


6
Questo è il Builder Pattern (opzione 2)
Simon Nickerson

Non sarebbe un oggetto di fabbrica ("costruttore") che emette i suoi oggetti immutabili?
bmargulies

questa distinzione sembra piuttosto semantica. Come viene prodotto il fagiolo? In cosa differisce da un costruttore?
Carl

Non vuoi il pacchetto delle convenzioni Java Bean per un builder (o per molto altro).
Tom Hawtin - tackline

4

Un'altra possibile opzione è il refactoring per avere meno campi configurabili. Se gruppi di campi funzionano solo (principalmente) tra loro, raccogliili nel loro piccolo oggetto immutabile. I costruttori / builder di quell'oggetto "piccolo" dovrebbero essere più gestibili, così come il costruttore / builder per questo oggetto "grande".


1
nota: il chilometraggio per questa risposta può variare a seconda del problema, della base di codice e delle capacità dello sviluppatore.
Carl,

2

Uso C # e questi sono i miei approcci. Tener conto di:

class Foo
{
    // private fields only to be written inside a constructor
    private readonly int i;
    private readonly string s;
    private readonly Bar b;

    // public getter properties
    public int I { get { return i; } }
    // etc.
}

Opzione 1. Costruttore con parametri opzionali

public Foo(int i = 0, string s = "bla", Bar b = null)
{
    this.i = i;
    this.s = s;
    this.b = b;
}

Usato come ad es new Foo(5, b: new Bar(whatever)). Non per le versioni Java o C # precedenti alla 4.0. ma vale comunque la pena mostrare, in quanto è un esempio di come non tutte le soluzioni siano indipendenti dalla lingua.

Opzione 2. Costruttore che accetta un singolo oggetto parametro

public Foo(FooParameters parameters)
{
    this.i = parameters.I;
    // etc.
}

class FooParameters
{
    // public properties with automatically generated private backing fields
    public int I { get; set; }
    public string S { get; set; }
    public Bar B { get; set; }

    // All properties are public, so we don't need a full constructor.
    // For convenience, you could include some commonly used initialization
    // patterns as additional constructors.
    public FooParameters() { }
}

Esempio di utilizzo:

FooParameters fp = new FooParameters();
fp.I = 5;
fp.S = "bla";
fp.B = new Bar();
Foo f = new Foo(fp);`

C # da 3.0 in poi lo rende più elegante con la sintassi dell'inizializzatore di oggetti (semanticamente equivalente all'esempio precedente):

FooParameters fp = new FooParameters { I = 5, S = "bla", B = new Bar() };
Foo f = new Foo(fp);

Opzione 3:
ridisegna la tua classe in modo da non richiedere un numero così elevato di parametri. Potresti suddividere le sue responsabilità in più classi. Oppure passare i parametri non al costruttore ma solo a metodi specifici, su richiesta. Non sempre fattibile, ma quando lo è, vale la pena farlo.

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.