Java: come implementare un generatore di fasi per il quale l'ordine dei setter non è importante?

9

Modifica: vorrei sottolineare che questa domanda descrive un problema teorico e sono consapevole che posso usare gli argomenti del costruttore per i parametri obbligatori o lanciare un'eccezione di runtime se l'API è usato in modo errato. Tuttavia, sto cercando una soluzione che non richieda argomenti del costruttore o controllo del runtime.

Immagina di avere un'interfaccia Car come questa:

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

Come suggerito dai commenti, un Car deve avere Engine e Transmission ma un Stereo è facoltativo. Ciò significa che un Builder che può build() a% istanza diCar dovrebbe sempre avere un metodo build() se un Engine e Transmission sono già stati assegnati all'istanza del builder. In questo modo il programma di controllo dei tipi rifiuterà di compilare qualsiasi codice che tenta di creare un'istanza Car senza Engine o Transmission .

Questo richiede un Step Builder . Normalmente 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;
                }
            };
        }
    }
}

Esiste una catena di diverse classi di builder ( Builder , BuilderWithEngine , CompleteBuilder ), che aggiunge un metodo setter richiesto dopo l'altro, con l'ultima classe contenente anche tutti i metodi setter opzionali.
Ciò significa che gli utenti di questo step builder sono confinati nell'ordine in cui l'autore ha reso disponibili setter obbligatori . Ecco un esempio di possibili usi (si noti che sono tutti rigorosamente ordinati: engine(e) prima, seguito da transmission(t) e infine% facoltativostereo(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 costruttore, specialmente se il costruttore non ha solo setter, ma anche adder, o se l'utente non può controllare l'ordine in cui certe proprietà per il builder diventare disponibile.

L'unica soluzione a cui potrei pensare è molto complessa: per ogni combinazione di proprietà obbligatorie impostate o non ancora impostate, ho creato una classe di build dedicata che sa quale potenziale altro i setter obbligatori devono essere chiamati prima di arrivare a uno stato in cui dovrebbe essere disponibile il metodo build() , e ciascuno di questi setter restituisce un tipo più completo di builder che è un passo avanti verso il contenimento di un metodo build() .
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 BuilderWithEngine o BuilderWithTransmission , che entrambi possono quindi essere trasformato in CompleteBuilder , che implementa il metodo build() . I setter facoltativi possono essere richiamati su una di queste istanze del builder.

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 è scalabile, poiché il numero di diverse classi di builder richieste sarebbe O (2 ^ n) dove n è il numero di setter obbligatori.

Quindi la mia domanda: può essere eseguita in modo più elegante?

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

    
posta derabbink 08.07.2016 - 00:55
fonte

4 risposte

4

Sembra che tu abbia due requisiti diversi, in base alle chiamate al metodo che hai fornito.

  1. Solo un motore (richiesto), solo una (richiesta) trasmissione e solo uno (opzionale) stereo.
  2. Uno o più motori (richiesti), una o più trasmissioni (richieste) e uno o più impianti stereo (facoltativi).

Penso che il primo problema qui sia che non sai cosa vuoi la classe da fare. In parte questo è che non si sa come si vuole che l'oggetto costruito sia simile.

Un'auto può avere solo un motore e una trasmissione. Anche le auto ibride hanno un solo motore (forse un 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 richiesti un motore e una trasmissione, dovrebbero essere nel costruttore.

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

    
risposta data 08.07.2016 - 19:57
fonte
2

Perché non utilizzare il modello di oggetto nullo? Sbarazzati di questo costruttore, 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;
    }

    //...

}
    
risposta data 08.07.2016 - 15:31
fonte
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 di consentire alcun ordine di operazioni o semplicemente di vivere con il fatto che puoi specificare più di una radio. Nota che stai parlando di codice, non di input dell'utente, quindi puoi avere asserzioni che falliranno durante il test dell'unità piuttosto che un secondo prima in fase di compilazione.

Tuttavia, se il tuo vincolo è, come indicato nei commenti, che devi avere un motore e una trasmissione, quindi applicalo mettendo tutte le proprietà obbligatorie è il 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 è opzionale, è possibile fare il passo finale usando le sottoclassi di builder, ma oltre a ciò il guadagno derivante dall'ottenere l'errore in fase di compilazione piuttosto che in testing probabilmente non vale la pena.

    
risposta data 08.07.2016 - 11:55
fonte
0

the number of different builder classes required would be O(2^n) where n is the number of mandatory setters.

Hai già indovinato la direzione corretta per questa domanda.

Se vuoi ottenere il controllo in fase di compilazione, avrai bisogno di% tipi di(2^n). Se si desidera ottenere il controllo in fase di esecuzione, è necessaria una variabile in grado di memorizzare (2^n) stati; lo farà un intero n -bit.

Perché il C ++ supporta il parametro del modello non tipografico (ad es. valori interi) , è possibile che un modello di classe C ++ sia istanziato in O(2^n) diversi tipi, usando uno schema simile a questo .

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

La prossima opportunità è con annotazioni Java (e attributi C #). Questi metadati aggiuntivi possono essere utilizzati per innescare comportamenti definiti dall'utente in fase di compilazione, quando vengono utilizzati i processori di annotazione . Tuttavia, sarebbe troppo lavoro per te implementarli. Se stai usando framework che forniscono questa funzionalità per te, usala. Altrimenti, controlla la prossima opportunità.

Infine, si noti che la memorizzazione di O(2^n) di stati diversi come variabile in fase di esecuzione (letteralmente, come un numero intero che ha almeno% bit di larghezza din) è molto semplice. Questo è il motivo per cui le risposte più aggiornate raccomandano di eseguire questo controllo in fase di esecuzione, perché lo sforzo necessario per implementare il controllo in fase di compilazione è eccessivo rispetto al potenziale guadagno.

    
risposta data 07.03.2017 - 07:41
fonte

Leggi altre domande sui tag