Erstellen großer, unveränderlicher Objekte ohne Verwendung von Konstruktoren mit langen Parameterlisten

96

Ich habe einige große Objekte (mehr als 3 Felder), die unveränderlich sein können und sollten. Jedes Mal, wenn ich auf diesen Fall stoße, neige ich dazu, Konstruktor-Abscheulichkeiten mit langen Parameterlisten zu erstellen.

Es fühlt sich nicht richtig an, ist schwer zu bedienen und die Lesbarkeit leidet.

Es ist noch schlimmer, wenn die Felder eine Art Sammlungstyp wie Listen sind. Ein einfaches addSibling(S s)würde die Objekterstellung so sehr erleichtern, macht das Objekt jedoch veränderlich.

Was benutzt ihr in solchen Fällen?

Ich bin auf Scala und Java, aber ich denke, das Problem ist sprachunabhängig, solange die Sprache objektorientiert ist.

Lösungen, die mir einfallen:

  1. "Konstruktor-Greuel mit langen Parameterlisten"
  2. Das Builder-Muster
Malax
quelle
5
Ich denke, das Builder-Muster ist die beste und Standardlösung dafür.
Zachary Wright
@Zachary: Was Sie befürworten, funktioniert nur für eine spezielle Form des Builder-Musters, wie hier von Joshua Bloch erläutert: drdobbs.com/java/208403883?pgno=2 Ich bevorzuge es, den zugehörigen Begriff "fließende Schnittstelle" aufzurufen diese "spezielle Form des Builder-Musters" (siehe meine Antwort).
SyntaxT3rr0r

Antworten:

76

Nun, Sie möchten sowohl ein leichter lesbares als auch ein unveränderliches Objekt, sobald es erstellt wurde?

Ich denke, eine fließende Schnittstelle, die richtig gemacht wurde, würde Ihnen helfen.

Es würde so aussehen (rein erfundenes Beispiel):

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

Ich habe "RICHTIG GEMACHT" in Fettdruck geschrieben, weil die meisten Java-Programmierer fließende Schnittstellen falsch verstehen und ihr Objekt mit der zum Erstellen des Objekts erforderlichen Methode verschmutzen, was natürlich völlig falsch ist.

Der Trick ist, dass nur die build () -Methode tatsächlich ein Foo erstellt (daher kann Ihr Foo unveränderlich sein).

FooFactory.create () , wobeiXXX (..) und withXXX (..) alle "etwas anderes" erstellen.

Dass etwas anderes eine FooFactory sein kann, hier ist eine Möglichkeit, es zu tun ...

Ihre FooFactory würde so aussehen:

// 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 );
}
SyntaxT3rr0r
quelle
11
@ all: Bitte keine Beschwerde über das Postfix "Impl" in "FooImpl": Diese Klasse ist in einer Fabrik versteckt und niemand wird sie jemals sehen, außer der Person, die die fließende Oberfläche schreibt. Der Benutzer kümmert sich nur darum, dass er ein "Foo" bekommt. Ich hätte auch "FooImpl" "FooPointlessNitpick" nennen können;)
SyntaxT3rr0r
5
Fühlen Sie sich präventiv? ;) Du wurdest in der Vergangenheit darüber nicht ausgewählt. :)
Greg D
3
Ich glaube , dass der gemeinsame Fehler er mit Bezug ist , dass die Menschen die „withXXX“ (usw.) Methoden hinzufügen , um das FooObjekt, anstatt eine separate aufweist FooFactory.
Dean Harding
3
Sie haben noch den FooImplKonstruktor mit 8 Parametern. Was ist die Verbesserung?
Mot
4
Ich würde diesen Code nicht aufrufen immutable, und ich hätte Angst davor, dass Leute Fabrikobjekte wiederverwenden, weil sie denken, dass dies der Fall ist. Ich meine: FooFactory people = FooFactory.create().withType("person"); Foo women = people.withGender("female").build(); Foo talls = people.tallerThan("180m").build();wo tallswürde jetzt nur noch Frauen sein. Dies sollte mit einer unveränderlichen API nicht passieren.
Thomas Ahle
60

In Scala 2.8 können Sie benannte und Standardparameter sowie die copyMethode für eine Fallklasse verwenden. Hier ist ein Beispielcode:

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))
Martin Odersky
quelle
31
+1 für die Erfindung der Scala-Sprache. Ja, es ist ein Missbrauch des Reputationssystems, aber ... aww ... ich liebe Scala so sehr, dass ich es tun musste. :)
Malax
1
Oh Mann ... ich habe gerade etwas fast Identisches beantwortet! Nun, ich bin in guter Gesellschaft. :-) Ich frage mich, dass ich Ihre Antwort vorher nicht gesehen habe ... <Achselzucken>
Daniel C. Sobral
20

Betrachten Sie dies in 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))

Dies hat natürlich einige Probleme. Versuchen Sie zum Beispiel, espouseund Option[Person]und dann zwei Personen miteinander zu heiraten. Ich kann mir keinen Weg vorstellen, das zu lösen, ohne auf einen private varund / oder einen privateKonstrukteur und eine Fabrik zurückzugreifen.

Daniel C. Sobral
quelle
11

Hier sind einige weitere Optionen:

Option 1

Machen Sie die Implementierung selbst veränderbar, trennen Sie jedoch die Schnittstellen, die sie veränderbar und unveränderlich macht. Dies ist dem Design der Swing-Bibliothek entnommen.

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
  }
}

Option 2

Wenn Ihre Anwendung eine große, aber vordefinierte Menge unveränderlicher Objekte enthält (z. B. Konfigurationsobjekte), können Sie das Spring- Framework verwenden.

Kleine Bobby Tische
quelle
3
Option 1 ist klug (aber nicht zu klug), also mag ich es.
Timothy
4
Ich habe dies früher getan, aber meiner Meinung nach ist es weit davon entfernt, eine gute Lösung zu sein, da das Objekt immer noch veränderlich ist und nur die Mutationsmethoden "versteckt" sind. Vielleicht bin ich zu wählerisch in
Bezug auf
Eine Variante von Option 1 besteht darin, separate Klassen für unveränderliche und veränderbare Varianten zu haben. Dies bietet eine bessere Sicherheit als der Schnittstellenansatz. Wahrscheinlich belügen Sie den Konsumenten der API, wenn Sie ihm ein veränderliches Objekt mit dem Namen "Unveränderlich" geben, das einfach in die veränderbare Schnittstelle umgewandelt werden muss. Der separate Klassenansatz erfordert Methoden zum Hin- und Herkonvertieren. Die JodaTime-API verwendet dieses Muster. Siehe DateTime und MutableDateTime.
Toolbear
6

Es hilft, sich daran zu erinnern, dass es verschiedene Arten von Unveränderlichkeit gibt . Für Ihren Fall denke ich, dass die Unveränderlichkeit von "Eis am Stiel" wirklich gut funktionieren wird:

Eis am Stiel Unveränderlichkeit: ist das, was ich skurril eine leichte Schwächung der Unveränderlichkeit beim einmaligen Schreiben nenne. Man könnte sich ein Objekt oder ein Feld vorstellen, das während seiner Initialisierung für eine Weile veränderlich blieb und dann für immer „eingefroren“ wurde. Diese Art der Unveränderlichkeit ist besonders nützlich für unveränderliche Objekte, die sich kreisförmig aufeinander beziehen, oder unveränderliche Objekte, die auf die Platte serialisiert wurden und bei der Deserialisierung "flüssig" sein müssen, bis der gesamte Deserialisierungsprozess abgeschlossen ist. An diesem Punkt können sich alle Objekte befinden gefroren.

Sie initialisieren also Ihr Objekt und setzen dann ein "Freeze" -Flag, das angibt, dass es nicht mehr beschreibbar ist. Am besten verstecken Sie die Mutation hinter einer Funktion, damit die Funktion für Clients, die Ihre API verwenden, immer noch rein ist.

Julia
quelle
1
Downvotes? Möchte jemand einen Kommentar hinterlassen, warum dies keine gute Lösung ist?
Julia
+1. Vielleicht hat jemand dies abgelehnt, weil Sie auf die Verwendung von hinweisen clone(), um neue Instanzen abzuleiten.
Finnw
Oder vielleicht lag es daran, dass es die Thread-Sicherheit in Java gefährden kann: java.sun.com/docs/books/jls/third_edition/html/memory.html#17.5
finnw
Ein Nachteil ist, dass zukünftige Entwickler vorsichtig mit dem "Freeze" -Flag umgehen müssen. Wenn später eine Mutator-Methode hinzugefügt wird, die vergisst, dass die Methode nicht eingefroren ist, können Probleme auftreten. Wenn ein neuer Konstruktor geschrieben wird, der die freeze()Methode aufrufen soll, aber nicht, können die Dinge ebenfalls hässlich werden.
Stalepretzel
5

Sie können die unveränderlichen Objekte auch Methoden verfügbar machen, die wie Mutatoren aussehen (wie addSibling), sie jedoch eine neue Instanz zurückgeben lassen. Das machen die unveränderlichen Scala-Sammlungen.

Der Nachteil ist, dass Sie möglicherweise mehr Instanzen als erforderlich erstellen. Dies gilt auch nur, wenn zwischenzeitlich gültige Konfigurationen vorhanden sind (wie bei einigen Knoten ohne Geschwister, was in den meisten Fällen in Ordnung ist), es sei denn, Sie möchten sich nicht mit teilweise erstellten Objekten befassen.

Beispielsweise ist eine Diagrammkante, die noch kein Ziel hat, keine gültige Diagrammkante.

Zikzystar
quelle
Der angebliche Nachteil - mehr Instanzen als nötig zu erstellen - ist nicht wirklich ein großes Problem. Die Objektzuweisung ist sehr billig, ebenso wie die Speicherbereinigung kurzlebiger Objekte. Wenn die Escape-Analyse standardmäßig aktiviert ist, wird diese Art von "Zwischenobjekten" wahrscheinlich stapelweise zugewiesen und die Erstellung kostet buchstäblich nichts.
Gustafc
2
@gustafc: Ja. Cliff Click erzählte einmal die Geschichte, wie sie die Clojure Ant Colony-Simulation von Rich Hickey auf einer ihrer großen Boxen (864 Kerne, 768 GB RAM) verglichen haben: 700 parallele Threads, die auf 700 Kernen mit jeweils 100% ausgeführt werden und über 20 GB erzeugen vergänglicher Müll pro Sekunde . Der GC hat nicht einmal ins Schwitzen gekommen.
Jörg W Mittag
5

Betrachten Sie vier Möglichkeiten:

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");

Für mich ist jeder von 2, 3 und 4 an eine unterschiedliche Situation angepasst. Der erste ist aus den vom OP angeführten Gründen schwer zu lieben und ist im Allgemeinen ein Symptom für ein Design, das ein gewisses Kriechen erlitten hat und etwas umgestaltet werden muss.

Was ich als (2) aufführe, ist gut, wenn es keinen Zustand hinter der 'Fabrik' gibt, während (3) das Design der Wahl ist, wenn es einen Zustand gibt. Ich verwende (2) anstelle von (3), wenn ich mich nicht um Threads und Synchronisation kümmern möchte, und ich muss mich nicht darum kümmern, ein teures Setup über die Produktion vieler Objekte zu amortisieren. (3) wird dagegen aufgerufen, wenn echte Arbeit in den Bau der Fabrik fließt (Einrichten von einem SPI, Lesen von Konfigurationsdateien usw.).

Schließlich erwähnte die Antwort eines anderen die Option (4), bei der Sie viele kleine unveränderliche Objekte haben und das bevorzugte Muster darin besteht, Nachrichten von alten zu erhalten.

Beachten Sie, dass ich kein Mitglied des 'Pattern Fan Clubs' bin - sicher, einige Dinge sind es wert, nachgeahmt zu werden, aber es scheint mir, dass sie ein nicht hilfreiches Eigenleben führen, wenn die Leute ihnen Namen und lustige Hüte geben.

bmargulies
quelle
6
Dies ist das Builder-Muster (Option 2)
Simon Nickerson
Wäre das nicht ein Fabrikobjekt ('Builder'), das seine unveränderlichen Objekte emittiert?
Bmargulies
Diese Unterscheidung scheint ziemlich semantisch. Wie wird die Bohne hergestellt? Wie unterscheidet sich das von einem Builder?
Carl
Sie möchten das Paket der Java Bean-Konventionen nicht für einen Builder (oder für vieles andere).
Tom Hawtin - Tackline
4

Eine weitere mögliche Option ist die Umgestaltung, um weniger konfigurierbare Felder zu haben. Wenn Gruppen von Feldern (meistens) nur miteinander arbeiten, sammeln Sie sie zu einem eigenen kleinen unveränderlichen Objekt. Die Konstruktoren / Builder dieses "kleinen" Objekts sollten besser zu verwalten sein, ebenso wie der Konstruktor / Builder für dieses "große" Objekt.

Carl
quelle
1
Hinweis: Der Kilometerstand für diese Antwort kann je nach Problem, Codebasis und Entwicklerfähigkeiten variieren.
Carl
2

Ich benutze C # und das sind meine Ansätze. Erwägen:

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.
}

Option 1. Konstruktor mit optionalen Parametern

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

Verwendet als z new Foo(5, b: new Bar(whatever)). Nicht für Java- oder C # -Versionen vor 4.0. Es lohnt sich jedoch zu zeigen, dass nicht alle Lösungen sprachunabhängig sind.

Option 2. Konstruktor, der ein einzelnes Parameterobjekt verwendet

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() { }
}

Anwendungsbeispiel:

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

C # ab 3.0 macht dies mit der Objektinitialisierersyntax eleganter (semantisch äquivalent zum vorherigen Beispiel):

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

Option 3:
Gestalten Sie Ihre Klasse neu, um nicht so viele Parameter zu benötigen. Sie können die Verantwortlichkeiten in mehrere Klassen aufteilen. Oder übergeben Sie Parameter bei Bedarf nicht an den Konstruktor, sondern nur an bestimmte Methoden. Nicht immer lebensfähig, aber wenn es so ist, lohnt es sich.

Joren
quelle