Primitivo vs Classe para representar um objeto de domínio simples?


14

Quais são as diretrizes gerais ou regras práticas para quando usar um objeto específico do domínio versus uma string ou número simples?

Exemplos:

  • Classe etária vs Inteiro?
  • Classe FirstName vs String?
  • UniqueID vs String
  • Classe PhoneNumber vs String vs Long?
  • Classe DomainName vs String?

Eu acho que a maioria dos profissionais de OOP diria definitivamente aulas específicas para PhoneNumber e DomainName. Quanto mais regras sobre o que os torna válidos e como compará-los, tornam as classes simples mais fáceis e seguras de lidar. Mas para os três primeiros, há mais debate.

Eu nunca me deparei com uma classe "Age", mas alguém poderia argumentar que faz sentido, pois deve ser não negativo (tudo bem, eu sei que você pode argumentar por idades negativas, mas é um bom exemplo que é quase equivalente a um número inteiro primitivo).

String é comum para representar "Nome", mas não é perfeito porque uma String vazia é uma String válida, mas não um nome válido. A comparação geralmente seria feita ignorando o caso. Certamente, existem métodos para verificar se há vazio, fazer comparação sem distinção entre maiúsculas e minúsculas, etc., mas isso exige que o consumidor faça isso.

A resposta depende do ambiente? Estou preocupado principalmente com software corporativo / de alto valor que permanecerá e será mantido por possivelmente mais de uma década.

Talvez eu esteja pensando demais nisso, mas realmente gostaria de saber se alguém tem regras sobre quando escolher classe vs primitiva.


2
A classe de idade não é uma boa ideia se puder ser substituída por um número inteiro. Se for necessária uma data de nascimento no construtor e tiver os métodos getCurrentAge () e getAgeAt (Date date), a classe terá significado. Mas não pode ser substituído por um número inteiro.
Kiwiron #

5
"Às vezes, uma string não é uma string. Modele de acordo." Existe um bom post de Mark Seemann sobre 'obsessão primitiva': blog.ploeh.dk/2015/01/19/…
Serhii Shushliapin

4
Na verdade, uma classe de idade faz algum sentido de uma maneira estranha. A definição ocidental comum de Idade é zero no nascimento e adicione 1 no seu próximo aniversário. A metodologia do Leste Asiático é que você é 1 no nascimento e 1 é adicionado no próximo dia de Ano Novo. Assim, dependendo da sua localidade, sua idade pode ser calculada de maneira diferente. EG de en.wikipedia.org/wiki/East_Asian_age_reckoning people may be one or two years older in Asian reckoning than in the western age system
Peter M

@ PeterM, é uma boa informação! Datas / Horários e agora idades. Eles devem ser conceitos simples, mas isso prova que há muito mais complexidade no mundo do que estamos acostumados.
Machado

6
Vejo muitos escritos abaixo desta pergunta, mas a resposta não é realmente tão complicada. Você usa uma Ageclasse em vez de um número inteiro quando precisa do comportamento adicional que essa classe forneceria.
Robert Harvey

Respostas:


20

Quais são as diretrizes gerais ou regras práticas para quando usar um objeto específico do domínio versus uma string ou número simples?

A orientação geral é que você deseja modelar seu domínio em um idioma específico do domínio.

Considere: por que usamos número inteiro? Podemos representar todos os números inteiros com seqüências de caracteres com a mesma facilidade. Ou com bytes.

Se estivéssemos programando em uma linguagem independente de domínio que incluísse tipos primitivos para número inteiro e idade, qual você escolheria?

O que realmente se resume é que a natureza "primitiva" de certos tipos é um acidente da escolha da linguagem para nossa implementação.

Os números, em particular, geralmente requerem contexto adicional. A idade não é apenas um número, mas também possui dimensões (tempo), unidades (anos?), Regras de arredondamento! A adição de idades faz sentido de uma maneira que a adição de uma idade a um dinheiro não.

Distinguir os tipos nos permite modelar as diferenças entre um endereço de email não verificado e um endereço de email verificado .

O acidente de como esses valores são representados na memória é uma das partes menos interessantes. O modelo de domínio não se importa com um CustomerIdnó int, ou String, UUID / GUID ou JSON. Ele só quer as possibilidades.

Realmente nos importamos se números inteiros são big endian ou little endian? Nós nos importamos se o que Listnós passamos é uma abstração sobre uma matriz ou um gráfico? Quando descobrimos que a aritmética de precisão dupla é ineficiente e que precisamos mudar para uma representação de ponto flutuante, o modelo de domínio deve se importar ?

Parnas, em 1972 , escreveu

Em vez disso, propomos que se comece com uma lista de decisões de design difíceis ou que provavelmente mudarão. Cada módulo é projetado para ocultar tal decisão dos outros.

Em certo sentido, os tipos de valores específicos do domínio que introduzimos são módulos que isolam nossa decisão sobre qual representação subjacente dos dados deve ser usada.

Portanto, a vantagem é a modularidade - obtemos um design em que é mais fácil gerenciar o escopo de uma mudança. A desvantagem é o custo - é mais trabalhoso criar os tipos personalizados de que você precisa, para escolher os tipos corretos, é necessário adquirir um entendimento mais profundo do domínio. A quantidade de trabalho necessária para criar o módulo de valor dependerá do seu dialeto local do Blub .

Outros termos na equação podem incluir o tempo de vida útil esperado da solução (modelagem cuidadosa para o script ware que será executado quando o retorno do investimento for ruim), quão próximo o domínio está da competência principal do negócio.

Um caso especial que podemos considerar é o da comunicação através de um limite . Não queremos estar em uma situação em que as alterações em uma unidade implantável exijam alterações coordenadas com outras unidades implantáveis. Portanto, as mensagens tendem a se concentrar mais nas representações, sem considerar invariantes ou comportamentos específicos do domínio. Não tentaremos comunicar "esse valor deve ser estritamente positivo" no formato da mensagem, mas sim comunicar sua representação na ligação e aplicar a validação a essa representação no limite do domínio.


Ponto excelente sobre esconder (ou abstrair) aquelas coisas que são difíceis e que provavelmente mudarão.
user949300

4

Você está pensando em abstração, e isso é ótimo. No entanto, as coisas que sua listagem me parecem ser principalmente atributos individuais de algo maior (não é a mesma coisa, é claro).

O que eu acho mais importante é fornecer abstração que agrupe atributos e forneça a eles um lar adequado, como uma classe Person, para que o programador cliente não precise lidar com vários atributos, mas com uma abstração de nível superior.

Depois de ter essa abstração, você não precisa necessariamente de abstrações adicionais para atributos que são apenas valores. A classe Person pode conter uma Data de Nascimento como uma Data (primitiva), juntamente com PhoneNumbers como lista de seqüências de caracteres (primitivas) e um Nome como uma sequência de caracteres (primitiva).

Se você tiver um número de telefone com um código de formatação, agora existem dois atributos e mereceria uma classe para o número de telefone (como provavelmente também teria algum comportamento, como obter apenas números versus obter número de telefone formatado).

Se você tiver um Nome como vários atributos (por exemplo, prefixos / títulos, primeiro, último, sufixo) que mereceriam uma classe (como agora você tem alguns comportamentos, como obter o nome completo como uma string vs. obter pedaços menores, título etc. .).


1

Você deve modelar conforme necessário, se você quiser apenas alguns dados, pode usar tipos primitivos, mas se quiser fazer mais operações com esses dados, ele deve ser modelado em classes específicas, por exemplo (concordo com @kiwiron em seu comentário) e A classe de idade não tem sentido, mas seria melhor substituí-la por uma classe Data de nascimento e você coloca esses métodos lá.

O benefício de modelar esses conceitos nas classes é que você reforça a riqueza do seu modelo; por exemplo, em vez de ter um método que aceita qualquer Data, o usuário pode preenchê-lo com qualquer data, data de início da Segunda Guerra Mundial ou data de término da Guerra Fria, destas datas ainda uma data de nascimento válida. você pode substituí-lo por Data de nascimento. Nesse caso, você aplica seu método para aceitar uma data de nascimento para não aceitar nenhuma data.

Para o Primeiro nome, não vejo muitos benefícios, UniqueID também, PhoneNumber um pouco, você pode solicitar o país e a região, por exemplo, porque geralmente PhoneNumber se refere a países e regiões, algumas operadoras usam sufixos de número predefinido para classificar o Mobile , Office ... números, para que você possa adicionar esses métodos a essa classe, o mesmo para DomainName.

Para obter mais detalhes, recomendo que você dê uma olhada em Value Objects no DDD (Domain Driven Design).


1

O que depende é se você tem um comportamento (que você precisa manter e / ou alterar) associado a esses dados ou não.

Em resumo, se são apenas alguns dados que são lançados sem muito comportamento associado a eles, use um tipo primitivo ou, para dados um pouco mais complexos, uma estrutura de dados (em grande parte sem comportamento) (por exemplo, uma classe com apenas getters e setters).

Se, por outro lado, você descobrir que, para tomar decisões, está verificando ou manipulando esses dados em vários locais do seu código, os locais geralmente não estão próximos um do outro - então transforme-os em um objeto adequado, definindo uma classe e colocando o comportamento relevante dentro dele como métodos. Se, por algum motivo, você achar que não faz sentido definir essa classe, uma alternativa é consolidar esse comportamento em algum tipo de classe de "serviço" que opera com esses dados.


1

Seus exemplos se parecem muito com propriedades ou atributos de outra coisa. O que isso significa é que seria perfeitamente razoável começar apenas com o primitivo. Em algum momento, você precisará usar um primitivo, por isso costumo economizar complexidade para quando realmente precisar.

Por exemplo, digamos que estou fazendo um jogo e não preciso me preocupar com personagens envelhecidos. Usar um número inteiro para isso faz todo o sentido. A idade pode ser usada em verificações simples para ver se o personagem pode fazer algo ou não.

Como outros salientaram, a idade pode ser um assunto complicado, dependendo do seu local. Se você precisar reconciliar idades em diferentes locais etc., faz todo o sentido introduzir uma Ageclasse que tenha DateOfBirtheLocale como propriedades. Essa classe de idade também pode calcular a idade atual em qualquer timestamp.

Portanto, existem razões para promover uma primitiva para uma classe, mas elas são muito específicas para aplicativos. aqui estão alguns exemplos:

  • Você tem uma lógica de validação especial que deve ser aplicada em qualquer lugar em que o tipo de informação é usado (por exemplo, números de telefone, endereços de email).
  • Você tem uma lógica de formatação especial usada para renderizar para humanos lerem (por exemplo, endereços IP4 e IP6)
  • Você tem um conjunto de funções relacionadas a esse tipo de objeto
  • Você tem regras especiais para comparar tipos de valor semelhantes

Basicamente, se você possui várias regras sobre como um valor deve ser tratado e deseja ser usado de forma consistente em todo o projeto, crie uma classe. Se você pode viver com valores simples, porque não é realmente crítico para a funcionalidade, use um primitivo.


1

No final do dia, um objeto é apenas uma testemunha de que algum processo realmente foi executado .

Uma primitiva intsignifica que algum processo zerou 4 bytes de memória e depois inverteu os bits necessários para representar algum número inteiro no intervalo de -2.147.483.648 a 2.147.483.647; o que não é uma afirmação forte sobre o conceito de idade. Da mesma forma, a (não nulo) System.Stringsignifica que alguns bytes foram alocados e os bits foram invertidos para representar uma sequência de caracteres Unicode com relação a alguma codificação.

Portanto, ao decidir como representar seu objeto de domínio, pense no processo para o qual ele serve como testemunha. Se FirstNamerealmente deve ser uma seqüência de caracteres não vazia, deve-se testemunhar que o sistema executou algum processo que garante que pelo menos um caractere que não seja de espaço em branco tenha sido criado em uma sequência de caracteres que foram entregues a você.

Da mesma forma, um Ageobjeto provavelmente deve ser uma testemunha de que algum processo calculou a diferença entre duas datas. Como intsgeralmente não resultam do cálculo da diferença entre duas datas, são ipso facto insuficientes para representar as idades.

Portanto, é bastante óbvio que você quase sempre deseja criar uma classe (que é apenas um programa) para cada objeto de domínio. Então qual é o problema? O problema é que C # (que eu amo) e Java exigem muita cerimônia na criação de classes. Mas, podemos imaginar sintaxes alternativas que tornariam a definição de objetos de domínio muito mais simples:

//hypothetical syntax
class Age = |start:date - end:date|.Years

(* ML-like syntax *)
type Age(x, y) = datediff(year, x, y)

//C#-like syntax with primary constructors and expression-bodied classes
class Age(DateTime x, DateTime y) => implicit operator int (Age a) => Abs((y - x).Years);

O F #, por exemplo, na verdade possui uma boa sintaxe na forma de Padrões Ativos :

//Impossible to produce an `Age` without first computing the difference of two dates
//But, any pair (tuple) of dates is implicitly converted to an `Age` when needed.

let (|Age|) (x, y) =  (date_diff y x).Days / 365

O ponto é que, apenas porque sua linguagem de programação torna difícil definir objetos de domínio, não significa que seja realmente uma má idéia fazê-lo.


† Também chamamos essas coisas de condições de pós , mas prefiro o termo "testemunha", pois serve como prova de que algum processo pode e foi executado.


0

Pense no que você obtém ao usar uma classe versus um primitivo. Se usar uma classe pode levá-lo

  • validação
  • formatação
  • outros métodos úteis
  • digite safety (ou seja, com uma PhoneNumberclasse, deve ser impossível atribuí-lo a uma sequência ou uma sequência aleatória (por exemplo, "pepino") em que espero um número de telefone)
  • quaisquer outros benefícios

e esses benefícios facilitam sua vida, melhoram a qualidade do código, a legibilidade e superam a dor de usar essas coisas. Caso contrário, fique com os primitivos. Se estiver perto, faça um julgamento.

Perceba também que, às vezes, uma boa nomeação pode lidar com isso (por exemplo, uma string denominada firstNamevs. fazer uma classe inteira que apenas envolve uma propriedade de string). As aulas não são a única maneira de resolver as coisas.

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.