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 struct
que tiene un solo private readonly
campo 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 struct
tipo.
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 string
lanzamiento como implicit
esto nunca puede fallar, pero el string
lanzamiento explicit
como esto arrojará valores no válidos, pero, por supuesto, ambos podrían ser implicit
o 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
static
mé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?
Respuestas:
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:
ValidatedName
vez contiene un valor no válido, sabe que el error está en elIsValid
método.Si obtiene el
IsValid
método correcto, tiene la garantía de que cualquier función que recibe unValidatedName
de 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
Handle
estructura y solo proporcionar acceso al constructor a esa parte del código. Si el código que produce elHandle
s es correcto, solo se utilizarán identificadores válidos.fuente
buena :
ValidatedString
hace que sea mucho más claro acerca de la semántica de la llamada.Malo :
IsValid
antes del reparto es un poco extraño.ValidatedString
no es válido / validado.He visto este tipo de cosas más a menudo con
User
yAuthenticatedUser
tipo de cosas, donde el objeto realmente cambia. Puede ser un buen enfoque, aunque parece fuera de lugar en C #.fuente
Tu camino es bastante pesado e intenso. Normalmente defino entidades de dominio como:
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:
Estos validadores también pueden reutilizarse fácilmente, y usted escribe menos código repetitivo. Y otra ventaja es que es legible.
fuente
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)nameValue
y nuevoValidatedName(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
ToString
sobrecargas 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
Equals
yGetHashCode
estás duplicando este comportamiento predeterminado. Puedes eliminarlos y será más o menos lo mismo.fuente