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.
fonte
people may be one or two years older in Asian reckoning than in the western age system
Age
classe em vez de um número inteiro quando precisa do comportamento adicional que essa classe forneceria.Respostas:
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
CustomerId
nó 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
List
nó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 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.
fonte
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. .).
fonte
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).
fonte
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.
fonte
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
Age
classe que tenhaDateOfBirth
eLocale
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:
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.
fonte
No final do dia, um objeto é apenas uma testemunha de que algum processo realmente foi executado † .
Uma primitiva
int
significa 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.String
significa 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
FirstName
realmente 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
Age
objeto provavelmente deve ser uma testemunha de que algum processo calculou a diferença entre duas datas. Comoints
geralmente 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:
O F #, por exemplo, na verdade possui uma boa sintaxe na forma de Padrões Ativos :
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.
fonte
Pense no que você obtém ao usar uma classe versus um primitivo. Se usar uma classe pode levá-lo
PhoneNumber
classe, 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)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
firstName
vs. fazer uma classe inteira que apenas envolve uma propriedade de string). As aulas não são a única maneira de resolver as coisas.fonte