Li muitos artigos recentemente que descrevem a obsessão primitiva como um cheiro de código.
Existem dois benefícios em evitar a obsessão primitiva:
Isso torna o modelo de domínio mais explícito. Por exemplo, posso conversar com um analista de negócios sobre um Código Postal em vez de uma string que contém um código postal.
Toda a validação está em um só lugar, e não no aplicativo.
Existem muitos artigos por aí que descrevem quando é um cheiro de código. Por exemplo, posso ver o benefício de remover a obsessão primitiva por um código postal como este:
public class Address
{
public ZipCode ZipCode { get; set; }
}
Aqui está o construtor do ZipCode:
public ZipCode(string value)
{
// Perform regex matching to verify XXXXX or XXXXX-XXXX format
_value = value;
}
Você estaria violando o princípio DRY , colocando essa lógica de validação em todos os lugares onde um código postal é usado.
No entanto, e os seguintes objetos:
Data de nascimento: verifique se é maior que mindate e menor que a data de hoje.
Salário: verifique se é maior ou igual a zero.
Você criaria um objeto DateOfBirth e um objeto Salary? O benefício é que você pode falar sobre eles ao descrever o modelo de domínio. No entanto, este é um caso de superengenharia, pois não há muita validação. Existe uma regra que descreva quando e quando não remover a obsessão primitiva ou você deve sempre fazer isso, se possível?
Eu acho que eu poderia criar um alias de tipo em vez de uma classe, o que ajudaria no ponto um acima.
DateOfBirth
construtor para que ele verifique?Salary
eDistance
objeto que você não pode acidentalmente utilizá-los alternadamente. Você pode se ambos forem do tipodouble
.Respostas:
O oposto seria "modelagem de domínio" ou talvez "engenharia excedente".
A introdução de um objeto Salário pode ser uma boa ideia pelo seguinte motivo: os números raramente ficam sozinhos no modelo de domínio, eles quase sempre têm uma dimensão e uma unidade. Normalmente, não modelamos nada de útil se adicionarmos um comprimento a um tempo ou a uma massa e raramente obtemos bons resultados quando misturamos metros e pés.
Quanto a DateOfBirth, provavelmente - há duas questões a considerar. Primeiro, a criação de uma data não primitiva fornece um local para centralizar todas as preocupações estranhas em torno da matemática de datas. Muitos idiomas fornecem um pronto para uso; DateTime , java.util.Date . Estes são de domínio implementações agnósticos de datas, mas eles são não primitivos.
Segundo,
DateOfBirth
não é realmente uma data; aqui nos EUA, "data de nascimento" é uma construção cultural / ficção legal. Tendemos a medir a data de nascimento a partir da data local do nascimento de uma pessoa; Bob, nascido na Califórnia, pode ter uma data de nascimento "mais cedo" do que Alice, nascida em Nova York, mesmo sendo o mais novo dos dois.Certamente nem sempre; nos limites, os aplicativos não são orientados a objetos . É bastante comum ver primitivas usadas para descrever os comportamentos nos testes .
fonte
java.util.Date
comjava.time.LocalDate
Para ser sincero: depende.
Sempre existe o risco de sobreaquecer seu código. Até que ponto o DateOfBirth e o salário serão usados? Você os usará apenas em três classes fortemente acopladas ou elas serão usadas em todo o aplicativo? Você "apenas" os encapsularia em seu próprio tipo / classe para impor essa restrição ou você pode pensar em mais restrições / funções que realmente pertencem a ela?
Vamos considerar o Salário, por exemplo: Você tem alguma operação com "Salário" (por exemplo, manipulação de moedas diferentes ou talvez uma função toString ())? Considere o que o salário é / faz quando você não o considera um primitivo simples, e há uma boa chance de o salário ser de sua própria classe.
fonte
Uma possível regra geral pode depender da camada do programa. Para o Domínio (DDD), também conhecido como Camada de Entidades (Martin, 2018), isso também pode ser "para evitar primitivas para qualquer coisa que represente um conceito de domínio / negócio". As justificativas são as estabelecidas pelo OP: um modelo de domínio mais expressivo, validação de regras de negócios, explicitando explicitamente os conceitos implícitos (Evans, 2004).
Um alias de tipo pode ser uma alternativa leve (Ghosh, 2017) e refatorado para uma classe de entidade quando necessário. Por exemplo, podemos primeiro exigir que
Salary
seja>=0
, e mais tarde decidir proibir$100.33333
e qualquer coisa acima$10,000,000
(o que levaria à falência do cliente). O uso deNonnegative
primitivo para representarSalary
e outros conceitos complicaria essa refatoração.Evitar primitivos também pode ajudar a evitar o excesso de engenharia. Suponha que precisamos combinar salário e data de nascimento em uma estrutura de dados: por exemplo, para ter menos parâmetros de método ou para passar dados entre os módulos. Então, podemos usar uma tupla com o tipo
(Salary, DateOfBirth)
. De fato, uma tupla com primitivas(Nonnegative, Nonnegative)
,, não é informativa, enquanto que alguns inchadosclass EmployeeData
esconderiam os campos obrigatórios entre outros. A assinatura em digamoscalcPension(d: (Salary, DateOfBirth))
é mais focada do que emcalcPension(d: EmployeeData)
, o que viola o Princípio de Segregação de Interface. Da mesma forma, um especialistaclass SalaryAndDateOfBirth
parece estranho e provavelmente é um exagero. Mais tarde, podemos optar por definir uma classe de dados; tuplas e tipos de domínio elementar, vamos adiar essas decisões.Em uma camada externa (por exemplo, GUI), pode fazer sentido "reduzir" as entidades até suas primitivas constituintes (por exemplo, colocar em um DAO). Isso evita o vazamento de abstrações de domínio para as camadas externas, como preconizado por Martin (2018).
Referências
E. Evans, "Design Orientado a Domínio", 2004
D. Ghosh, "Modelagem de Domínio Funcional e Reativo", 2017
RC Martin, "Arquitetura limpa", 2018
fonte
Sofre melhor da obsessão primitiva ou de ser um astronauta da arquitetura ?
Ambos os casos são patológicos, em um caso você tem poucas abstrações, levando à repetição e confundindo facilmente uma maçã com uma laranja, e no outro você esqueceu de parar com ela e começar a fazer as coisas, dificultando a realização de qualquer coisa .
Como quase sempre, você deseja moderação, um caminho do meio, esperançosamente bem considerado.
Lembre-se de que uma propriedade possui um nome, além de um tipo. Além disso, decompor um endereço em suas partes constituintes pode ser muito restritivo, se sempre feito da mesma maneira. Nem todo o mundo está no centro de NY.
fonte
Se você tivesse uma classe Salary, poderia ter métodos como ApplyRaise.
Por outro lado, sua classe ZipCode não precisa ter validação interna para evitar duplicar a validação em qualquer lugar em que você possa ter uma classe ZipCodeValidator que possa ser injetada. Portanto, se seu sistema for executado em endereços nos EUA e no Reino Unido, basta injetar o validador correto e quando você precisar lidar com os endereços da AUS, basta adicionar um novo validador.
Outra preocupação é se você precisar gravar dados em um banco de dados por meio do EntityFramework, pois precisará saber como lidar com o salário ou o CEP.
Não há uma resposta clara de onde traçar a linha entre como as classes devem ser inteligentes, mas direi que tenho tendência a mover a lógica de negócios, como validar, para as classes de lógica de negócios que têm as classes de dados como dados puros, como isso parece para trabalhar melhor com o EntityFramework.
Quanto ao uso de aliases de tipo, o nome do membro / propriedade deve fornecer todas as informações necessárias sobre o conteúdo, para que eu não usasse aliases de tipo.
fonte
(Qual é realmente a pergunta)
Quando o uso do tipo primitivo não é um cheiro de código?
(Responda)
Quando o parâmetro não possui regras - use um tipo primitivo.
Use o tipo primitivo para coisas como:
Use objeto para itens como:
O último exemplo tem regras no mesmo, ou seja, o objecto
SimpleDate
é composta deYear
,Month
eDay
. Através do uso de Object neste caso, o conceito deSimpleDate
ser válido pode ser encapsulado dentro do objeto.fonte
Além dos exemplos canônicos de endereços de e-mail ou CEP fornecidos em outras partes desta pergunta, onde eu acho que a refatoração longe do Primitive Obsession pode ser particularmente útil é com os IDs de entidades (consulte https://andrewlock.net/using-strongly-typed-entity -ids-to-evitar-primitive-obsession-part-1 / para um exemplo de como fazê-lo no .NET).
Eu perdi a conta do número de vezes que um bug apareceu porque um método tinha uma assinatura como esta:
Compila muito bem e, se você não for rigoroso com o teste de unidade, também poderá ser aprovado. No entanto, refatore esses IDs de entidade em classes específicas de domínio e, e pronto, erros em tempo de compilação:
Imagine que o método escalou até 10 ou mais parâmetros, todos
int
os tipos de dados (não importa o cheiro do código da Long Parameter List ) .Fica ainda pior quando você usa algo como o AutoMapper para alternar entre objetos de domínio e DTOs e uma refatoração que você faz não é captado pelo mapeamento automagic.fonte
Por outro lado, ao lidar com muitos países diferentes e seus diferentes sistemas de CEP, isso significa que você não pode validar um CEP, a menos que conheça o país em questão. Então seu
ZipCode
turma também precisa armazenar o país.Mas você armazena separadamente o país como parte do
Address
(do qual o CEP também faz parte) e parte do CEP (para validação)?ZipCode
classe, mas umaAddress
classe, que novamente conterá umastring ZipCode
que significa que fizemos um círculo completo.Não entendo sua afirmação subjacente de que, quando uma informação tem um tipo de variável, você é obrigado a mencionar esse tipo sempre que estiver conversando com um analista de negócios.
Por quê? Por que você não consegue simplesmente falar sobre "o CEP" e omitir completamente o tipo específico? Que tipo de discussão você está tendo com o seu analista de negócios (não técnico!), Onde o tipo de propriedade é essencial para a conversa?
De onde eu sou, os códigos postais são sempre numéricos. Portanto, temos uma escolha, podemos armazená-lo como um
int
ou comostring
. Nós tendemos a usar uma string porque não há expectativa de operações matemáticas nos dados, mas nunca um analista de negócios me disse que precisava ser uma string. Essa decisão é deixada para o desenvolvedor (ou possivelmente para o analista técnico, embora, na minha experiência, eles não lidem diretamente com o âmago da questão).Um analista de negócios não se importa com o tipo de dados, desde que o aplicativo faça o que é esperado.
A validação é uma besta complicada de enfrentar, porque depende do que os humanos esperam.
Por um lado, não concordo com o argumento da validação como uma maneira de mostrar por que a obsessão primitiva deve ser evitada, porque não concordo que (como uma verdade universal) os dados sempre precisem ser validados o tempo todo.
Por exemplo, e se for uma pesquisa mais complicada? Em vez de uma simples verificação de formato, e se a sua validação envolver entrar em contato com uma API externa e aguardar uma resposta? Deseja realmente forçar seu aplicativo a chamar essa API externa para cada
ZipCode
objeto que você instancia?Talvez seja um requisito comercial estrito e, é claro, justificável. Mas isso não é uma verdade universal. Existem muitos casos de uso em que isso é mais um fardo do que uma solução.
Como segundo exemplo, ao inserir seu endereço em um formulário, é comum inserir seu código postal antes do seu país. Embora seja bom ter um feedback de validação imediato na interface do usuário, seria realmente um obstáculo para mim (como usuário) se o aplicativo me alertasse para um formato de código postal "errado", porque a fonte real do problema é (por exemplo) meu país não é o país selecionado por padrão e, portanto, a validação ocorreu para o país errado.
É a mensagem de erro errada, que distrai o usuário e causa confusão desnecessária.
Assim como a validação perpétua não é uma verdade universal, nem são meus exemplos. É contextual . Alguns domínios de aplicativo exigem validação de dados acima de tudo. Outros domínios não colocam uma validação tão alta na lista de prioridades porque o aborrecimento que ele traz consigo entra em conflito com as prioridades reais (por exemplo, experiência do usuário ou a capacidade de armazenar inicialmente dados com defeito para que possam ser corrigidos, em vez de nunca permitir que sejam). armazenado)
O problema com essas validações é que elas são incompletas, redundantes ou indicativas de um problema muito maior .
Verificar se uma data é maior que o mindate é redundante. O mindate significa literalmente que é a menor data possível. Além disso, onde você desenha a linha de relevância? Qual é o sentido de impedir,
DateTime.MinDate
mas permitirDateTime.MinDate.AddSeconds(1)
? Você está escolhendo um valor específico que não está particularmente errado em comparação com muitos outros valores.Meu aniversário é 2 de janeiro de 1978 (não é, mas vamos supor que seja). Mas digamos que os dados em seu aplicativo estejam incorretos e, em vez disso, diga que meu aniversário é:
Todas essas datas estão erradas. Nenhum deles é "mais correto" que o outro. Mas sua regra de validação captaria apenas um desses três exemplos.
Você também omitiu completamente o contexto de como está usando esses dados. Se isso for usado, por exemplo, em um lembrete de aniversário, eu diria que a validação é inútil, pois não há conseqüências ruins em particular no preenchimento da data errada.
Por outro lado, se este é dados do governo e você precisa da data de nascimento para a identidade de alguém autenticar (e não fazê-lo leva a conseqüências ruins, por exemplo, negando alguém da segurança social), então a correção dos dados é fundamental e você precisa totalmente validar os dados. A validação proposta que você tem agora não é adequada.
Para um salário, existe algum senso comum de que ele não pode ser negativo. Mas se você realisticamente espera que dados sem sentido estejam sendo inseridos, sugiro que você investigue a fonte desses dados sem sentido. Porque se eles não podem confiar em inserir dados sensoriais, você também não pode confiar neles para inserir dados corretos .
Se, em vez disso, o salário for calculado pelo seu aplicativo e, de alguma forma, for possível acabar com um número negativo (e correto), então uma abordagem melhor seria fazer
Math.Max(myValue, 0)
com que os números negativos fossem 0, em vez de falhar na validação. Como se sua lógica decidir que o resultado é um número negativo, falhar na validação significa que terá que refazer o cálculo e não há razão para pensar que ele apresentará um número diferente na segunda vez.E se surgir um número diferente, isso levará você a suspeitar que o cálculo não é consistente e, portanto, não pode ser confiável.
Isso não quer dizer que a validação não seja útil. Mas a validação inútil é ruim, tanto porque exige esforço e não soluciona realmente um problema, como proporciona às pessoas uma falsa sensação de segurança.
fonte