Usando struct para impor a validação do tipo interno

9

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, structque possui um único private readonlycampo 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 structtipo.

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, implicitpois isso nunca pode falhar, mas a exibição string, explicitpois isso gera valores inválidos, mas é claro que ambos podem ser um implicitou 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 staticmé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?
gmoody1979
fonte
você poderia fazer uma versão modelada que leva um bool (T) lambda
catraca anormal

Respostas:

4

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:

  • Ele permite que um pedaço de código imponha que uma sequência tenha sido validada, sem precisar cuidar dessa validação.
  • Ele permite que você localize o código de validação em um único local. Se um ValidatedNamesempre contém um valor inválido, você sabe que o erro está no IsValidmétodo

Se você acertar o IsValidmétodo, você tem a garantia de que qualquer função que recebe um ValidatedNameestá 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 Handleestrutura e fornecer apenas acesso ao construtor para essa parte do código. Se o código que produz os Handles estiver correto, apenas identificadores válidos serão usados.

Doval
fonte
1

quais são as vantagens e desvantagens do uso desse padrão e por quê?

Bom :

  • É independente. Muitos bits de validação têm gavinhas chegando a lugares diferentes.
  • Ajuda a auto-documentação. Ver um método take ValidatedStringtorna muito mais claro a semântica da chamada.
  • Ajuda a limitar a validação a um ponto, em vez de precisar ser duplicado por métodos públicos.

Ruim :

  • O truque de fundição está oculto. Não é C # idiomático, portanto pode causar confusão ao ler o código.
  • Joga. Ter strings que não atendem à validação não é um cenário excepcional. Fazer IsValidantes do elenco é um pouco desagradável.
  • Não pode dizer por que algo é inválido.
  • O padrão ValidatedStringnão é válido / validado.

Eu já vi esse tipo de coisa com mais frequência Usere AuthenticatedUsertipo de coisa, onde o objeto realmente muda. Pode ser uma boa abordagem, embora pareça deslocada em C #.

Telastyn
fonte
11
Obrigado, acho que o seu quarto "con" é o argumento mais convincente até o momento - usar o padrão ou uma matriz do tipo pode fornecer valores inválidos (dependendo se a string zero / nulo é um valor válido, é claro). Essas são (eu acho) as duas únicas maneiras de acabar com um valor inválido. Mas então, se não estivéssemos usando esse padrão, essas duas coisas ainda nos forneceriam valores inválidos, mas suponho que pelo menos saberíamos que elas precisavam ser validadas. Portanto, isso pode potencialmente invalidar a abordagem em que o valor padrão do tipo subjacente não é válido para o nosso tipo.
precisa saber é o seguinte
Todos os contras são questões de implementação e não problemas com o conceito. Além disso, acho que as "exceções devem ser excepcionais", um conceito confuso e mal definido. A abordagem mais pragmática é fornecer um método baseado em exceção e não baseado em exceção e deixar o chamador escolher.
Doval
@Doval Concordo, exceto conforme indicado no meu outro comentário. O ponto principal do padrão é saber com certeza que, se tivermos um ValidatedName, ele deverá ser válido. Isso será interrompido se o valor padrão do tipo subjacente também não for um valor válido do tipo de domínio. É claro que isso depende do domínio, mas é mais provável que seja o caso (eu teria pensado) para tipos baseados em string do que para tipos numéricos. O padrão funciona melhor onde o padrão do tipo subjacente também é adequado como o padrão do tipo de domínio.
precisa saber é o seguinte
@Doval - eu geralmente concordo. O conceito em si é bom, mas está tentando efetivamente calçar tipos de refinamento em um idioma que não os suporte. Sempre haverá problemas de implementação.
Telastyn
Dito isto, suponho que você possa verificar o valor padrão no elenco "de saída" e em qualquer outro local necessário nos métodos da estrutura e lançar se não for inicializado, mas isso começa a ficar confuso.
precisa saber é o seguinte
0

Seu caminho é bastante pesado e intensivo. Normalmente, defino entidades de domínio como:

public class Institution
{
    private Institution() { }

    public Institution(int organizationId, string name)
    {
        OrganizationId = organizationId;            
        Name = name;
        ReplicationKey = Guid.NewGuid();

        new InstitutionValidator().ValidateAndThrow(this);
    }

    public int Id { get; private set; }
    public string Name { get; private set; }        
    public virtual ICollection<Department> Departments { get; private set; }

    ... other properties    

    public Department AddDepartment(string name)
    {
        var department = new Department(Id, name);
        if (Departments == null) Departments = new List<Department>();
        Departments.Add(department);            
        return department;
    }

    ... other domain operations
}

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:

public class InstitutionValidator : AbstractValidator<Institution>
{
    public InstitutionValidator()
    {
        RuleFor(institution => institution.Name).NotNull().Length(1, 100).WithLocalizedName(() =>   Prim.Mgp.Infrastructure.Resources.GlobalResources.InstitutionName);       
        RuleFor(institution => institution.OrganizationId).GreaterThan(0);
        RuleFor(institution => institution.ReplicationKey).NotNull().NotEqual(Guid.Empty);
    }  
}

Esses validadores também podem ser facilmente reutilizados e você escreve menos código padrão. E outra vantagem é que é legível.

L-Four
fonte
O downvoter se importaria em explicar por que minha resposta foi reduzida?
L-Four
A pergunta era sobre uma estrutura para restringir os tipos de valor, e você alternava para uma classe sem explicar POR QUE. (Não é um downvoter, apenas fazendo uma sugestão.)
DougM
Expliquei por que considero essa uma alternativa melhor e essa foi uma de suas perguntas. Obrigado pela resposta.
L-Four
0

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)nameValuee o novo ValidatedName(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 ToStringsobrecargas 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ê Equalse GetHashCodeestá duplicando esse comportamento padrão. Você pode removê-los e será praticamente a mesma coisa.

Eufórico
fonte
Transmissão: Semanticamente, isso me parece mais a transformação de uma string em um ValidatedName do que a criação de um novo ValidatedName: estamos identificando uma string existente como sendo um ValidatedName. Portanto, para mim, o elenco parece mais correto semanticamente. Concordou que há pouca diferença na digitação (dos dedos na variedade do teclado). Eu discordo sobre o elenco para string: ValidatedName é um subconjunto de corda, de modo que não pode ser uma perda de precisão ...
gmoody1979
ToString: Eu discordo. Para mim, o ToString é um método perfeitamente válido para usar fora dos cenários de depuração, assumindo que ele se encaixa no requisito. Também nessa situação em que um tipo é um subconjunto de outro tipo, acho que faz sentido tornar a capacidade de transformar do subconjunto para o superconjunto o mais fácil possível, para que, se o usuário desejar, possa quase tratá-lo como do tipo super-set, ou seja, string ...
gmoody1979
Equals e GetHashCode: Yes structs usam igualdade estrutural, mas neste caso está comparando a referência de string, não o valor da string. Portanto, precisamos substituir os iguais. Concordo que isso não seria necessário se o tipo subjacente fosse um tipo de valor. Pelo meu entendimento da implementação padrão GetHashCode para tipos de valor (que é bastante limitado), isso fornecerá o mesmo valor, mas terá melhor desempenho. Eu realmente deveria testar se é esse o caso, mas é um pouco de uma questão paralela ao ponto principal da questão. Obrigado pela sua resposta, a propósito :-).
precisa saber é o seguinte
@ gmoody1979 As estruturas são comparadas usando Equals em todos os campos, por padrão. Não deve ser um problema com seqüências de caracteres. Mesmo com GetHashCode. Quanto à estrutura sendo subconjunto de string. Eu gosto de pensar no tipo como rede de segurança. Não quero trabalhar com ValidatedName e, acidentalmente, escorregar para usar a string. Eu preferiria que o compilador me fizesse especificar explicitamente que agora quero trabalhar com dados não verificados.
Euphoric
Desculpe sim, bom ponto sobre iguais. Embora a substituição deva ter um desempenho melhor, dado que o comportamento padrão precisa usar a reflexão para fazer a comparação. Elenco: sim, possivelmente um bom argumento para torná-lo um elenco explícito.
gmoody1979