Recientemente he leído muchos artículos que describen la obsesión primitiva como un olor a código.
Hay dos beneficios de evitar la obsesión primitiva:
Hace que el modelo de dominio sea más explícito. Por ejemplo, puedo hablar con un analista de negocios sobre un código postal en lugar de una cadena que contiene un código postal.
Toda la validación está en un solo lugar en lugar de en toda la aplicación.
Hay muchos artículos por ahí que describen cuándo es un olor a código. Por ejemplo, puedo ver el beneficio de eliminar la obsesión primitiva por un código postal como este:
public class Address
{
public ZipCode ZipCode { get; set; }
}
Aquí está el constructor del código postal:
public ZipCode(string value)
{
// Perform regex matching to verify XXXXX or XXXXX-XXXX format
_value = value;
}
Estaría rompiendo el principio DRY poniendo esa lógica de validación en todas partes donde se usa un código postal.
Sin embargo, ¿qué pasa con los siguientes objetos:
Fecha de nacimiento: verifique que sea mayor que la fecha mínima y menor que la fecha de hoy.
Salario: verifique que sea mayor o igual que cero.
¿Crearía un objeto DateOfBirth y un objeto Salario? El beneficio es que puede hablar sobre ellos al describir el modelo de dominio. Sin embargo, este es un caso de ingeniería excesiva ya que no hay mucha validación. ¿Existe una regla que describa cuándo y cuándo no eliminar la obsesión primitiva o debería hacerlo siempre que sea posible?
Supongo que podría crear un alias de tipo en lugar de una clase, lo que ayudaría con el punto uno anterior.
DateOfBirth
constructor para que lo compruebe?Salary
yDistance
no puede usarlo de manera accidental de manera intercambiable. Puedes hacerlo si ambos son de tipodouble
.Respuestas:
Lo contrario sería "modelado de dominio", o tal vez "sobre ingeniería".
Introducir un objeto Salario puede ser una buena idea por la siguiente razón: los números rara vez son independientes en el modelo de dominio, casi siempre tienen una dimensión y una unidad. Normalmente no modelamos nada útil si agregamos una longitud a un tiempo o una masa, y rara vez obtenemos buenos resultados cuando mezclamos metros y pies.
En cuanto a DateOfBirth, probablemente, hay dos cuestiones a considerar. Primero, crear una Fecha no primitiva le brinda un lugar para centrar todas las inquietudes extrañas relacionadas con las matemáticas de fechas. Muchos idiomas proporcionan uno fuera de la caja; DateTime , java.util.Date . Estos son de dominio implementaciones agnósticas de fechas, pero son no primitivas.
Segundo,
DateOfBirth
no es realmente una hora de cita; aquí en los Estados Unidos, "fecha de nacimiento" es una construcción cultural / ficción legal. Tendemos a medir la fecha de nacimiento a partir de la fecha local del nacimiento de una persona; Bob, nacido en California, podría tener una fecha de nacimiento "anterior" a la de Alice, nacida en Nueva York, a pesar de que es el menor de los dos.Ciertamente no siempre; en los límites, las aplicaciones no están orientadas a objetos . Es bastante común ver primitivas utilizadas para describir los comportamientos en las pruebas .
fuente
java.util.Date
conjava.time.LocalDate
Para ser honesto: depende.
Siempre existe el riesgo de diseñar en exceso su código. ¿Cuán extendido será el uso de DateOfBirth and Salary? ¿Los usará solo en tres clases estrechamente acopladas, o se usarán en toda la aplicación? ¿Los "encapsularía" en su propio Tipo / Clase para imponer esa restricción, o puede pensar en más restricciones / funciones que realmente pertenecen allí?
Tomemos por ejemplo Salario: ¿Tiene alguna operación con "Salario" (por ejemplo, manejo de monedas diferentes, o tal vez una función toString ())? Considere qué es / hace Salario cuando no lo ve como una simple primitiva, y hay una buena posibilidad de que Salario sea su propia clase.
fuente
Una posible regla general puede depender de la capa del programa. Para el Dominio (DDD), también conocido como Capa de entidades (Martin, 2018), esto también podría ser "evitar primitivas para cualquier cosa que represente un concepto de dominio / negocio". Las justificaciones son las establecidas por el OP: un modelo de dominio más expresivo, validación de reglas de negocio, haciendo explícitos los conceptos implícitos (Evans, 2004).
Un alias de tipo puede ser una alternativa ligera (Ghosh, 2017) y refactorizarse a una clase de entidad cuando sea necesario. Por ejemplo, primero podemos exigir que
Salary
sea así>=0
, y luego decidir no permitir$100.33333
y cualquier cosa anterior$10,000,000
(que llevaría a la quiebra al cliente). El uso deNonnegative
primitivo para representarSalary
y otros conceptos complicaría esta refactorización.Evitar las primitivas también puede ayudar a evitar el exceso de ingeniería. Supongamos que necesitamos combinar Salario y Fecha de nacimiento en una estructura de datos: por ejemplo, tener menos parámetros de método o pasar datos entre módulos. Entonces podemos usar una tupla con tipo
(Salary, DateOfBirth)
. De hecho, una tupla con primitivas(Nonnegative, Nonnegative)
no es informativa, mientras que algunos hinchadosclass EmployeeData
ocultarían los campos requeridos entre otros. La firma en saycalcPension(d: (Salary, DateOfBirth))
está más enfocada que encalcPension(d: EmployeeData)
, lo que viola el Principio de segregación de interfaz. Del mismo modo, un especialistaclass SalaryAndDateOfBirth
parece incómodo y probablemente es una exageración. Más adelante, podemos elegir definir una clase de datos; Las tuplas y los tipos de dominio elemental nos permiten diferir tales decisiones.En una capa externa (por ejemplo, GUI) puede tener sentido "despojar" a las entidades hasta sus primitivas constituyentes (por ejemplo, ponerlas en un DAO). Esto evita que se filtren abstracciones de dominio en las capas externas, como propugna Martin (2018).
Referencias
E. Evans, "Diseño dirigido por dominio", 2004
D. Ghosh, "Modelado de dominio funcional y reactivo", 2017
RC Martin, "Arquitectura limpia", 2018
fuente
¿Mejor sufrir de obsesión primitiva o ser astronauta de arquitectura ?
Ambos casos son patológicos, en un caso tiene muy pocas abstracciones, lo que lleva a la repetición y confunde fácilmente una manzana con una naranja, y en el otro se olvidó de detenerse ya y comenzar a hacer las cosas, lo que dificulta hacer cualquier cosa. .
Como casi siempre, quieres moderación, un camino intermedio bien considerado.
Recuerde que una propiedad tiene un nombre, además de un tipo. Además, descomponer una dirección en sus partes constituyentes puede ser demasiado restrictivo si siempre se hace de la misma manera. No todo el mundo es el centro de Nueva York.
fuente
Si tuviera una clase de Salario, podría tener métodos como ApplyRaise.
Por otro lado, su clase ZipCode no tiene que tener una validación interna para evitar duplicar la validación en cualquier lugar donde pueda tener una clase ZipCodeValidator que podría inyectarse, por lo que si su sistema se ejecuta tanto en direcciones de EE. UU. Como del Reino Unido, puede simplemente inyectar el validador correcto y cuando tiene que manejar direcciones AUS también puede agregar un nuevo validador.
Otra preocupación es que si tiene que escribir datos en una base de datos a través de EntityFramework, entonces necesitará saber cómo manejar Salario o Código postal.
No hay una respuesta clara de dónde trazar la línea entre lo inteligentes que deberían ser las clases, pero diré que tiendo a mover la lógica de negocios, como la validación, a las clases de lógica de negocios teniendo las clases de datos como datos puros como parece. para trabajar mejor con EntityFramework.
En cuanto al uso de alias de tipo, el nombre de miembro / propiedad debería proporcionar toda la información necesaria sobre el contenido, por lo que no usaría alias de tipo.
fuente
(Lo que la pregunta probablemente es realmente)
¿Cuándo el uso del tipo primitivo no es un olor a código?
(Responder)
Cuando el parámetro no tiene reglas, use un tipo primitivo.
Use el tipo primitivo para los gustos de:
Use objeto para gustos de:
El último ejemplo tiene reglas en ella, es decir, el objeto
SimpleDate
está compuesto deYear
,Month
yDay
. Mediante el uso de Object en este caso, el concepto deSimpleDate
ser válido puede encapsularse dentro del objeto.fuente
Además de los ejemplos canónicos de direcciones de correo electrónico o códigos postales proporcionados en otra parte de esta pregunta, donde encuentro refactorizar lejos de Primitive Obsession puede ser particularmente útil es con ID de entidad (consulte https://andrewlock.net/using-strongly-typed-entity -ids-to-evitar-primitive-obsession-part-1 / para ver un ejemplo de cómo hacerlo en .NET).
He perdido la cuenta de la cantidad de veces que un error se ha introducido porque un método tenía una firma como esta:
Compila muy bien, y si no eres riguroso con la prueba de tu unidad, también puede pasar eso. Sin embargo, refactorice esas ID de entidad en clases específicas de dominio y, oiga, errores de tiempo de compilación:
Imagine que el método escala hasta 10 o más parámetros, todos de
int
tipo de datos (sin importar el olor del código de la Lista de parámetros largos ). Empeora aún más cuando usa algo como AutoMapper para intercambiar entre objetos de dominio y DTO, y una refactorización que hace no es recogido por el mapeo automágico.fuente
Por otro lado, cuando se trata con muchos países diferentes y sus diferentes sistemas de códigos postales, eso significa que no puede validar un código postal a menos que conozca el país en cuestión. Por lo tanto, su
ZipCode
clase también debe almacenar el país.Pero, ¿almacena por separado el país como parte del
Address
código postal (del que también forma parte el código postal) y parte del código postal (para la validación)?ZipCode
clase sino unaAddress
clase, que nuevamente contendrá unastring ZipCode
que significa que hemos cerrado el círculo.No entiendo su afirmación subyacente de que cuando una información tiene un tipo de variable dado, de alguna manera está obligado a mencionar ese tipo cada vez que habla con un analista de negocios.
¿Por qué? ¿Por qué no puede simplemente hablar sobre "el código postal" y omitir completamente el tipo específico? ¿Qué tipo de discusiones está teniendo con su analista de negocios (no técnico) donde el tipo de propiedad es la esencia de la conversación?
De donde soy, los códigos postales son siempre numéricos. Entonces tenemos una opción, podríamos almacenarlo como un
int
o como unstring
. Tendemos a usar una cadena porque no se esperan operaciones matemáticas en los datos, pero nunca un analista de negocios me dijo que tenía que ser una cadena. Esa decisión se deja en manos del desarrollador (o posiblemente el analista técnico, aunque en mi experiencia no tratan directamente con el meollo del asunto).Un analista de negocios no se preocupa por el tipo de datos, siempre que la aplicación haga lo que se espera que haga.
La validación es una bestia difícil de abordar, porque se basa en lo que los humanos esperan.
Por un lado, no estoy de acuerdo con el argumento de validación como una forma de mostrar por qué se debe evitar la obsesión primitiva, porque no estoy de acuerdo en que (como una verdad universal) los datos siempre deben validarse en todo momento.
Por ejemplo, ¿qué pasa si esta es una búsqueda más complicada? En lugar de una simple verificación de formato, ¿qué sucede si su validación implica contactar a una API externa y esperar una respuesta? ¿Realmente desea obligar a su aplicación a llamar a esta API externa para cada
ZipCode
objeto que cree una instancia?Tal vez es un requisito comercial estricto, y luego, por supuesto, es justificable. Pero esta no es una verdad universal. Habrá muchos casos de uso donde esto es más una carga que una solución.
Como segundo ejemplo, al ingresar su dirección en un formulario, es común ingresar su código postal antes de su país. Si bien es bueno tener comentarios de validación inmediata en la interfaz de usuario, en realidad sería un obstáculo para mí (como usuario) si la aplicación me alertara de un formato de código postal "incorrecto", porque la fuente real del problema es (por ejemplo) que mi país no es el país seleccionado de forma predeterminada y, por lo tanto, la validación se realizó para el país incorrecto.
Es el mensaje de error incorrecto, que distrae al usuario y causa confusión innecesaria.
Al igual que la validación perpetua no es una verdad universal, tampoco lo son mis ejemplos. Es contextual . Algunos dominios de aplicación requieren validación de datos por encima de todo. Otros dominios no colocan la validación tan arriba en la lista de prioridades porque la molestia que conlleva entra en conflicto con sus prioridades reales (por ejemplo, la experiencia del usuario o la capacidad de almacenar inicialmente datos defectuosos para que puedan corregirse en lugar de nunca permitir que sean almacenado)
El problema con estas validaciones es que son incompletas, redundantes o indican un problema mucho mayor .
Comprobar que una fecha es mayor que la fecha mínima es redundante. La fecha mínima literalmente significa que es la fecha más pequeña posible. Además, ¿dónde trazas la línea de relevancia? ¿De qué sirve prevenir?
DateTime.MinDate
pero permitirDateTime.MinDate.AddSeconds(1)
? Estás seleccionando un valor particular que no es particularmente incorrecto en comparación con muchos otros valores.Mi cumpleaños es el 2 de enero de 1978 (no lo es, pero supongamos que sí). Pero digamos que los datos en su aplicación son incorrectos, y en cambio dice que mi cumpleaños es:
Todas estas fechas están mal. Ninguno de ellos tiene "más razón" que el otro. Pero su regla de validación solo atraparía uno de estos tres ejemplos.
También ha omitido por completo el contexto de cómo está utilizando estos datos. Si esto se usa, por ejemplo, en un bot de recordatorio de cumpleaños, diría que la validación no tiene sentido ya que no hay ninguna consecuencia negativa particular al completar la fecha incorrecta.
Por otro lado, si se trata de datos del gobierno y lo que necesita la fecha de nacimiento de la identidad de alguien autenticar (y el no hacerlo conduce a malas consecuencias, por ejemplo, que niegan la seguridad social a alguien), entonces la exactitud de los datos es de suma importancia y lo que necesita totalmente validar los datos. La validación propuesta que tiene ahora no es adecuada.
Para un salario, hay algo de sentido común en que no puede ser negativo. Pero si realmente espera que se ingresen datos sin sentido, le sugiero que investigue la fuente de estos datos sin sentido. Porque si no se puede confiar en que ingresen datos sensibles, tampoco puede confiar en ellos para que ingresen datos correctos .
Si, en cambio, su aplicación calcula el salario, y de alguna manera es posible terminar con un número negativo (y correcto), entonces un mejor enfoque sería
Math.Max(myValue, 0)
convertir los números negativos en 0, en lugar de fallar la validación. Porque si su lógica decidió que el resultado es un número negativo, no aprobar la validación significa que tendrá que rehacer el cálculo, y no hay razón para pensar que saldrá con un número diferente la segunda vez.Y si aparece un número diferente, eso nuevamente lo lleva a sospechar que el cálculo no es consistente y, por lo tanto, no se puede confiar.
Esto no quiere decir que la validación no sea útil. Pero la validación sin sentido es mala, tanto porque requiere esfuerzo sin resolver realmente un problema, y da a las personas una falsa sensación de seguridad.
fuente