As primitivas, como string
ou int
, não têm significado em um domínio de negócios. Uma conseqüência direta disso é que você pode usar um URL por engano quando um ID de produto é esperado ou usar quantidade ao esperar preço .
É também por isso que o desafio Object Calisthenics apresenta as primitivas embrulho como uma de suas regras:
Rule 3: Wrap all primitives and Strings
In the Java language, int is a primitive, not a real object, so it obeys different rules than objects. It is used with a syntax that isn’t object-oriented. More importantly, an int on its own is just a scalar, so it has no meaning. When a method takes an int as a parameter, the method name needs to do all of the work of expressing the intent. If the same method takes an Hour as a parameter, it’s much easier to see what’s going on.
O mesmo documento explica que há um benefício adicional:
Small objects like Hour or Money also give us an obvious place to put behavior that would otherwise have been littered around other classes.
De fato, quando os primitivos são usados, normalmente é extremamente difícil rastrear a localização exata do código relacionado a esses tipos, geralmente levando a uma severa duplicação de código . Se houver Price: Money
class, é natural encontrar a verificação dentro do intervalo. Se, em vez disso, um int
(pior, um double
) é usado para armazenar os preços dos produtos, quem deve validar o intervalo? O produto? O desconto? O carrinho?
Finalmente, um terceiro benefício não mencionado no documento é a capacidade de alterar relativamente fácil o tipo subjacente. Se hoje meu ProductId
tiver short
como seu tipo subjacente e depois eu precisar usar int
, provavelmente o código a ser alterado não abrangerá toda a base de código.
A desvantagem - e o mesmo argumento aplica-se a todas as regras do exercício da Object Calisthenics - é que se torna rapidamente muito avassalador para criar uma classe para tudo . Se Product
contiver ProductPrice
que herda de PositivePrice
que herda de Price
, que por sua vez herda de Money
, essa não é uma arquitetura limpa, mas sim uma bagunça completa onde, para encontrar uma única coisa, uma mantenedor deve abrir algumas dezenas de arquivos o tempo todo.
Outro ponto a considerar é o custo (em termos de linhas de código) de criar classes adicionais. Se os invólucros são imutáveis (como deveriam ser, geralmente), isso significa que, se tomarmos C #, você tem que ter, dentro do wrapper, pelo menos:
- O getter de propriedade,
- Seu campo de apoio,
- Um construtor que atribui o valor ao campo de apoio,
- Um
ToString()
personalizado, - comentários da documentação XML (o que torna muito de linhas),
- Um
Equals
e umGetHashCode
substitui (também um monte de LOC).
e, eventualmente, quando relevante:
- Um atributo DebuggerDisplay ,
- Uma substituição de
==
e!=
operadores, - Eventualmente, uma sobrecarga do operador de conversão implícito para converter e para o tipo encapsulado,
- Contratos de código (incluindo o invariante, que é um método bastante longo, com seus três atributos),
- Diversos conversores que serão utilizados durante a serialização de XML, serialização de JSON ou armazenamento / carregamento de um valor para / de um banco de dados.
Uma centena de LOC para um simples invólucro faz com que seja bastante proibitivo, e é por isso que você pode estar completamente certo da rentabilidade a longo prazo de tal invólucro. A noção de escopo explicada por Thomas Junk é particularmente relevante aqui. Escrever cem LOCs para representar um ProductId
usado em toda a sua base de código parece bastante útil. Escrever uma classe desse tamanho para um pedaço de código que faz três linhas em um único método é muito mais questionável.
Conclusão:
-
Faça quebra de primitivas em classes que tenham um significado em um domínio de negócios do aplicativo quando (1) ajudar a reduzir erros, (2) reduzir o risco de duplicação de código ou (3) ajudar a alterar o tipo subjacente posteriormente .
-
Não agrupe automaticamente todas as primitivas encontradas em seu código: há muitos casos em que usar
string
ouint
é perfeitamente aceitável.
Na prática, em public string CreateNewThing()
, retornar uma instância de ThingId
class em vez de string
pode ajudar, mas você também pode:
-
Retorna uma instância de
Id<string>
class, que é um objeto do tipo genérico, indicando que o tipo subjacente é uma string. Você tem o seu benefício de legibilidade, sem a desvantagem de ter que manter muitos tipos. -
Retorna uma instância da classe
Thing
. Se o usuário precisar apenas do ID, isso pode ser feito facilmente com:var thing = this.CreateNewThing(); var id = thing.Id;