Java: Wie implementiere ich einen Step Builder, für den die Reihenfolge der Setter keine Rolle spielt?

10

Bearbeiten: Ich möchte darauf hinweisen, dass diese Frage ein theoretisches Problem beschreibt, und mir ist bewusst, dass ich Konstruktorargumente für obligatorische Parameter verwenden oder eine Laufzeitausnahme auslösen kann, wenn die API falsch verwendet wird. Ich suche jedoch nach einer Lösung, die keine Konstruktorargumente oder Laufzeitprüfungen erfordert.

Stellen Sie sich vor, Sie haben eine CarSchnittstelle wie diese:

public interface Car {
    public Engine getEngine(); // required
    public Transmission getTransmission(); // required
    public Stereo getStereo(); // optional
}

Wie aus den Kommentaren hervorgeht, ist ein CarMuss ein Engineund Transmissionaber ein Stereoist optional. Das bedeutet, dass ein Builder, der build()eine CarInstanz kann, immer nur eine build()Methode haben sollte, wenn der Builder-Instanz bereits ein Engineund Transmissionbeide zugewiesen wurden. Auf diese Weise weigert sich die Typprüfung, Code zu kompilieren, der versucht, eine CarInstanz ohne Engineoder zu erstellen Transmission.

Dies erfordert einen Step Builder . Normalerweise implementieren Sie Folgendes:

public interface Car {
    public Engine getEngine(); // required
    public Transmission getTransmission(); // required
    public Stereo getStereo(); // optional

    public class Builder {
        public BuilderWithEngine engine(Engine engine) {
            return new BuilderWithEngine(engine);
        }
    }

    public class BuilderWithEngine {
        private Engine engine;
        private BuilderWithEngine(Engine engine) {
            this.engine = engine;
        }
        public BuilderWithEngine engine(Engine engine) {
            this.engine = engine;
            return this;
        }
        public CompleteBuilder transmission(Transmission transmission) {
            return new CompleteBuilder(engine, transmission);
        }
    }

    public class CompleteBuilder {
        private Engine engine;
        private Transmission transmission;
        private Stereo stereo = null;
        private CompleteBuilder(Engine engine, Transmission transmission) {
            this.engine = engine;
            this.transmission = transmission;
        }
        public CompleteBuilder engine(Engine engine) {
            this.engine = engine;
            return this;
        }
        public CompleteBuilder transmission(Transmission transmission) {
            this.transmission = transmission;
            return this;
        }
        public CompleteBuilder stereo(Stereo stereo) {
            this.stereo = stereo;
            return this;
        }
        public Car build() {
            return new Car() {
                @Override
                public Engine getEngine() {
                    return engine;
                }
                @Override
                public Transmission getTransmission() {
                    return transmission;
                }
                @Override
                public Stereo getStereo() {
                    return stereo;
                }
            };
        }
    }
}

Es gibt eine Kette von verschiedenen Builder - Klassen ( Builder, BuilderWithEngine, CompleteBuilder), das Hinzufügen einer erforderlichen Setter - Methode nach der anderen, mit der letzten Klasse alle optionalen Setter - Methoden als auch enthalten.
Dies bedeutet, dass Benutzer dieses Step Builders auf die Reihenfolge beschränkt sind , in der der Autor obligatorische Setter zur Verfügung gestellt hat . Hier ist ein Beispiel für mögliche Verwendungen (beachten Sie, dass alle streng geordnet sind: engine(e)zuerst, gefolgt von transmission(t)und schließlich optional stereo(s)).

new Builder().engine(e).transmission(t).build();
new Builder().engine(e).transmission(t).stereo(s).build();
new Builder().engine(e).engine(e).transmission(t).stereo(s).build();
new Builder().engine(e).transmission(t).engine(e).stereo(s).build();
new Builder().engine(e).transmission(t).stereo(s).engine(e).build();
new Builder().engine(e).transmission(t).transmission(t).stereo(s).build();
new Builder().engine(e).transmission(t).stereo(s).transmission(t).build();
new Builder().engine(e).transmission(t).stereo(s).stereo(s).build();

Es gibt jedoch viele Szenarien, in denen dies für den Benutzer des Builders nicht ideal ist, insbesondere wenn der Builder nicht nur Setter, sondern auch Addierer hat oder wenn der Benutzer die Reihenfolge nicht steuern kann, in der bestimmte Eigenschaften für den Builder verfügbar werden.

Die einzige Lösung, die ich mir vorstellen kann , ist sehr kompliziert: Für jede Kombination von obligatorischen Eigenschaften, die festgelegt wurden oder noch nicht festgelegt wurden, habe ich eine dedizierte Builder-Klasse erstellt, die weiß, welche potenziellen anderen obligatorischen Setter aufgerufen werden müssen, bevor sie zu a gelangen Geben Sie an, wo die build()Methode verfügbar sein soll, und jeder dieser Setter gibt einen vollständigeren Builder-Typ zurück, der dem Enthalten einer build()Methode einen Schritt näher kommt .
Ich habe den folgenden Code hinzugefügt, aber Sie könnten sagen, dass ich das Typsystem verwende, um einen FSM zu erstellen, mit dem Sie einen erstellen Builderkönnen, der entweder in einen BuilderWithEngineoder BuilderWithTransmissionin einen verwandelt werden kann, und beide können dann in einen umgewandelt werden CompleteBuilder, der den implementiertbuild()Methode. Optionale Setter können für jede dieser Builder-Instanzen aufgerufen werden. Geben Sie hier die Bildbeschreibung ein

public interface Car {
    public Engine getEngine(); // required
    public Transmission getTransmission(); // required
    public Stereo getStereo(); // optional

    public class Builder extends OptionalBuilder {
        public BuilderWithEngine engine(Engine engine) {
            return new BuilderWithEngine(engine, stereo);
        }
        public BuilderWithTransmission transmission(Transmission transmission) {
            return new BuilderWithTransmission(transmission, stereo);
        }
        @Override
        public Builder stereo(Stereo stereo) {
            super.stereo(stereo);
            return this;
        }
    }

    public class OptionalBuilder {
        protected Stereo stereo = null;
        private OptionalBuilder() {}
        public OptionalBuilder stereo(Stereo stereo) {
            this.stereo = stereo;
            return this;
        }
    }

    public class BuilderWithEngine extends OptionalBuilder {
        private Engine engine;
        private BuilderWithEngine(Engine engine, Stereo stereo) {
            this.engine = engine;
            this.stereo = stereo;
        }
        public CompleteBuilder transmission(Transmission transmission) {
            return new CompleteBuilder(engine, transmission, stereo);
        }
        public BuilderWithEngine engine(Engine engine) {
            this.engine = engine;
            return this;
        }
        @Override
        public BuilderWithEngine stereo(Stereo stereo) {
            super.stereo(stereo);
            return this;
        }
    }

    public class BuilderWithTransmission extends OptionalBuilder {
        private Transmission transmission;
        private BuilderWithTransmission(Transmission transmission, Stereo stereo) {
            this.transmission = transmission;
            this.stereo = stereo;
        }
        public CompleteBuilder engine(Engine engine) {
            return new CompleteBuilder(engine, transmission, stereo);
        }
        public BuilderWithTransmission transmission(Transmission transmission) {
            this.transmission = transmission;
            return this;
        }
        @Override
        public BuilderWithTransmission stereo(Stereo stereo) {
            super.stereo(stereo);
            return this;
        }
    }

    public class CompleteBuilder extends OptionalBuilder {
        private Engine engine;
        private Transmission transmission;
        private CompleteBuilder(Engine engine, Transmission transmission, Stereo stereo) {
            this.engine = engine;
            this.transmission = transmission;
            this.stereo = stereo;
        }
        public CompleteBuilder engine(Engine engine) {
            this.engine = engine;
            return this;
        }
        public CompleteBuilder transmission(Transmission transmission) {
            this.transmission = transmission;
            return this;
        }
        @Override
        public CompleteBuilder stereo(Stereo stereo) {
            super.stereo(stereo);
            return this;
        }
        public Car build() {
            return new Car() {
                @Override
                public Engine getEngine() {
                    return engine;
                }
                @Override
                public Transmission getTransmission() {
                    return transmission;
                }
                @Override
                public Stereo getStereo() {
                    return stereo;
                }
            };
        }
    }
}

Wie Sie sehen können, lässt sich dies nicht gut skalieren, da die Anzahl der verschiedenen erforderlichen Builder-Klassen O (2 ^ n) wäre, wobei n die Anzahl der obligatorischen Setter ist.

Daher meine Frage: Kann das eleganter gemacht werden?

(Ich suche nach einer Antwort, die mit Java funktioniert, obwohl Scala auch akzeptabel wäre)

derabbink
quelle
1
Was hindert Sie daran, nur einen IoC-Container zu verwenden, um all diese Abhängigkeiten aufrechtzuerhalten? Ich bin mir auch nicht sicher, warum Sie, wenn die Reihenfolge keine Rolle spielt, wie Sie behauptet haben, nicht einfach gewöhnliche Setter-Methoden verwenden können, die zurückkehren this?
Robert Harvey
Was bedeutet es, .engine(e)zweimal für einen Builder aufzurufen ?
Erik Eidt
3
Wenn Sie es statisch überprüfen möchten, ohne manuell eine Klasse für jede Kombination zu schreiben, müssen Sie wahrscheinlich Dinge auf Nackenbart-Ebene wie Makros oder Metaprogrammierung von Vorlagen verwenden. Java ist dafür meines Wissens nicht ausdrucksstark genug, und der Aufwand würde sich bei dynamisch verifizierten Lösungen in anderen Sprachen wahrscheinlich nicht lohnen.
Karl Bielefeldt
1
Robert: Das Ziel ist, dass der Typprüfer die Tatsache erzwingt, dass sowohl ein Motor als auch ein Getriebe obligatorisch sind. Auf diese Weise können nicht einmal anrufen können , build()wenn Sie nicht genannt haben engine(e)und transmission(t)vor.
Derabbink
Erik: Vielleicht möchten Sie mit einer Standardimplementierung beginnen Engineund diese später mit einer spezifischeren Implementierung überschreiben. Aber höchstwahrscheinlich wäre dies sinnvoller, wenn engine(e)nicht ein Setter, sondern ein Addierer wäre : addEngine(e). Dies wäre nützlich für einen CarHersteller, der Hybridautos mit mehr als einem Motor herstellen kann. Da dies ein erfundenes Beispiel ist, habe ich nicht näher darauf eingegangen, warum Sie dies tun möchten - der Kürze halber.
Derabbink

Antworten:

3

Sie scheinen zwei unterschiedliche Anforderungen zu haben, basierend auf den von Ihnen angegebenen Methodenaufrufen.

  1. Nur ein (erforderlicher) Motor, nur ein (erforderliches) Getriebe und nur eine (optionale) Stereoanlage.
  2. Ein oder mehrere (erforderliche) Motoren, ein oder mehrere (erforderliche) Getriebe und eine oder mehrere (optionale) Stereoanlagen.

Ich denke , die erste Frage ist hier , dass Sie nicht wissen , was Sie wollen die Klasse zu tun. Ein Teil davon ist, dass nicht bekannt ist, wie das erstellte Objekt aussehen soll.

Ein Auto kann nur einen Motor und ein Getriebe haben. Sogar Hybridautos haben nur einen Motor (vielleicht einen GasAndElectricEngine)

Ich werde beide Implementierungen ansprechen:

public class CarBuilder {

    public CarBuilder(Engine engine, Transmission transmission) {
        // ...
    }

    public CarBuilder setStereo(Stereo stereo) {
        // ...
        return this;
    }
}

und

public class CarBuilder {

    public CarBuilder(List<Engine> engines, List<Transmission> transmission) {
        // ...
    }

    public CarBuilder addStereo(Stereo stereo) {
        // ...
        return this;
    }
}

Wenn ein Motor und ein Getriebe benötigt werden, sollten sie sich im Konstruktor befinden.

Wenn Sie nicht wissen, welcher Motor oder welches Getriebe benötigt wird, stellen Sie noch keinen ein. Dies ist ein Zeichen dafür, dass Sie den Builder zu weit oben im Stapel erstellen.

Zymus
quelle
2

Warum nicht das Nullobjektmuster verwenden? Befreien Sie sich von diesem Builder. Der eleganteste Code, den Sie schreiben können, ist der, den Sie eigentlich nicht schreiben müssen.

public final class CarImpl implements Car {
    private final Engine engine;
    private final Transmission transmission;
    private final Stereo stereo;

    public CarImpl(Engine engine, Transmission transmission) {
        this(engine, transmission, new DefaultStereo());
    }

    public CarImpl(Engine engine, Transmission transmission, Stereo stereo) {
        this.engine = engine;
        this.transmission = transmission;
        this.stereo = stereo;
    }

    //...

}
Gepunktet
quelle
Das habe ich mir sofort gedacht. Obwohl ich den Drei-Parameter-Konstruktor nicht hätte, nur den Zwei-Parameter-Konstruktor, der die obligatorischen Elemente enthält, und dann einen Setter für die Stereoanlage, da dieser optional ist.
Encaitar
1
In einem einfachen (erfundenen) Beispiel wie Carwäre dies sinnvoll, da die Anzahl der c'tor-Argumente sehr gering ist. Sobald Sie sich jedoch mit etwas mäßig Komplexem befassen (> = 4 obligatorische Argumente), wird das Ganze schwieriger zu handhaben / weniger lesbar ("Hat der Motor oder das Getriebe an erster Stelle gestanden?"). Aus diesem Grund würden Sie einen Builder verwenden: Die API zwingt Sie dazu, genauer zu bestimmen, was Sie erstellen.
Derabbink
1
@derabbink Warum brechen Sie in diesem Fall nicht Ihre Klasse in kleinere? Die Verwendung eines Builders verbirgt nur die Tatsache, dass die Klasse zu viel tut und nicht mehr zu warten ist.
Spotted
1
Ein großes Lob für die Beendigung des Musterwahnsinns.
Robert Harvey
@Spotted Einige Klassen enthalten nur viele Daten. Wenn Sie beispielsweise eine Zugriffsprotokollklasse erstellen möchten, die alle zugehörigen Informationen zur HTTP-Anforderung enthält, und die Daten im CSV- oder JSON-Format ausgeben möchten. Es werden viele Daten vorhanden sein. Wenn Sie festlegen möchten, dass einige Felder während der Kompilierungszeit vorhanden sind, benötigen Sie ein Builder-Muster mit einem sehr langen Konstruktor für Arg-Listen, der nicht gut aussieht.
SSGAO
1

Erstens, wenn Sie nicht viel mehr Zeit haben als in einem Geschäft, in dem ich gearbeitet habe, lohnt es sich wahrscheinlich nicht, eine Reihenfolge der Operationen zuzulassen oder nur damit zu leben, dass Sie mehr als ein Radio angeben können. Beachten Sie, dass es sich um Code handelt, nicht um Benutzereingaben. Daher können Sie Zusicherungen haben, die während des Komponententests und nicht eine Sekunde zuvor beim Kompilieren fehlschlagen.

Wenn Ihre Einschränkung jedoch darin besteht, dass Sie, wie in den Kommentaren angegeben, einen Motor und ein Getriebe benötigen, erzwingen Sie dies, indem Sie alle obligatorischen Eigenschaften als Konstruktor des Erstellers angeben.

new Builder(e, t).build();                      // ok
new Builder(e, t).stereo(s).build();            // ok
new Builder(e, t).stereo(s).stereo(s).build();  // exception on second call to stereo as stereo is already set 

Wenn nur die Stereoanlage optional ist, ist der letzte Schritt mit Unterklassen von Buildern möglich, aber darüber hinaus ist der Gewinn durch das Erhalten des Fehlers zur Kompilierungszeit und nicht beim Testen wahrscheinlich nicht die Mühe wert.

Pete Kirkham
quelle
0

Die Anzahl der verschiedenen erforderlichen Builder-Klassen wäre O (2 ^ n), wobei n die Anzahl der obligatorischen Setter ist.

Sie haben bereits die richtige Richtung für diese Frage erraten.

Wenn Sie eine Überprüfung zur Kompilierungszeit erhalten möchten, benötigen Sie (2^n)Typen. Wenn Sie eine Laufzeitprüfung erhalten möchten, benötigen Sie eine Variable, in der Status gespeichert (2^n)werden können. Eine n-bit-Ganzzahl reicht aus.


Da C ++ unterstützt nicht-Typ Template - Parameter (zB ganzzahlige Werte) ist es möglich , für eine C ++ Klasse Vorlage in instanziiert werden O(2^n)verschiedene Arten, ein Schema ähnlich dem unter Verwendung dieser .

In Sprachen, die keine Nicht-Typ-Vorlagenparameter unterstützen, können Sie sich jedoch nicht auf das Typsystem verlassen, um O(2^n)verschiedene Typen zu instanziieren .


Die nächste Möglichkeit sind Java-Annotationen (und C # -Attribute). Diese zusätzlichen Metadaten können verwendet werden, um beim Kompilieren ein benutzerdefiniertes Verhalten auszulösen, wenn Anmerkungsprozessoren verwendet werden. Es wäre jedoch zu viel Arbeit für Sie, diese zu implementieren. Wenn Sie Frameworks verwenden, die diese Funktionalität für Sie bereitstellen, verwenden Sie sie. Überprüfen Sie andernfalls die nächste Gelegenheit.


Beachten Sie schließlich, dass das Speichern O(2^n)verschiedener Zustände als Variable zur Laufzeit (wörtlich als Ganzzahl mit mindestens einer nBreite von Bits) sehr einfach ist. Aus diesem Grund empfehlen die am besten bewerteten Antworten, diese Prüfung zur Laufzeit durchzuführen, da der Aufwand für die Implementierung der Prüfung zur Kompilierungszeit im Vergleich zum potenziellen Gewinn zu hoch ist.

rwong
quelle