Ao implementar o Padrão do Construtor, muitas vezes me vejo confuso sobre quando deixar a construção falhar e até consigo tomar posições diferentes sobre o assunto a cada poucos dias.
Primeiro alguma explicação:
- Com a falha inicial, quero dizer que a construção de um objeto deve falhar assim que um parâmetro inválido for passado. Assim, dentro do
SomeObjectBuilder
. - Com o atraso tardio, quero dizer que a construção de um objeto só pode falhar na
build()
chamada que implicitamente chama um construtor do objeto a ser construído.
Depois, alguns argumentos:
- A favor de falhar tarde: Uma classe de construtor não deve ser mais do que uma classe que simplesmente manté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 para verificar seria na classe do construtor 'construtor', 'setters' e, finalmente, no método de construção.
Qual é o consenso geral sobre isso?
java
design-patterns
skiwi
fonte
fonte
null
objeto quando houver um problemabuild()
.Respostas:
Vamos dar uma olhada nas opções, onde podemos colocar o código de validação:
build()
método.build()
método quando a entidade estiver sendo criada.A opção 1 nos permite detectar problemas mais cedo, mas pode haver casos complicados em que podemos validar a entrada apenas com o contexto completo, fazendo assim pelo menos parte da validação no
build()
método. Assim, escolher a opção 1 levará a um código inconsistente, com parte da validação sendo feita em um local e outra parte sendo feita em outro local.A opção 2 não é significativamente pior que a opção 1, porque, geralmente, os setters no construtor são chamados imediatamente antes
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 nesse 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 está estabelecendo 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 da perspectiva do design, mas também é melhor falhar dentro da entidade construída do que no próprio construtor.
UPD: esse comentário me lembrou 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 sequência com conteúdo específico, por exemplo, lista de intervalos de números
1-2,3-4,5-6
. Este construtor pode ter um método comoaddRange(int min, int max)
. A sequência resultante não sabe nada sobre esses números, nem precisa saber. O próprio construtor define o formato da sequência e as restrições nos números. Portanto, o métodoaddRange(int,int)
deve validar os números de entrada e gerar uma exceção se max for menor que min.Dito isto, a regra geral será validar apenas os contratos definidos pelo próprio construtor.
fonte
Como você usa Java, considere as orientações autorizadas e detalhadas fornecidas por Joshua Bloch no artigo Criando e destruindo objetos Java (a fonte em negrito na citação abaixo é minha):
Observe que, de acordo com a explicação do editor deste artigo, "itens" na citação acima se referem às regras apresentadas no Effective Java, Second Edition .
O artigo não explica detalhadamente por que isso é recomendado, mas se você pensar nisso, os motivos são bastante aparentes. Uma dica genérica para entender isso é fornecida logo no artigo, na explicação de como o conceito de construtor está conectado ao de construtor - e espera-se que os invariantes de classe sejam verificados no construtor, e não em qualquer outro código que possa preceder / preparar sua invocação.
Para um entendimento mais concreto sobre por que a verificação de invariantes antes de invocar uma construção estaria errada, considere um exemplo popular do CarBuilder . Os métodos do construtor podem ser chamados em uma ordem arbitrária e, como resultado, não se pode realmente saber se um parâmetro específico é 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? É apenas na construção que se pode saber com certeza sesetSportsCar()
foi invocado ou não, ou seja, se deve ser lançadoTooManySeatsException
ou não.fonte
Valores inválidos que são inválidos porque não são tolerados devem ser divulgados imediatamente na minha opinião. Em outras palavras, se você aceitar apenas números positivos e um número negativo for passado, não será necessário esperar até que
build()
seja chamado. Eu não consideraria esses os tipos de problemas que você "esperaria" que acontecessem, pois é um pré-requisito para chamar o método para começar. Em outras palavras, você provavelmente não dependeria da falha em definir determinados parâmetros. É mais provável que você presuma que os parâmetros estejam corretos ou faça alguma verificação por conta própria.No entanto, para problemas mais complicados que não são tão facilmente validados, pode ser melhor conhecido quando você liga
build()
. Um bom exemplo disso pode ser o uso das informações de conexão fornecidas para estabelecer uma conexão com um banco de dados. Nesse caso, embora você possa tecnicamente verificar essas condições, ele não é mais intuitivo e apenas complica seu código. Na minha opinião, esses também são os tipos de problemas que podem realmente acontecer e que você não pode realmente antecipar até tentar. É meio que a diferença entre combinar uma string com uma expressão regular para ver se ela pode ser analisada como int e simplesmente tentar analisá-la, lidando com possíveis exceções que possam ocorrer como consequência.Geralmente, eu não gosto de lançar exceções ao definir parâmetros, pois significa ter que capturar qualquer exceção lançada, por isso tendem a favorecer a validação
build()
. Portanto, por esse motivo, prefiro usar o RuntimeException, pois erros nos parâmetros passados geralmente não devem ocorrer.No entanto, essa é mais uma prática recomendada do que qualquer coisa. Espero que isso responda à sua pergunta.
fonte
Tanto quanto eu sei, a prática geral (não tenho certeza se há consenso) é falhar o mais cedo possível. Isso também dificulta o uso indevido acidental de sua API.
Se é um atributo trivial que pode ser verificado na entrada, como capacidade ou comprimento que não deve ser negativo, é melhor falhar imediatamente. Adiar o erro aumenta a distância entre erro e feedback, o que torna mais difícil encontrar a fonte do problema.
Se você tem a infelicidade de estar em uma situação em que a validade de um atributo depende de outros, então você tem duas opções:
build()
é chamado assim.Como na maioria das coisas, essa é uma decisão tomada em um contexto. Se o contexto tornar difícil ou complicado a falha precoce, uma compensação pode ser feita para adiar as verificações posteriormente, mas a falha rápida deve ser o padrão.
fonte
unsigned
,@NonNull
etc.X
com um valor inválido, considerando o valor atual deY
, mas antes de chamarbuild()
definidoY
como um valor que tornariaX
válido.Shape
eo construtor temWithLeft
eWithRight
propriedades, e se deseja ajustar um construtor para construir um objeto em um lugar diferente, exigindo queWithRight
ser chamado pela primeira vez ao mover um certo objeto eWithLeft
ao movê-lo para a esquerda, iria adicionar complexidade desnecessária comparado com a permissãoWithLeft
para definir a borda esquerda à direita da borda direita antiga, desde queWithRight
a borda direita seja corrigida antesbuild
.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 ...
... então você rejeita imediatamente.
Outros casos podem precisar que os valores sejam verificados em combinação e podem ser melhor colocados no método build ():
fonte