Ao criar tipos customizados para a aplicação, é comum utilizarmos os tipos primitivos da linguagem (inteiro, data, decimal, etc.) para especificar os campos que ele deverá conter, podendo trabalhar internamente ou externamente com estas informações. Se não nos atentarmos no momento da criação, é capaz de inflarmos o novo tipo com uma porção de propriedades, que muitas vezes podem estar relacionadas, e prejudicando assim a interface exposta para os consumidores.
Além disso, podemos perder parte do encapsulamento, pois se deixarmos para o consumidor combinar os campos para testar uma condição, pode ser que em algum momento ele se esqueça de avaliar de forma correta e, consequentemente, ter um resultado inesperado. Para exemplificar, considere a classe abaixo:
Note que as propriedades estão divididas nas cores vermelha e azul. As propriedades em vermelho dizem a respeito do vencimento do boleto, enquanto as propriedade em azul se referem ao valor do título. O valor a ser pago pelo título é o valor bruto deduzindo o abatimento e o desconto (desde que esteja sendo pago antes da data limite para o mesmo). Já o vencimento agrupa tudo o que é necessário para indicar a real data de pagamento e/ou a quantidade de dias que falta para vencer ou que está vencido.
Se o consumidor quiser saber a data real de vencimento terá que fazer esta análise; se ele quiser saber o valor real do pagamento (deduzindo os descontos), terá que calcular. E o pior, isso deverá ser replicado para todos os pontos da aplicação que precisar disso. Uma solução seria criar propriedades de somente leitura na classe Boleto e já retornar os valores calculados. Só que fazendo isso, voltamos ao problema inicial: comprometer a interface pública do tipo. Mesmo que estivermos confortáveis em prejudicar a interface, pode ter outros tipos dentro do nosso domínio que precisa também lidar com isso (uma nota promissória, por exemplo, também tem vencimento), e tornamos a recriar tudo isso lá.
Para centralizar as regras, padronizar o consumo e reutilizar quando necessário, o ideal é abrir mão dos tipos primitivos e passar a criar tipos específicos para o nosso domínio, que agrupe os campos e forneça os dados já calculados, para que seja possível o consumo unificado das informações.
Depois disso, a nossa classe Boleto fica bem mais enxuta, e a medida que vamos se aprofundando nas propriedades, chegamos nos mesmos dados, dando mais legibilidade e tendo a mesma riqueza de informações que tínhamos antes.
public class Boleto { public Vencimento Vencimento { get; set; } public Valor Valor { get; set; } } var boleto = new Boleto() { Vencimento = DateTime.Now.AddDays(5), Valor = 1250.29M };
Note que se ajustarmos os operadores (overloading), conseguimos converter tipos primitivos em tipos customizados, conforme é possível ver acima, e tornar ainda mais sucinto o código.
Por fim, os ORMs que temos hoje permitem a serialização de todo o tipo ou de parte dele. Fica a nosso critério o que armazenar, ou seja, se optamos apenas pelos dados que originam o restante ou se armazenamos tudo o que temos, mas não é uma decisão fácil de tomar. Se optar por armazenar tudo, você ocupará mais espaço em disco; em contrapartida, se guardar apenas os dados que originam o restante e utilizar algum outro mecanismo para extração dos mesmo (DAL) sem passar pelo domínio, você não conseguirá reconstruir algumas informações sem replicar a regra na sua camada de leitura.