Uso de struct para exigir la validación del tipo incorporado

9

Comúnmente, los objetos de dominio tienen propiedades que pueden ser representadas por un tipo incorporado pero cuyos valores válidos son un subconjunto de los valores que pueden ser representados por ese tipo.

En estos casos, el valor puede almacenarse utilizando el tipo incorporado, pero es necesario asegurarse de que los valores siempre se validan en el punto de entrada, de lo contrario podríamos terminar trabajando con un valor no válido.

Una forma de resolver esto es almacenar el valor como una costumbre structque tiene un solo private readonlycampo de respaldo del tipo incorporado y cuyo constructor valida el valor proporcionado. Entonces siempre podemos estar seguros de usar solo valores validados al usar este structtipo.

También podemos proporcionar operadores de conversión desde y hacia el tipo incorporado subyacente para que los valores puedan ingresar y salir sin problemas como el tipo subyacente.

Tome como ejemplo una situación en la que necesitamos representar el nombre de un objeto de dominio, y los valores válidos son cualquier cadena que tenga entre 1 y 255 caracteres de longitud inclusive. Podríamos representar esto usando la siguiente estructura:

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;
    }
}

El ejemplo muestra el stringlanzamiento como implicitesto nunca puede fallar, pero el stringlanzamiento explicitcomo esto arrojará valores no válidos, pero, por supuesto, ambos podrían ser implicito explicit.

Tenga en cuenta también que solo se puede inicializar esta estructura a través de un lanzamiento desde string, pero se puede probar si dicho lanzamiento fallará de antemano utilizando el IsValid staticmétodo.

Este parece ser un buen patrón para exigir la validación de los valores de dominio que pueden representarse mediante tipos simples, pero no veo que se use con frecuencia o se sugiera y me interesa saber por qué.

Entonces mi pregunta es: ¿cuáles considera que son las ventajas y desventajas de usar este patrón y por qué?

Si cree que este es un mal patrón, me gustaría entender por qué y también cuál es la mejor alternativa.

NB : Originalmente hice esta pregunta en Stack Overflow, pero se suspendió porque se basa principalmente en la opinión (irónicamente subjetiva en sí misma), espero que pueda tener más éxito aquí.

Arriba está el texto original, debajo de un par de pensamientos más, en parte en respuesta a las respuestas recibidas allí antes de que se suspendiera:

  • Uno de los puntos principales de las respuestas fue la cantidad de código de placa de caldera necesaria para el patrón anterior, especialmente cuando se requieren muchos de estos tipos. Sin embargo, en defensa del patrón, esto podría automatizarse en gran medida usando plantillas y, en realidad, para mí no parece tan malo de todos modos, pero esa es solo mi opinión.
  • Desde un punto de vista conceptual, no parece extraño cuando se trabaja con un lenguaje fuertemente tipado como C # para aplicar solo el principio fuertemente tipado a valores compuestos, en lugar de extenderlo a valores que pueden ser representados por una instancia de un tipo incorporado?
gmoody1979
fuente
podrías hacer una versión con plantilla que tome un bool (T) lambda
freak de trinquete

Respuestas:

4

Esto es bastante común en lenguajes de estilo ML como Standard ML / OCaml / F # / Haskell, donde es mucho más fácil crear los tipos de envoltura. Le proporciona dos beneficios:

  • Permite que una parte del código imponga que una cadena ha sido validada, sin tener que ocuparse de esa validación.
  • Le permite localizar el código de validación en un solo lugar. Si alguna ValidatedNamevez contiene un valor no válido, sabe que el error está en el IsValidmétodo.

Si obtiene el IsValidmétodo correcto, tiene la garantía de que cualquier función que recibe un ValidatedNamede hecho está recibiendo un nombre validado.

Si necesita manipular cadenas, puede agregar un método público que acepte una función que tome una Cadena (el valor de ValidatedName) y devuelva una Cadena (el nuevo valor) y valide el resultado de aplicar la función. Eso elimina la rutina de obtener el valor de cadena subyacente y volver a envolverlo.

Un uso relacionado para ajustar los valores es rastrear su procedencia. Por ejemplo, las API de SO basadas en C a veces proporcionan identificadores de recursos como enteros. Puede ajustar las API del sistema operativo para utilizar una Handleestructura y solo proporcionar acceso al constructor a esa parte del código. Si el código que produce el Handles es correcto, solo se utilizarán identificadores válidos.

Doval
fuente
1

¿Cuáles considera que son las ventajas y desventajas de usar este patrón y por qué?

buena :

  • Es independiente. Demasiados bits de validación tienen zarcillos que llegan a diferentes lugares.
  • Ayuda a la autodocumentación. Ver un método tomar un ValidatedStringhace que sea mucho más claro acerca de la semántica de la llamada.
  • Ayuda a limitar la validación a un punto en lugar de tener que duplicarse en todos los métodos públicos.

Malo :

  • El truco de lanzamiento está oculto. No es C # idiomático, por lo que puede causar confusión al leer el código.
  • Se tira. Tener cadenas que no cumplan con la validación no es un escenario excepcional. Hacer IsValidantes del reparto es un poco extraño.
  • No puede decirte por qué algo no es válido.
  • El valor predeterminado ValidatedStringno es válido / validado.

He visto este tipo de cosas más a menudo con Usery AuthenticatedUsertipo de cosas, donde el objeto realmente cambia. Puede ser un buen enfoque, aunque parece fuera de lugar en C #.

Telastyn
fuente
1
Gracias, creo que su cuarta "estafa" es el argumento más convincente en su contra: usar el valor predeterminado o una matriz del tipo podría proporcionarle valores no válidos (dependiendo de si la cadena cero / nula es un valor válido, por supuesto). Estas son (creo) las dos únicas formas de terminar con un valor no válido. Pero entonces, si NO ESTAMOS utilizando este patrón, estas dos cosas aún nos darían valores no válidos, pero supongo que al menos sabríamos que debían validarse. Por lo tanto, esto podría invalidar el enfoque donde el valor predeterminado del tipo subyacente no es válido para nuestro tipo.
gmoody1979
Todos los inconvenientes son problemas de implementación en lugar de problemas con el concepto. Además, encuentro que las "excepciones deberían ser excepcionales", un concepto difuso y mal definido. El enfoque más pragmático es proporcionar un método basado en excepciones y no basado en excepciones y dejar que la persona que llama elija.
Doval
@Doval Estoy de acuerdo, excepto como se señaló en mi otro comentario. El objetivo del patrón es saber con certeza que si tenemos un ValidatedName, debe ser válido. Esto se desglosa si el valor predeterminado del tipo subyacente no es también un valor válido del tipo de dominio. Por supuesto, esto depende del dominio, pero es más probable que sea el caso (habría pensado) para los tipos basados ​​en cadenas que para los tipos numéricos. El patrón funciona mejor cuando el valor predeterminado del tipo subyacente también es adecuado como el predeterminado del tipo de dominio.
gmoody1979
@Doval: generalmente estoy de acuerdo. El concepto en sí está bien, pero está tratando efectivamente de calzar los tipos de refinamiento en un lenguaje que no los admite. Siempre habrá problemas de implementación.
Telastyn
Dicho esto, supongo que puede verificar el valor predeterminado en el reparto "saliente" y en cualquier otro lugar necesario dentro de los métodos de la estructura y lanzar si no se inicializa, pero eso comienza a complicarse.
gmoody1979
0

Tu camino es bastante pesado e intenso. Normalmente defino entidades de dominio 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
}

En el constructor de la entidad, la validación se activa usando FluentValidation.NET, para asegurarse de que no pueda crear una entidad con un estado no válido. Tenga en cuenta que las propiedades son de solo lectura: solo puede establecerlas a través del constructor o las operaciones de dominio dedicado.

La validación de esta entidad es una clase 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);
    }  
}

Estos validadores también pueden reutilizarse fácilmente, y usted escribe menos código repetitivo. Y otra ventaja es que es legible.

L-cuatro
fuente
¿Al votante le importaría explicar por qué mi respuesta fue rechazada?
L-Four
La pregunta se refería a una estructura para restringir los tipos de valor, y usted cambió a una clase sin explicar POR QUÉ. (No es un votante negativo, solo hace una sugerencia.)
DougM
Le expliqué por qué considero que esta es una mejor alternativa, y esta fue una de sus preguntas. Gracias por la respuesta.
L-Four
0

Me gusta este enfoque de los tipos de valor. El concepto es genial, pero tengo algunas sugerencias / quejas sobre la implementación.

Casting : no me gusta el uso de casting en este caso. La conversión explícita de cadenas no es un problema, pero no hay mucha diferencia entre (ValidatedName)nameValuey nuevo ValidatedName(nameValue). Entonces parece un poco innecesario. La conversión implícita a cadenas es el peor problema. Creo que obtener el valor real de la cadena debería ser más explícito, porque podría asignarse accidentalmente a la cadena y el compilador no le advertirá sobre la posible "pérdida de precisión". Este tipo de pérdida de precisión debe ser explícito.

ToString : prefiero usar ToStringsobrecargas solo para fines de depuración. Y no creo que devolver el valor bruto sea una buena idea. Este es el mismo problema que con la conversión implícita a cadenas. Obtener el valor interno debe ser una operación explícita. Creo que está tratando de hacer que la estructura se comporte como una cadena normal para el código externo, pero creo que al hacerlo, está perdiendo parte del valor que obtiene al implementar este tipo de tipo.

Equals y GetHashCode : las estructuras utilizan la igualdad estructural de forma predeterminada. Entonces tu Equalsy GetHashCodeestás duplicando este comportamiento predeterminado. Puedes eliminarlos y será más o menos lo mismo.

Eufórico
fuente
Casting: Semánticamente, esto me parece más como la transformación de una cadena en un ValidatedName en lugar de la creación de un nuevo ValidatedName: estamos identificando una cadena existente como ValidatedName. Por lo tanto, el elenco me parece más correcto semánticamente. De acuerdo, hay poca diferencia en la escritura (de los dedos en la variedad de teclado). No estoy de acuerdo con el reparto de cadenas: ValidatedName es un subconjunto de cadenas, por lo que nunca puede haber una pérdida de precisión ...
gmoody1979
ToString: No estoy de acuerdo. Para mí ToString es un método perfectamente válido para usar fuera de los escenarios de depuración, suponiendo que cumpla con el requisito. También en esta situación en la que un tipo es un subconjunto de otro tipo, creo que tiene sentido hacer que la capacidad de transformar del subconjunto al superconjunto sea lo más fácil posible, de modo que si el usuario lo desea, casi pueda tratarlo como del tipo superconjunto, es decir, cadena ...
gmoody1979
Equals y GetHashCode: Sí, las estructuras usan igualdad estructural, pero en este caso se compara la referencia de la cadena, no el valor de la cadena. Por lo tanto, necesitamos anular Equals. Estoy de acuerdo en que esto no sería necesario si el tipo subyacente fuera un tipo de valor. Según tengo entendido, la implementación predeterminada de GetHashCode para los tipos de valor (que es bastante limitada), dará el mismo valor pero será más eficiente. Realmente debería probar si ese es el caso, pero es un problema secundario al punto principal de la pregunta. Gracias por tu respuesta por cierto :-).
gmoody1979
@ gmoody1979 Las estructuras se comparan utilizando Equals en cada campo de forma predeterminada. No debería ser un problema con las cadenas. Lo mismo con GetHashCode. En cuanto a la estructura que es subconjunto de cadena. Me gusta pensar en el tipo como red de seguridad. No quiero trabajar con ValidatedName y luego deslizarme accidentalmente para usar string. Preferiría que el compilador me hiciera especificar explícitamente que ahora quiero trabajar con datos no verificados.
Eufórico
Lo siento, sí, buen punto en Iguales. Aunque la anulación debería funcionar mejor dado el comportamiento predeterminado, necesita usar la reflexión para hacer la comparación. Casting: sí, posiblemente un buen argumento para convertirlo en un elenco explícito.
gmoody1979