¿Cuándo la obsesión primitiva no es un olor a código?

22

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:

  1. 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.

  2. 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:

  1. Fecha de nacimiento: verifique que sea mayor que la fecha mínima y menor que la fecha de hoy.

  2. 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.

w0051977
fuente
8
"Estaría rompiendo el principio DRY poniendo esa lógica de validación en todas partes donde se usa un código postal". Eso no es verdad. La validación debe hacerse tan pronto como se ingresen los datos en su módulo . Si hay más de un "punto de entrada", la validación debe estar en una unidad reutilizable , y eso no necesita ser (ni debería ser) el DTO ...
Timothy Truckle
1
¿Cómo le estás dando "fecha mínima" y "fecha de hoy" al DateOfBirthconstructor para que lo compruebe?
Caleth
11
Otro beneficio de crear tipos personalizados es la seguridad de tipos. Si tiene un objeto Salaryy Distanceno puede usarlo de manera accidental de manera intercambiable. Puedes hacerlo si ambos son de tipo double.
Scroog1
3
@ w0051977 Su declaración (tal como la entendí) implicaba que cualquier otra cosa que no fuera la validación en el constructor de DTO violaría DRY. De hecho, la validación debería estar fuera del DTO ...
Timothy Truckle
2
Para mí todo es cuestión de alcance. Si le da a las primitivas un amplio alcance, existen numerosas formas en que pueden ser mal utilizadas y mal manejadas. Por lo tanto, generalmente desea darles un alcance más limitado, y una forma de hacerlo es diseñar una clase que represente un concepto utilizando un primitivo, almacenado de forma privada como interno, para implementarlo. Ahora, el alcance de la primitiva es limitado y es poco probable que se use / maneje mal, y puede mantener invariantes de manera efectiva. Pero si el alcance de la primitiva era limitado para empezar, esto podría ser excesivo e introducir una gran cantidad de acoplamiento y código adicionales para mantener.

Respuestas:

17

La obsesión primitiva está utilizando tipos de datos primitivos para representar ideas de dominio.

Lo contrario sería "modelado de dominio", o tal vez "sobre ingeniería".

¿Crearía un objeto DateOfBirth y un objeto Salario?

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, DateOfBirthno 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.

¿Existe una regla que describa cuándo y cuándo no eliminar la obsesión primitiva o si debe hacerlo siempre que sea posible?

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 .

VoiceOfUnreason
fuente
1
El primer comentario después de la cita en la parte superior parece no ser sequitur. Además, solo repite el tema de la pregunta. De lo contrario, es una buena respuesta, pero me parece realmente molesto.
JimmyJames
ni C # DateTime ni java.util.Date son tipos subyacentes apropiados para DateOfBirth.
Kevin Cline
Tal vez reemplazar java.util.Dateconjava.time.LocalDate
Koray Tugay
7

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.

CharonX
fuente
¿Es un alias de tipo una buena alternativa?
w0051977
@ w0051977 Estoy de acuerdo con charonx y el alias de tipo podría ser una alternativa
techagrammer
@ w0051977 un alias de tipo puede ser una alternativa si el objetivo principal es imponer una escritura estricta, establecer explícitamente qué valor es (Salario) para evitar una asignación accidental de "dólares flotantes" (por hora, semana, mes o mes) a "salario flotante" (por mes? año?). Realmente depende de cuáles sean sus necesidades.
CharonX
@CharonX, creo que se debe usar un decimal para un salario y no un flotante. ¿Estás de acuerdo?
w0051977
@ w0051977 Si tiene un buen tipo decimal, entonces ese sería preferible, sí. (Estoy trabajando en un proyecto de C ++ en este momento, por lo que los booleanos, enteros y flotantes están en primer plano)
CharonX
5

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 Salarysea ​​así >=0, y luego decidir no permitir $100.33333y cualquier cosa anterior $10,000,000(que llevaría a la quiebra al cliente). El uso de Nonnegativeprimitivo para representar Salaryy 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 hinchados class EmployeeDataocultarían los campos requeridos entre otros. La firma en say calcPension(d: (Salary, DateOfBirth))está más enfocada que en calcPension(d: EmployeeData), lo que viola el Principio de segregación de interfaz. Del mismo modo, un especialista class SalaryAndDateOfBirthparece 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

Tupolev._
fuente
+1 para todas las referencias.
w0051977
4

¿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.

Deduplicador
fuente
3

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.

Doblado
fuente
¿Es un alias de tipo una buena alternativa?
w0051977
2

(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:

htmlEntityEncode(string value)

Use objeto para gustos de:

numberOfDaysSinceUnixEpoch(SimpleDate value)

El último ejemplo tiene reglas en ella, es decir, el objeto SimpleDateestá compuesto de Year, Monthy Day. Mediante el uso de Object en este caso, el concepto de SimpleDateser válido puede encapsularse dentro del objeto.

Ingeniero PHP Stoked
fuente
1

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:

int leaveId = 12345;
int submitterId = 23456;
int approverId = 34567;

SubmitLeaveApplication(leaveId, approverId, submitterId);

public void SubmitLeaveApplication(int leaveId, int submitterId, int approverId) {
  // implementation here
}

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:

LeaveId leaveId = 12345;
SubmitterId submitterId = 23456;
ApproverId approverId = 34567;

SubmitLeaveApplication(leaveId, approverId, submitterId);

public void SubmitLeaveApplication(LeaveId leaveId, SubmitterId submitterId, ApproverId approverId) {
  // implementation here
}

Imagine que el método escala hasta 10 o más parámetros, todos de inttipo 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.

David Keaveny
fuente
0

Estaría rompiendo el principio DRY poniendo esa lógica de validación en todas partes donde se usa un código postal.

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 ZipCodeclase también debe almacenar el país.

Pero, ¿almacena por separado el país como parte del Addresscódigo postal (del que también forma parte el código postal) y parte del código postal (para la validación)?

  • Si lo haces, también estás violando DRY. Incluso si no lo llama una violación SECA (porque cada instancia tiene un propósito diferente), todavía está ocupando innecesariamente memoria adicional, además de abrir la puerta a los errores cuando los valores de los dos países son diferentes (que lógicamente nunca deberían ser).
    • O, alternativamente, hace que necesite sincronizar los dos puntos de datos para asegurarse de que siempre sean los mismos, lo que sugiere que de todos modos realmente debería almacenar estos datos en un solo punto, lo que anula el propósito.
  • Si no lo hace, entonces no es una ZipCodeclase sino una Addressclase, que nuevamente contendrá una string ZipCodeque significa que hemos cerrado el círculo.

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.

El beneficio es que puede hablar sobre ellos al describir el modelo de dominio.

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 into como un string. 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 ZipCodeobjeto 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)

Fecha de nacimiento: verifique que sea mayor que mindate y menor que la fecha de hoy.
Salario: verifique que sea mayor o igual que cero.

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 permitir DateTime.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:

  • 1 de enero de 1978
  • 1 de enero de 1722
  • 1 de enero de 2355

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.

Flater
fuente
La fecha de nacimiento de alguien puede ser realmente posterior a la fecha actual, si un bebé nace en este momento en una zona horaria que ya se ha saltado al día siguiente. Y un hospital podría almacenar "fecha de nacimiento esperada" en una base de datos que podría ser meses en el futuro. ¿Quieres un tipo diferente para eso?
gnasher729
@ gnasher729: No estoy seguro de seguirlo, parece que estás de acuerdo conmigo (la validación es contextual y no es universalmente correcta), pero la redacción de tu comentario sugiere que crees que no estoy de acuerdo. ¿O estoy leyendo mal?
Flater