Como devo lidar com configurações incompatíveis com o padrão Builder?


9

Isso é motivado por esta resposta a uma pergunta separada .

O padrão do construtor é usado para simplificar a inicialização complexa, especialmente com parâmetros de inicialização opcionais). Mas não sei como gerenciar adequadamente configurações mutuamente exclusivas.

Aqui está uma Imageaula. Imagepode ser inicializado de um arquivo ou de um tamanho, mas não ambos . O uso de construtores para impor essa exclusão mútua é óbvio quando a classe é bastante simples:

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

Agora assuma que Imageé realmente configurável o suficiente para que o padrão do construtor seja útil, de repente isso pode ser possível:

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

Esses problemas devem ser detectados no tempo de execução e não no tempo de compilação, o que não é a pior coisa. O problema é que a detecção consistente e abrangente desses problemas dentro da ImageBuilderclasse pode se tornar complexa, especialmente em termos de manutenção.

Como devo lidar com configurações incompatíveis no padrão do construtor?

Respostas:


12

Você tem seu construtor. No entanto, neste ponto, você precisa de algumas interfaces.

Há uma interface FileBuilder que define um subconjunto de métodos (não setSize) e uma interface SizeBuilder que define outro subconjunto de métodos (não setFilename). Você pode desejar que uma interface GenericBuilder estenda o FileBuilder e o SizeBuilder - não é necessário, embora algumas pessoas possam preferir essa abordagem.

O método setSize()retorna um SizeBuilder. O método setFilename()retorna um FileBuilder.

O ImageBuilder tem toda a lógica para ambos setSize()e setFileName(). No entanto, o tipo de retorno para estes especificaria a interface de subconjunto apropriada.

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(...);
    }
}

Uma parte especial aqui é que, depois de ter um SizeBuilder, todos os retornos precisam ser SizeBuilders. A interface para ele se parece com:

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

Portanto, depois de chamar um desses métodos, você não pode mais chamar o outro e criar um objeto com um estado inválido.


Muito interessante, obrigado. Estou um pouco confuso sobre como eles seriam usados. Especificamente, não consigo descobrir quais seriam os tipos de declaração e inicialização. Provavelmente, estou imaginando coisas muito mais complicadas do que o necessário. Você poderia incluir um exemplo de uso?
Kdbanman

O construtor de imagens retorna a interface correspondente à mudança de estado que esse método chama. No entanto, depois que você recuperar uma interface específica do ImageBuilder, futuras chamadas contra esse objeto serão feitas nessa interface, o que restringe a capacidade de chamar métodos incompatíveis.

1
@rwong, embora eu admita que não analisou profundamente o problema, o problema que pensei ter com essa abordagem foi que o 'estado' do construtor poderia ser redefinido. Seria necessário garantir que, uma vez que setSize () fosse chamado, todas as outras chamadas de construtor estivessem no SizeBuilder. Se o tipo de setRange () não for o SizeBuilder ou algo que estenda / implemente isso poderia ser chamado de setFilename nele novamente. Você também tem a situação (não descrita aqui) em que, em vez do tamanho, possui int width e int height, de modo que ambos precisam ser chamados.

1
@MichaelT Dados os intrincados problemas de evasão, eu suspeito que impor uma ordem estrita de inicialização de parâmetro (resultando em uma árvore de prefixo de itens de parâmetro) pode ser uma boa coisa ao usar o padrão do construtor. Como resultado, itens de parâmetros comuns como Rangee Stuffdevem ser inicializados primeiro, não em momentos arbitrários.
Rwong

1
@ MichaelT: nesse ponto, o LSP entra em jogo. Você pode ter certeza de que os métodos do tipo aparente ( RangeAndStuffBuilder) podem ser chamados no tipo real. Restrições adicionais podem ser implementadas retornando tipos mais básicos para alguns métodos (embora isso cause um aumento exponencial nos tipos), removendo efetivamente as operações. Desde que os resultados do método não retornem à hierarquia, você não receberá erros de tipo. O cenário setHeight/ setWidthpode ser implementado com uma hierarquia de irmãos que não possui um buildmétodo.
Outis
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.