Builder Pattern: quando falhar?

44

Ao implementar o Builder Pattern, muitas vezes me confundo com quando deixar a construção falhar e até consigo tomar diferentes posições sobre o assunto a cada poucos dias.

Primeiro, alguma explicação:

  • Com falha no início , quero dizer que a construção de um objeto deve falhar assim que um parâmetro inválido é passado. Então, dentro do SomeObjectBuilder .
  • Com atrasando , quero dizer que a construção de um objeto só pode falhar na chamada build() que chama implicitamente um construtor do objeto a ser construído.

Então alguns argumentos:

  • A favor do atraso tardio: uma classe de construtor não deve ser mais do que uma classe que simplesmente contém valores. Além disso, leva a menos duplicação de código.
  • A favor da falha inicial: Uma abordagem geral na programação de software é que você deseja detectar problemas o mais cedo possível e, portanto, o local mais lógico a ser verificado seria na classe do construtor 'constructor', 'setters' e, por fim, no método de construção.

Qual é o consenso geral sobre isso?

    
por skiwi 28.05.2014 / 13:43
fonte

5 respostas

34

Vamos ver as opções, onde podemos colocar o código de validação:

  1. Dentro dos setters no construtor.
  2. Dentro do método build() .
  3. Dentro da entidade construída: ela será invocada no método build() quando a entidade estiver sendo criada.

A opção 1 nos permite detectar problemas mais cedo, mas pode haver casos complicados quando podemos validar a entrada tendo apenas o contexto completo, fazendo assim pelo menos parte da validação no método build() . Assim, a escolha da opção 1 resultará em código inconsistente, com parte da validação sendo feita em um local e outra sendo feita em outro local.

A opção 2 não é significativamente pior que a opção 1, porque, geralmente, os configuradores no construtor são invocados imediatamente antes do build() , especialmente em interfaces fluentes. Assim, ainda é possível detectar um problema com antecedência suficiente na maioria dos casos. No entanto, se o construtor não for a única maneira de criar um objeto, isso levará à duplicação do código de validação, porque você precisará tê-lo em todos os lugares em que criar um objeto. A solução mais lógica, neste caso, será colocar a validação o mais próximo possível do objeto criado, ou seja, dentro dele. E esta é a opção 3 .

Do ponto de vista do SOLID, colocar a validação no construtor também viola o SRP: a classe do construtor já tem a responsabilidade de agregar os dados para construir um objeto. A validação é estabelecer contratos em seu próprio estado interno, é uma nova responsabilidade verificar o estado de outro objeto.

Assim, do meu ponto de vista, não só é melhor falhar tarde na perspectiva do design, como também é melhor falhar dentro da entidade construída, e não no próprio construtor.

UPD: > este comentário me lembrou de mais uma possibilidade, quando a validação dentro do construtor (opção 1 ou 2) faz sentido. Faz sentido se o construtor tiver seus próprios contratos nos objetos que está criando. Por exemplo, suponha que tenhamos um construtor que construa uma cadeia com conteúdo específico, digamos, lista de intervalos de números 1-2,3-4,5-6 . Esse construtor pode ter um método como addRange(int min, int max) . A string resultante não sabe nada sobre esses números, nem deveria saber. O próprio construtor define o formato da string e as restrições nos números. Assim, o método addRange(int,int) deve validar os números de entrada e lançar uma exceção se max for menor que min.

Dito isso, a regra geral será validar apenas os contratos definidos pelo próprio construtor.

    
por 28.05.2014 / 15:49
fonte
32

Considerando que você usa Java, considere a orientação autoritativa e detalhada fornecida por Joshua Bloch no artigo Criando e destruindo objetos Java (a fonte em negrito abaixo da citação é minha):

Like a constructor, a builder can impose invariants on its parameters. The build method can check these invariants. It is critical that they be checked after copying the parameters from the builder to the object, and that they be checked on the object fields rather than the builder fields (Item 39). If any invariants are violated, the build method should throw an IllegalStateException (Item 60). The exception's detail method should indicate which invariant is violated (Item 63).

Another way to impose invariants involving multiple parameters is to have setter methods take entire groups of parameters on which some invariant must hold. If the invariant isn't satisfied, the setter method throws an IllegalArgumentException. This has the advantage of detecting the invariant failure as soon as the invalid parameters are passed, instead of waiting for build to be invoked.

Observe de acordo com explicação do editor neste artigo, os "itens" na citação acima se referem às regras apresentadas em Java efetivo, segunda edição .

O artigo não explora profundamente por que isso é recomendado, mas se você pensar nisso, as razões são bastante aparentes. Dica genérica para entender isso é fornecida ali mesmo no artigo, na explicação de como o conceito de construtor está conectado àquele do construtor - e espera-se que as invariantes de classe sejam verificadas no construtor, não em qualquer outro código que possa preceder / preparar sua invocação.

Para um entendimento mais concreto do motivo pelo qual a verificação de invariantes antes de invocar uma compilação seria errada, considere um exemplo popular de CarBuilder . Os métodos do construtor podem ser invocados em uma ordem arbitrária e, como resultado, não é possível saber se um determinado parâmetro é válido até a compilação.

Considere que o carro esportivo não pode ter mais de 2 assentos, como saber se setSeats(4) está bem ou não? É somente na compilação quando se pode saber com certeza se setSportsCar() foi invocado ou não, significando se você deve jogar TooManySeatsException ou não.

    
por 28.05.2014 / 16:04
fonte
19

Valores inválidos inválidos porque não são tolerados devem ser imediatamente informados na minha opinião. Em outras palavras, se você aceitar apenas números positivos e um número negativo for passado, não haverá necessidade de esperar até que build() seja chamado. Eu não consideraria esses tipos de problemas que você "esperaria" que acontecessem, já que é um pré-requisito para chamar o método para começar. Em outras palavras, você provavelmente não dependeria da falha de definir certos parâmetros. É mais provável que você presumisse que os parâmetros estão corretos ou que você realizaria alguma verificação por conta própria.

No entanto, para problemas mais complicados que não são tão facilmente validados, é melhor dar a conhecer quando você chama build() . Um bom exemplo disso pode estar usando as informações de conexão fornecidas para estabelecer uma conexão com um banco de dados. Neste caso, enquanto você tecnicamente poderia verificar essas condições, não é mais intuitivo e apenas complica seu código. A meu ver, estes são também os tipos de problemas que podem realmente acontecer e que você não pode realmente prever até que você tente. É uma espécie de diferença entre combinar uma string com uma expressão regular para ver se ela poderia ser analisada como um int e simplesmente tentar analisá-la, manipulando quaisquer exceções potenciais que possam ocorrer como conseqüência.

Eu geralmente não gosto de jogar exceções ao definir parâmetros, pois isso significa ter que pegar qualquer exceção lançada, então tenho a tendência de favorecer a validação em build() . Portanto, por esse motivo, prefiro usar o RuntimeException, pois os erros nos parâmetros passados geralmente não devem acontecer.

No entanto, isso é mais uma prática recomendada do que qualquer outra coisa. Espero que responda a sua pergunta.

    
por 28.05.2014 / 14:30
fonte
11

Até onde eu sei, a prática geral (não tenho certeza se há consenso) é falhar assim que você puder descobrir um erro. Isso também dificulta o uso indevido inadvertido de sua API.

Se for um atributo trivial que pode ser verificado na entrada, como uma capacidade ou tamanho que não deve ser negativo, é melhor falhar imediatamente. Suspender o erro aumenta a distância entre o erro e o feedback, o que torna mais difícil encontrar a origem do problema.

Se você tem a infelicidade de estar em uma situação em que a validade de um atributo depende dos outros, então você tem duas opções:

  • Exija que ambos (ou mais) atributos sejam fornecidos simultaneamente (ou seja, invocação de método único).
  • Teste a validade assim que você souber que nenhuma outra alteração será recebida: quando build() ou assim for chamado.

Como na maioria das coisas, essa é uma decisão tomada em um contexto. Se o contexto faz com que seja complicado ou complicado falhar no início, um trade-off pode ser feito para adiar as verificações para um momento posterior, mas fail-fast deve ser o padrão.

    
por 28.05.2014 / 14:43
fonte
0

A regra básica é "falhar cedo".

A regra um pouco mais avançada é "falhar o mais cedo possível".

Se uma propriedade é intrinsecamente inválida ...

CarBuilder.numberOfWheels( -1 ). ...  

... então você rejeita isso imediatamente.

Outros casos podem precisar de valores a serem verificados em combinação e podem estar melhor posicionados no método build ():

CarBuilder.numberOfWheels( 0 ).type( 'Hovercraft' ). ...  
    
por 19.07.2017 / 15:20
fonte