Java: come implementare uno step builder per il quale l'ordine dei setter non ha importanza?


10

Modifica: vorrei sottolineare che questa domanda descrive un problema teorico e sono consapevole di poter utilizzare argomenti di costruzione per parametri obbligatori o generare un'eccezione di runtime se l'API viene utilizzata in modo errato. Tuttavia, sto cercando una soluzione che non richiede argomenti del costruttore o controllo del runtime.

Immagina di avere Carun'interfaccia come questa:

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

Come suggeriscono i commenti, un Cardeve avere un Enginee Transmissionma un Stereoè facoltativo. Questo significa che un costruttore che può un'istanza deve sempre e solo avere un metodo se un e avere sia già stata data l'istanza costruttore. In questo modo il controllo del tipo rifiuterà di compilare qualsiasi codice che tenti di creare un'istanza senza un o .build()Carbuild()EngineTransmissionCarEngineTransmission

Questo richiede uno Step Builder . In genere implementeresti qualcosa del genere:

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

C'è una catena di diverse classi di build ( Builder, BuilderWithEngine, CompleteBuilder), che aggiungere uno richiesto metodo setter dopo l'altro, con l'ultima classe che contiene tutti i metodi setter opzionali pure.
Ciò significa che gli utenti di questo step builder sono limitati all'ordine in cui l'autore ha reso disponibili setter obbligatori . Ecco un esempio di possibili usi (notare che sono tutti rigorosamente ordinati: engine(e)primo, seguito da transmission(t), e infine facoltativo 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();

Tuttavia, ci sono molti scenari in cui questo non è l'ideale per l'utente del builder, specialmente se il builder non ha solo setter, ma anche additivi o se l'utente non può controllare l'ordine in cui diventeranno disponibili determinate proprietà per il builder.

L'unica soluzione a cui potrei pensare è molto contorta: per ogni combinazione di proprietà obbligatorie che sono state impostate o non ancora impostate, ho creato una classe di costruttore dedicata che sa quali potenziali altri setter obbligatori devono essere chiamati prima di arrivare a un indica dove build()dovrebbe essere disponibile il metodo e ognuno di questi setter restituisce un tipo più completo di builder che è un passo avanti verso il contenimento di un build()metodo.
Ho aggiunto il codice qui sotto, ma potresti dire che sto usando il sistema dei tipi per creare un FSM che ti consente di creare un Builder, che può essere trasformato in un BuilderWithEngineo BuilderWithTransmission, che entrambi possono quindi essere trasformati in un CompleteBuilder, che implementa ilbuild()metodo. I setter opzionali possono essere invocati in una di queste istanze del builder. inserisci qui la descrizione dell'immagine

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

Come puoi vedere, questo non si adatta bene, poiché il numero di diverse classi di builder richieste sarebbe O (2 ^ n) dove n è il numero di setter obbligatori.

Da qui la mia domanda: questo può essere fatto in modo più elegante?

(Sto cercando una risposta che funzioni con Java, anche se Scala sarebbe accettabile)


1
Cosa ti impedisce di usare solo un contenitore IoC per far fronte a tutte queste dipendenze? Inoltre, non sono chiaro perché, se l'ordine non ha importanza come hai affermato, non potresti semplicemente utilizzare i normali metodi setter che restituiscono this?
Robert Harvey,

Cosa significa invocare .engine(e)due volte per un costruttore?
Erik Eidt,

3
Se vuoi verificarlo staticamente senza scrivere manualmente una classe per ogni combinazione, probabilmente devi usare cose a livello del collo come le macro o la metaprogrammazione dei template. Java non è abbastanza espressivo per questo, per quanto ne so, e lo sforzo probabilmente non varrebbe la pena rispetto alle soluzioni verificate dinamicamente in altre lingue.
Karl Bielefeldt,

1
Robert: L'obiettivo è che il controllo del tipo imponga il fatto che sia un motore che una trasmissione sono obbligatori; in questo modo non puoi nemmeno chiamare build()se non hai chiamato engine(e)e transmission(t)prima.
derabbink,

Erik: potresti voler iniziare con Engineun'implementazione predefinita e successivamente sovrascriverla con un'implementazione più specifica. Ma molto probabilmente questo avrebbe più senso se engine(e)non era un setter, ma una vipera: addEngine(e). Ciò sarebbe utile per un Carcostruttore in grado di produrre auto ibride con più di un motore / motore. Dato che questo è un esempio inventato, non sono entrato nei dettagli sul motivo per cui potresti voler farlo - per brevità.
derabbink,

Risposte:


3

Sembra che tu abbia due requisiti diversi, in base alle chiamate di metodo fornite.

  1. Solo un motore (richiesto), solo una trasmissione (obbligatoria) e un solo stereo (opzionale).
  2. Uno o più motori (obbligatori), una o più trasmissioni (obbligatori) e uno o più stereo (opzionali).

Penso che il primo problema qui sia che non sai cosa vuoi che faccia la classe. Parte di ciò è che non si sa come si desidera che l'oggetto costruito appaia.

Un'auto può avere solo un motore e una trasmissione. Anche le auto ibride hanno un solo motore (forse a GasAndElectricEngine)

Tratterò entrambe le implementazioni:

public class CarBuilder {

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

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

e

public class CarBuilder {

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

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

Se sono necessari un motore e una trasmissione, dovrebbero essere nel costruttore.

Se non sai quale motore o trasmissione è necessario, non impostarne ancora uno; è un segno che stai creando il costruttore troppo in alto nello stack.


2

Perché non usare il modello di oggetti null? Sbarazzati di questo builder, il codice più elegante che puoi scrivere è quello che in realtà non devi scrivere.

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

    //...

}

Questo è quello che ho pensato subito. anche se non avrei il costruttore di tre parametri, solo il costruttore di due parametri che ha gli elementi obbligatori e quindi un setter per lo stereo in quanto è opzionale.
Encaitar,

1
In un semplice esempio (inventivo) simile Car, questo avrebbe senso in quanto il numero di argomenti c'tor è molto piccolo. Tuttavia, non appena hai a che fare con qualcosa di moderatamente complesso (> = 4 argomenti obbligatori), l'intera faccenda diventa più difficile da gestire / meno leggibile ("Il motore o la trasmissione sono venuti prima?"). Ecco perché dovresti usare un builder: l'API ti costringe a essere più esplicito su ciò che stai costruendo.
derabbink,

1
@derabbink Perché non spezzare la tua classe in classi più piccole in questo caso? L'uso di un builder nasconderà semplicemente il fatto che la classe sta facendo troppo e che è diventata irraggiungibile.
Scoperto l'

1
Complimenti per la fine della follia del modello.
Robert Harvey,

@Spotted alcune classi contengono solo molti dati. Ad esempio, se si desidera produrre una classe di registro di accesso che contenga tutte le informazioni correlate sulla richiesta HTTP e produca i dati in formato CSV o JSON. Ci saranno molti dati e se vuoi imporre la presenza di alcuni campi durante il tempo di compilazione, avrai bisogno di un modello di builder con un costruttore di liste arg molto lungo, che non sembra buono.
ssgao,

1

In primo luogo, a meno che tu non abbia molto più tempo di qualsiasi negozio in cui ho lavorato, probabilmente non vale la pena consentire alcun ordine di operazioni o semplicemente convivere con il fatto che puoi specificare più di una radio. Nota che stai parlando di codice, non di input dell'utente, quindi puoi avere affermazioni che falliranno durante il test dell'unità piuttosto che un secondo prima durante la compilazione.

Tuttavia, se il tuo vincolo è, come indicato nei commenti, che devi avere un motore e una trasmissione, allora applicalo mettendo tutte le proprietà obbligatorie nel costruttore del costruttore.

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 

Se è solo lo stereo che è facoltativo, è possibile fare il passo finale usando sottoclassi di costruttori, ma oltre a ciò il guadagno derivante dall'ottenere l'errore in fase di compilazione piuttosto che nel test probabilmente non vale la pena.


0

il numero di diverse classi di costruttore richieste sarebbe O (2 ^ n) dove n è il numero di setter obbligatori.

Hai già indovinato la direzione corretta per questa domanda.

Se vuoi ottenere il controllo in fase di compilazione, avrai bisogno dei (2^n)tipi. Se si desidera ottenere il controllo del tempo di esecuzione, sarà necessaria una variabile in grado di memorizzare gli (2^n)stati; nfarà un numero intero a bit.


Poiché C ++ supporta parametri di modello non di tipo (ad esempio valori interi) , è possibile creare un'istanza di un modello di classe C ++ in O(2^n)tipi diversi, utilizzando uno schema simile a questo .

Tuttavia, in lingue che non supportano parametri di modello non di tipo, non è possibile fare affidamento sul sistema di O(2^n)tipi per creare un'istanza di tipi diversi.


La prossima opportunità è con le annotazioni Java (e gli attributi C #). Questi metadati aggiuntivi possono essere utilizzati per attivare il comportamento definito dall'utente in fase di compilazione, quando vengono utilizzati processori di annotazione . Tuttavia, sarebbe troppo lavoro per te implementarli. Se si utilizzano framework che forniscono questa funzionalità, utilizzarla. Altrimenti, controlla la prossima opportunità.


Infine, nota che la memorizzazione di O(2^n)stati diversi come variabile in fase di esecuzione (letteralmente, come un numero intero che ha almeno nbit di larghezza) è molto semplice. Questo è il motivo per cui le risposte più votate ti consigliano di eseguire questo controllo in fase di esecuzione, perché lo sforzo necessario per implementare il controllo in fase di compilazione è troppo, rispetto al potenziale guadagno.

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.