Java: Como implementar um construtor de etapas para o qual a ordem dos setters não importa?


10

Editar: gostaria de salientar que esta pergunta descreve um problema teórico e sei que posso usar argumentos construtores para parâmetros obrigatórios ou lançar uma exceção de tempo de execução se a API for usada incorretamente. No entanto, estou procurando uma solução que não exija argumentos de construtor ou verificação de tempo de execução.

Imagine que você tem uma Carinterface como esta:

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

Como os comentários sugerem, um Cardeve ter um Enginee Transmissionmas a Stereoé opcional. Isso significa que um Construtor que pode build()uma Carinstância só deve ter um build()método se um Enginee Transmissionjá tiverem sido dados à instância do construtor. Dessa forma, o verificador de tipos se recusará a compilar qualquer código que tente criar uma Carinstância sem um Engineou Transmission.

Isso exige um Step Builder . Normalmente, você implementaria algo assim:

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

Há uma cadeia de diferentes classes construtor ( Builder, BuilderWithEngine, CompleteBuilder), que um add necessário método setter após o outro, com a última classe que contém todos os métodos setter opcionais também.
Isso significa que os usuários deste construtor de etapas estão confinados à ordem em que o autor disponibilizou os configuradores obrigatórios . Aqui está um exemplo de possíveis usos (observe que todos eles são estritamente ordenados: engine(e)primeiro, seguido por transmission(t)e, finalmente, opcional 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();

No entanto, existem muitos cenários nos quais isso não é ideal para o usuário do construtor, especialmente se o construtor não tiver apenas setters, mas também adicionadores, ou se o usuário não puder controlar a ordem na qual determinadas propriedades para o construtor ficarão disponíveis.

A única solução em que pude pensar é muito complicada: para cada combinação de propriedades obrigatórias que foram definidas ou ainda não definidas, criei uma classe de construtor dedicada que sabe quais outros setters obrigatórios em potencial precisam ser chamados antes de chegar a um indique onde o build()método deve estar disponível e cada um desses setters retorna um tipo mais completo de construtor, que está a um passo de conter um build()método.
Adicionei o código abaixo, mas você pode dizer que estou usando o sistema de tipos para criar um FSM que permite criar um Builder, que pode ser transformado em um BuilderWithEngineou BuilderWithTransmission, que ambos podem ser transformados em um CompleteBuilder, que implementa obuild()método. Os setters opcionais podem ser chamados em qualquer uma dessas instâncias do construtor. insira a descrição da imagem aqui

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

Como você pode ver, isso não é bem dimensionado, pois o número de diferentes classes de construtores necessárias seria O (2 ^ n) em que n é o número de setters obrigatórios.

Daí a minha pergunta: isso pode ser feito de maneira mais elegante?

(Estou procurando uma resposta que funcione com Java, embora Scala também seja aceitável)


1
O que impede você de usar apenas um contêiner de IoC para suportar todas essas dependências? Além disso, não sei por que, se a ordem não importa como você afirmou, você não pode simplesmente usar métodos de setter comuns que retornam this?
Robert Harvey

O que significa chamar .engine(e)duas vezes para um construtor?
Erik Eidt 07/07

3
Se você deseja verificá-lo estaticamente sem escrever manualmente uma classe para cada combinação, provavelmente precisará usar itens no nível da barba como macros ou metaprogramação de modelos. Java não é expressivo o suficiente para isso, que eu saiba, e o esforço provavelmente não valeria a pena em relação às soluções verificadas dinamicamente em outras linguagens.
Karl Bielefeldt

1
Robert: O objetivo é fazer com que o verificador de tipos imponha o fato de que um motor e uma transmissão são obrigatórios; Dessa forma, você nem pode ligar build()se não ligou engine(e)e transmission(t)antes.
Derabbink 8/07

Erik: Você pode querer começar com uma Engineimplementação padrão e depois substituí-la por uma implementação mais específica. Mas, muito provavelmente isso faria mais sentido se engine(e)não era um setter, mas uma víbora: addEngine(e). Isso seria útil para um Carconstrutor que pode produzir carros híbridos com mais de um motor / motor. Como este é um exemplo artificial, não entrei em detalhes sobre por que você pode querer fazer isso - por uma questão de brevidade.
Derabbink

Respostas:


3

Você parece ter dois requisitos diferentes, com base nas chamadas de método fornecidas.

  1. Apenas um mecanismo (obrigatório), apenas uma transmissão (necessária) e apenas um estéreo (opcional).
  2. Um ou mais motores (necessários), uma ou mais transmissões (necessárias) e um ou mais aparelhos de som (opcionais).

Acho que a primeira questão aqui é que você não sabe o que deseja que a turma faça. Parte disso é que não se sabe como você deseja que o objeto criado seja.

Um carro pode ter apenas um motor e uma transmissão. Até carros híbridos têm apenas um motor (talvez a GasAndElectricEngine)

Vou abordar as duas implementações:

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 um motor e uma transmissão forem necessários, eles deverão estar no construtor.

Se você não sabe qual motor ou transmissão é necessária, não configure um ainda; é um sinal de que você está criando o construtor muito acima da pilha.


2

Por que não usar o padrão de objeto nulo? Livre-se desse construtor, o código mais elegante que você pode escrever é aquele que você realmente não precisa escrever.

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

    //...

}

Isto é o que eu pensei imediatamente. embora eu não tivesse o construtor de três parâmetros, apenas o construtor de dois parâmetros que possui os elementos obrigatórios e, em seguida, um setter para o estéreo, pois é opcional.
Encaitar

1
Em um exemplo simples (artificial) como Car, isso faria sentido, pois o número de argumentos c'tor é muito pequeno. No entanto, assim que você lida com algo moderadamente complexo (> = 4 argumentos obrigatórios), tudo fica mais difícil de manusear / menos legível ("O motor ou a transmissão vieram primeiro?"). É por isso que você usaria um construtor: a API força você a ser mais explícito sobre o que está construindo.
21416 derabbink

1
@derabbink Por que não quebrar sua classe em turmas menores neste caso? O uso de um construtor ocultará apenas o fato de que a classe está fazendo muito e se tornou insustentável.
manchado

1
Parabéns por acabar com a loucura dos padrões.
Robert Harvey

@Spotted algumas classes apenas contêm muitos dados. Por exemplo, se você deseja produzir uma classe de log de acesso que contém todas as informações relacionadas sobre a solicitação HTTP e gera os dados no formato CSV ou JSON. Haverá muitos dados e, se você quiser impor a presença de alguns campos durante o tempo de compilação, precisará de um padrão de construtor com um construtor de lista de argumentos muito longo, o que não parece bom.
ssgao

1

Em primeiro lugar, a menos que você tenha muito mais tempo do que qualquer loja em que trabalhei, provavelmente não vale a pena permitir nenhuma ordem de operações ou apenas conviver com o fato de que você pode especificar mais de um rádio. Observe que você está falando de código, não de entrada do usuário, para poder ter afirmações que falharão durante o teste de unidade, em vez de um segundo antes no tempo de compilação.

No entanto, se sua restrição é, conforme fornecido nos comentários, que você precisa ter um mecanismo e uma transmissão, imponha isso colocando todas as propriedades obrigatórias no construtor do construtor.

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 é apenas o estéreo que é opcional, é possível executar a etapa final usando subclasses de construtores, mas além disso, o ganho de obter o erro no tempo de compilação e não no teste provavelmente não vale o esforço.


0

o número de diferentes classes de construtores necessárias seria O (2 ^ n) em que n é o número de configuradores obrigatórios.

Você já adivinhou a direção correta para esta pergunta.

Se você deseja obter a verificação em tempo de compilação, precisará de (2^n)tipos. Se você deseja obter a verificação em tempo de execução, precisará de uma variável que possa armazenar (2^n)estados; um nnúmero inteiro de bits serve.


Como o C ++ suporta parâmetros de modelo que não sejam do tipo (por exemplo, valores inteiros) , é possível que um modelo de classe C ++ seja instanciado em O(2^n)tipos diferentes, usando um esquema semelhante a este .

No entanto, em idiomas que não suportam parâmetros de modelo que não sejam do tipo, não é possível confiar no sistema de O(2^n)tipos para instanciar tipos diferentes.


A próxima oportunidade é com anotações Java (e atributos C #). Esses metadados adicionais podem ser usados ​​para disparar o comportamento definido pelo usuário em tempo de compilação, quando processadores de anotação são usados. No entanto, seria muito trabalho para você implementá-los. Se você estiver usando estruturas que fornecem essa funcionalidade para você, use-a. Caso contrário, verifique a próxima oportunidade.


Por fim, observe que armazenar O(2^n)estados diferentes como uma variável em tempo de execução (literalmente, como um número inteiro com pelo menos nbits de largura) é muito fácil. É por isso que as respostas mais votadas recomendam que você execute essa verificação em tempo de execução, porque o esforço necessário para implementar a verificação em tempo de compilação é muito alto, quando comparado ao ganho potencial.

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.