Os objetos de domínio comumente têm propriedades que podem ser representadas por um tipo interno, mas cujos valores válidos são um subconjunto dos valores que podem ser representados por esse tipo.
Nesses casos, o valor pode ser armazenado usando o tipo interno, mas é necessário garantir que os valores sejam sempre validados no ponto de entrada, caso contrário, podemos acabar trabalhando com um valor inválido.
Uma maneira de resolver isso é armazenar o valor como um costume, struct
que possui um único private readonly
campo de apoio do tipo interno e cujo construtor valida o valor fornecido. Em seguida, sempre podemos ter certeza de usar apenas valores validados usando esse struct
tipo.
Também podemos fornecer operadores de conversão de e para o tipo interno subjacente, para que os valores possam entrar e sair sem problemas como o tipo subjacente.
Tome como exemplo uma situação em que precisamos representar o nome de um objeto de domínio, e valores válidos são qualquer sequência que tenha entre 1 e 255 caracteres, inclusive. Podemos representar isso usando a seguinte estrutura:
public struct ValidatedName : IEquatable<ValidatedName>
{
private readonly string _value;
private ValidatedName(string name)
{
_value = name;
}
public static bool IsValid(string name)
{
return !String.IsNullOrEmpty(name) && name.Length <= 255;
}
public bool Equals(ValidatedName other)
{
return _value == other._value;
}
public override bool Equals(object obj)
{
if (obj is ValidatedName)
{
return Equals((ValidatedName)obj);
}
return false;
}
public static implicit operator string(ValidatedName x)
{
return x.ToString();
}
public static explicit operator ValidatedName(string x)
{
if (IsValid(x))
{
return new ValidatedName(x);
}
throw new InvalidCastException();
}
public static bool operator ==(ValidatedName x, ValidatedName y)
{
return x.Equals(y);
}
public static bool operator !=(ValidatedName x, ValidatedName y)
{
return !x.Equals(y);
}
public override int GetHashCode()
{
return _value.GetHashCode();
}
public override string ToString()
{
return _value;
}
}
O exemplo mostra a exibição string
, implicit
pois isso nunca pode falhar, mas a exibição string
, explicit
pois isso gera valores inválidos, mas é claro que ambos podem ser um implicit
ou outro explicit
.
Observe também que só é possível inicializar essa estrutura por meio de uma conversão de string
, mas é possível testar se essa conversão falhará antecipadamente usando o IsValid
static
método
Esse parece ser um bom padrão para impor a validação de valores de domínio que podem ser representados por tipos simples, mas não o vejo usado com frequência ou sugerido e estou interessado no motivo.
Então, minha pergunta é: quais são as vantagens e desvantagens do uso desse padrão e por quê?
Se você acha que esse é um padrão ruim, eu gostaria de entender por que e também o que você acha que é a melhor alternativa.
Nota : originalmente eu fiz essa pergunta no Stack Overflow, mas ela foi colocada em espera principalmente como baseada em opiniões (ironicamente subjetiva em si mesma) - espero que possa ter mais sucesso aqui.
Acima está o texto original, abaixo mais algumas reflexões, em parte em resposta às respostas recebidas antes da suspensão:
- Um dos principais pontos apontados pelas respostas foi a quantidade de código da placa da caldeira necessária para o padrão acima, especialmente quando muitos desses tipos são necessários. No entanto, em defesa do padrão, isso pode ser amplamente automatizado usando modelos e, na verdade, para mim não parece tão ruim assim, mas essa é apenas a minha opinião.
- Do ponto de vista conceitual, não parece estranho quando se trabalha com uma linguagem fortemente tipada como C # aplicar apenas o princípio fortemente tipado a valores compostos, em vez de estendê-lo a valores que podem ser representados por uma instância de um tipo embutido?
Respostas:
Isso é bastante comum em linguagens de estilo ML, como Standard ML / OCaml / F # / Haskell, onde é muito mais fácil criar os tipos de wrapper. Ele oferece dois benefícios:
ValidatedName
sempre contém um valor inválido, você sabe que o erro está noIsValid
métodoSe você acertar o
IsValid
método, você tem a garantia de que qualquer função que recebe umValidatedName
está de fato recebendo um nome validado.Se você precisar fazer manipulações de string, poderá adicionar um método público que aceite uma função que aceite uma String (o valor de
ValidatedName
) e retorne uma String (o novo valor) e valide o resultado da aplicação da função. Isso elimina o clichê de obter o valor subjacente de String e envolvê-lo novamente.Um uso relacionado para agrupar valores é rastrear sua proveniência. Por exemplo, as APIs do SO baseadas em C às vezes fornecem identificadores de recursos como números inteiros. Você pode agrupar as APIs do SO para, em vez disso, usar uma
Handle
estrutura e fornecer apenas acesso ao construtor para essa parte do código. Se o código que produz osHandle
s estiver correto, apenas identificadores válidos serão usados.fonte
Bom :
ValidatedString
torna muito mais claro a semântica da chamada.Ruim :
IsValid
antes do elenco é um pouco desagradável.ValidatedString
não é válido / validado.Eu já vi esse tipo de coisa com mais frequência
User
eAuthenticatedUser
tipo de coisa, onde o objeto realmente muda. Pode ser uma boa abordagem, embora pareça deslocada em C #.fonte
Seu caminho é bastante pesado e intensivo. Normalmente, defino entidades de domínio como:
No construtor da entidade, a validação é acionada usando o FluentValidation.NET, para garantir que você não possa criar uma entidade com estado inválido. Observe que as propriedades são todas somente leitura - você pode defini-las apenas através do construtor ou operações de domínio dedicadas.
A validação desta entidade é uma classe separada:
Esses validadores também podem ser facilmente reutilizados e você escreve menos código padrão. E outra vantagem é que é legível.
fonte
Eu gosto dessa abordagem para tipos de valor. O conceito é ótimo, mas tenho algumas sugestões / reclamações sobre a implementação.
Elenco : não gosto do uso de elenco nesse caso. O elenco explícito da cadeia de caracteres não é um problema, mas não há muita diferença entre
(ValidatedName)nameValue
e o novoValidatedName(nameValue)
. Então parece meio desnecessário. A conversão implícita de string é o pior problema. Eu acho que a obtenção do valor real da string deve ser mais explícito, porque pode ser atribuído acidentalmente à string e o compilador não avisará sobre a possível "perda de precisão". Esse tipo de perda de precisão deve ser explícito.ToString : Prefiro usar
ToString
sobrecargas apenas para fins de depuração. E não acho que devolver o valor bruto seja uma boa ideia. Esse é o mesmo problema da conversão implícita em string. Obter o valor interno deve ser uma operação explícita. Eu acredito que você está tentando fazer a estrutura se comportar como uma string normal para o código externo, mas acho que, ao fazer isso, você está perdendo parte do valor que obtém ao implementar esse tipo de tipo.Equals e GetHashCode : Structs estão usando a igualdade estrutural por padrão. Portanto, você
Equals
eGetHashCode
está duplicando esse comportamento padrão. Você pode removê-los e será praticamente a mesma coisa.fonte