Quando a obsessão primitiva não é um cheiro de código?


22

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:

  1. 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.

  2. 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:

  1. Data de nascimento: verifique se é maior que mindate e menor que a data de hoje.

  2. 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.


8
"Você estaria violando o princípio DRY, colocando essa lógica de validação em todos os lugares onde um código postal é usado." Isso não é verdade. A validação deve ser feita assim que os dados forem inseridos no seu módulo . Se houver mais do que um "ponto de entrada" a validação deve estar em uma unidade reutilizável , e que não precisa ser (nem deve ser) o DTO ...
Timothy Truckle

1
Como você está dando "mindate" e "date de hoje" ao DateOfBirthconstrutor para que ele verifique?
Caleth

11
Outro benefício da criação de tipos personalizados é a segurança do tipo. Se você tem Salarye Distanceobjeto que você não pode acidentalmente utilizá-los alternadamente. Você pode se ambos forem do tipo double.
precisa saber é o seguinte

3
@ w0051977 A sua declaração (como eu a entendi) implicava que qualquer outra coisa além de ter a validação no construtor de DTOs violaria o DRY. Na verdade, a validação deve estar fora do DTO ...
Timothy Truckle

2
Para mim, é tudo uma questão de escopo. Se você der aos primitivos um amplo escopo, existem inúmeras maneiras pelas quais eles podem ser mal utilizados e manipulados. Portanto, geralmente você deseja dar a eles um escopo mais restrito, e uma maneira de fazer isso é projetar uma classe representando um conceito usando um primitivo, armazenado em particular como interno, para implementá-lo. Agora, o escopo da primitiva é restrito e é improvável que seja usado de maneira inadequada / incorreta e você pode efetivamente manter os invariantes. Mas se o escopo da primitiva era estreito para começar, isso poderia ser um exagero e introduzir muito acoplamento e código extras para manter.

Respostas:


17

O Obsession primitivo está usando tipos de dados primitivos para representar idéias de domínio.

O oposto seria "modelagem de domínio" ou talvez "engenharia excedente".

Você criaria um objeto DateOfBirth e um objeto Salary?

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, DateOfBirthnã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.

Existe uma regra que descreva quando e quando não remover a obsessão primitiva ou você sempre deve fazer isso, se possível.

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 .


1
O primeiro comentário após a citação no topo parece não ser sequitur. Além disso, apenas reafirma o assunto da questão. Caso contrário, é uma boa resposta, mas acho isso realmente perturbador.
JimmyJames 02/02

nem C # DateTime nem java.util.Date são tipos subjacentes apropriados para DateOfBirth.
precisa

Talvez substituir java.util.Datecomjava.time.LocalDate
Koray Tugay

7

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.


Um alias de tipo é uma boa alternativa?
w0051977 31/01

@ w0051977 eu concordo com charonx e tipo de alias poderia ser uma alternativa
techagrammer

@ w0051977 um alias de tipo pode ser uma alternativa se o objetivo principal for forçar a digitação estrita, declarar explicitamente qual é um determinado valor (salário) para evitar uma atribuição acidental de "dólares flutuantes" (por hora? semana? mês?) a "salário flutuante" (por mês? ano?). Realmente depende de quais são suas necessidades.
CharonX

@CharonX, acredito que um decimal deve ser usado para um salário e não um float. Você concorda?
W0051977

@ w0051977 Se você tem um bom tipo decimal, esse seria preferível, sim. (Eu estou trabalhando em projeto de um C ++ agora, então booleanos, inteiros e carros alegóricos estão na vanguarda da minha mente)
CharonX

5

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 queSalary seja >=0, e mais tarde decidir proibir $100.33333e qualquer coisa acima $10,000,000(o que levaria à falência do cliente). O uso de Nonnegativeprimitivo para representar Salarye 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 inchados class EmployeeDataesconderiam os campos obrigatórios entre outros. A assinatura em digamos calcPension(d: (Salary, DateOfBirth))é mais focada do que em calcPension(d: EmployeeData), o que viola o Princípio de Segregação de Interface. Da mesma forma, um especialista class SalaryAndDateOfBirthparece 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


+1 para todas as referências.
W0051977

4

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.


3

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.


Um alias de tipo é uma boa alternativa?
w0051977 31/01

2

(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:

htmlEntityEncode(string value)

Use objeto para itens como:

numberOfDaysSinceUnixEpoch(SimpleDate value)

O último exemplo tem regras no mesmo, ou seja, o objecto SimpleDateé composta de Year, Monthe Day. Através do uso de Object neste caso, o conceito de SimpleDateser válido pode ser encapsulado dentro do objeto.


1

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:

int leaveId = 12345;
int submitterId = 23456;
int approverId = 34567;

SubmitLeaveApplication(leaveId, approverId, submitterId);

public void SubmitLeaveApplication(int leaveId, int submitterId, int approverId) {
  // implementation here
}

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:

LeaveId leaveId = 12345;
SubmitterId submitterId = 23456;
ApproverId approverId = 34567;

SubmitLeaveApplication(leaveId, approverId, submitterId);

public void SubmitLeaveApplication(LeaveId leaveId, SubmitterId submitterId, ApproverId approverId) {
  // implementation here
}

Imagine que o método escalou até 10 ou mais parâmetros, todos intos 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.


0

Você estaria violando o princípio DRY, colocando essa lógica de validação em todos os lugares onde um código postal é usado.

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 seuZipCode 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)?

  • Se o fizer, você também está violando o DRY. Mesmo que você não a chame de violação DRY (porque cada instância tem uma finalidade diferente), ainda assim é desnecessário ocupar memória extra, além de abrir a porta para erros quando os valores dos dois países forem diferentes (o que eles logicamente nunca deveriam estar).
    • Ou, como alternativa, você precisará sincronizar os dois pontos de dados para garantir que eles sejam sempre os mesmos, o que sugere que você realmente deve armazenar esses dados em um único ponto de qualquer maneira, derrotando o objetivo.
  • Caso contrário, não é uma ZipCodeclasse, mas uma Addressclasse, que novamente conterá uma string ZipCodeque significa que fizemos um círculo completo.

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.

O benefício é que você pode falar sobre eles ao descrever o modelo de domínio.

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 intou como string. 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 ZipCodeobjeto 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)

Data de nascimento: verifique se é maior que mindate e menor que a data de hoje.
Salário: verifique se é maior ou igual a zero.

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.MinDatemas permitir DateTime.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 é:

  • 1 de janeiro de 1978
  • 1 de janeiro de 1722
  • 1 de janeiro de 2355

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.


A data de nascimento de alguém pode realmente ultrapassar a data atual, se um bebê nascer agora em um fuso horário que já passou para o dia seguinte. E um hospital pode armazenar as "datas esperadas de nascimento" em um banco de dados que poderá levar meses no futuro. Você gostaria de um tipo diferente para isso?
gnasher729 6/06

@ gnasher729: Não tenho muita certeza de que sigo, parece que você está concordando comigo (a validação é contextual e não universalmente correta), mas o fraseado de seu comentário sugere que você discorda. Ou estou interpretando mal?
Flater
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.