Wie soll ich mit inkompatiblen Konfigurationen mit dem Builder-Muster umgehen?

9

Dies wird durch diese Antwort auf eine separate Frage motiviert .

Das Builder-Muster wird verwendet, um die komplexe Initialisierung zu vereinfachen, insbesondere mit optionalen Initialisierungsparametern. Ich weiß jedoch nicht, wie ich mich gegenseitig ausschließende Konfigurationen richtig verwalten soll.

Hier ist eine ImageKlasse. Imagekann aus einer Datei oder aus einer Größe initialisiert werden, aber nicht aus beiden . Die Verwendung von Konstruktoren zur Durchsetzung dieses gegenseitigen Ausschlusses ist offensichtlich, wenn die Klasse einfach genug ist:

public class Image
{
    public Image(Size size, Thing stuff, int range)
    {
    // ... initialize empty with size
    }

    public Image(string filename, Thing stuff, int range)
    {
        // ... initialize from file
    }
}

Angenommen, es Imageist tatsächlich so konfigurierbar, dass das Builder-Muster nützlich ist. Dies könnte plötzlich möglich sein:

Image image = new ImageBuilder()
                  .setStuff(stuff)
                  .setRange(range)
                  .setSize(size)           // <----------  NOT
                  .setFilename(filename)   // <----------  COMPATIBLE
                  .build();

Diese Probleme müssen zur Laufzeit und nicht zur Kompilierungszeit behoben werden, was nicht das Schlimmste ist. Das Problem ist, dass das konsequente und umfassende Erkennen dieser Probleme innerhalb der ImageBuilderKlasse komplex werden kann, insbesondere im Hinblick auf die Wartung.

Wie soll ich mit inkompatiblen Konfigurationen im Builder-Muster umgehen?

kdbanman
quelle

Antworten:

12

Du hast deinen Builder. Zu diesem Zeitpunkt benötigen Sie jedoch einige Schnittstellen.

Es gibt eine FileBuilder-Schnittstelle, die eine Teilmenge von Methoden definiert (nicht setSize), und eine SizeBuilder-Schnittstelle, die eine andere Teilmenge von Methoden definiert (nicht setFilename). Möglicherweise möchten Sie, dass eine GenericBuilder-Oberfläche den FileBuilder und den SizeBuilder erweitert. Dies ist nicht erforderlich, obwohl einige Benutzer diesen Ansatz bevorzugen.

Die Methode setSize()gibt einen SizeBuilder zurück. Die Methode setFilename()gibt einen FileBuilder zurück.

Der ImageBuilder verfügt über die gesamte Logik für setSize()und setFileName(). Der Rückgabetyp für diese würde jedoch die entsprechende Teilmengenschnittstelle angeben.

class ImageBulder implements FileBuilder, SizeBuilder {
    ImageBuilder() {
        doInitThings;
    }

    ImageBuilder setStuff(Thing) {
        doStuff;
        return this;
    }

    ImageBuilder setRange(int range) {
        rangeStuff;
        return this;
    }

    SizeBuilder setSize(Size size) {
        stuff;
        return this;
    }

    FileBuilder setFilename(String filename) {
        otherStuff;
        return this;
    }

    Image build() {
        return new Image(...);
    }
}

Ein besonderes Merkmal ist, dass, sobald Sie einen SizeBuilder haben, alle Rückgaben SizeBuilder sein müssen. Die Schnittstelle dafür sieht aus wie:

interface SizeBuilder {
    SizeBuilder setRange(int range);
    SizeBuilder setSize(Size size);
    SizeBuilder setStuff(Thing stuff);
    Image build();
}

interface FileBuilder {
    FileBuilder setRange(int range);
    FileBuilder setFilename(String filename);
    FileBuilder setStuff(Thing stuff);
    Image build();
}

Sobald Sie eine dieser Methoden aufrufen, können Sie die andere nicht mehr aufrufen und ein Objekt mit einem ungültigen Status erstellen.


quelle
Wirklich interessant, danke. Ich bin ein bisschen verwirrt darüber, wie diese verwendet werden würden. Insbesondere kann ich nicht herausfinden, wie die Deklarations- und Initialisierungstypen aussehen würden. Ich stelle mir wahrscheinlich nur Dinge vor, die viel komplizierter als nötig sind. Könnten Sie ein Anwendungsbeispiel hinzufügen?
Kdbanman
Der Image Builder gibt die Schnittstelle zurück, die der Statusänderung entspricht, die diese Methode aufruft. Sobald Sie jedoch eine bestimmte Schnittstelle von ImageBuilder zurückerhalten, werden zukünftige Aufrufe für dieses Objekt auf dieser Schnittstelle ausgeführt, wodurch die Möglichkeit eingeschränkt wird, inkompatible Methoden aufzurufen.
1
@rwong Ich gebe zwar zu, dass ich mich nicht zu sehr damit befasst habe, aber das Problem, das ich bei einem solchen Ansatz zu haben glaubte, war, dass der 'Status' des Builders zurückgesetzt werden konnte. Man müsste sicherstellen, dass nach dem Aufruf von setSize () alle weiteren Builder-Aufrufe in SizeBuilder waren. Wenn der Typ von setRange () nicht der SizeBuilder oder etwas ist, das erweitert / implementiert , könnte man umgehen, um setFilename erneut darauf aufzurufen. Sie haben auch die Situation (hier nicht beschrieben), in der Sie anstelle der Größe int width und int height haben, sodass beide aufgerufen werden müssen.
1
@MichaelT Angesichts der komplizierten Umgehungsprobleme vermute ich, dass die Durchsetzung einer strengen Reihenfolge der Parameterinitialisierung (die zu einem Präfixbaum von Parameterelementen führt) bei Verwendung des Builder-Musters eine gute Sache sein kann. Infolgedessen müssen allgemeine Parameterelemente wie Rangeund Stuffzuerst initialisiert werden, nicht zu beliebigen Zeiten.
Rwong
1
@ MichaelT: An diesem Punkt kommt LSP ins Spiel. Sie können sicher sein, dass die Methoden des scheinbaren Typs ( RangeAndStuffBuilder) für den tatsächlichen Typ aufgerufen werden können. Weitere Einschränkungen können implementiert werden, indem für einige Methoden mehr Basaltypen zurückgegeben werden (obwohl dies zu einer exponentiellen Zunahme der Typen führt), wodurch Operationen effektiv entfernt werden. Solange die Methodenergebnisse nicht in der Hierarchie zurückgehen, werden keine Tippfehler angezeigt. Das setHeight/ setWidth-Szenario könnte mit einer Geschwisterhierarchie implementiert werden, die keine buildMethode hat.
Outis