¿Primitivo vs clase para representar un objeto de dominio simple?


14

¿Cuáles son las pautas generales o reglas generales para cuándo usar un objeto específico de dominio frente a una cadena o número simple?

Ejemplos:

  • ¿Clase de edad vs entero?
  • FirstName class vs String?
  • UniqueID vs String
  • PhoneNumber class vs String vs Long?
  • DomainName clase vs cadena?

Creo que la mayoría de los practicantes de OOP definitivamente dirían clases específicas para PhoneNumber y DomainName. Cuantas más reglas se refieran a lo que los hace válidos y cómo compararlos, las clases simples serán más fáciles y seguras de manejar. Pero para los tres primeros hay más debate.

Nunca me he encontrado con una clase de "Edad", pero uno podría argumentar que tiene sentido dado que debe ser no negativo (está bien, sé que puede argumentar por edades negativas, pero es un buen ejemplo de que es casi equivalente a un entero primitivo).

La cadena es común para representar "Nombre" pero no es perfecta porque una cadena vacía es una cadena válida pero no un nombre válido. La comparación generalmente se haría ignorando el caso. Claro que hay métodos para verificar si está vacío, hacer una comparación que no distinga entre mayúsculas y minúsculas, etc., pero requiere que el consumidor haga esto.

¿La respuesta depende del medio ambiente? Me preocupa principalmente el software empresarial / de alto valor que vivirá y se mantendrá durante posiblemente más de una década.

Tal vez estoy pensando demasiado en esto, pero realmente me gustaría saber si alguien tiene reglas sobre cuándo elegir clase versus primitivo.


2
La clase de edad no es una buena idea si se puede reemplazar por un número entero. Si toma una fecha de nacimiento en el constructor y tiene métodos getCurrentAge () y getAgeAt (Date date), entonces la clase tiene significado. Pero no puede ser reemplazado por un número entero.
kiwiron

55
"Una cadena a veces no es una cadena. Modele en consecuencia". Hay una buena publicación de Mark Seemann sobre 'obsesión primitiva': blog.ploeh.dk/2015/01/19/…
Serhii Shushliapin

44
En realidad, una clase de edad tiene sentido de una manera extraña. La definición occidental común de Edad es cero al nacer y suma 1 en su próximo cumpleaños. La metodología del este asiático es que usted es 1 al nacer y 1 se agrega al siguiente día de Año Nuevo. Por lo tanto, dependiendo de su ubicación, su edad se puede calcular de manera diferente. EG de en.wikipedia.org/wiki/East_Asian_age_reckoning people may be one or two years older in Asian reckoning than in the western age system
Peter M

@PeterM, ¡es buena información! Fechas / Horas y ahora edades. Deberían ser conceptos simples, pero esto demuestra que hay mucha más complejidad en el mundo de lo que estamos acostumbrados a manejar.
Machado

66
Veo muchos escritos debajo de esta pregunta, pero la respuesta no es realmente tan complicada. Utiliza una Ageclase en lugar de un número entero cuando necesita el comportamiento adicional que tal clase proporcionaría.
Robert Harvey

Respuestas:


20

¿Cuáles son las pautas generales o reglas generales para cuándo usar un objeto específico de dominio frente a una cadena o número simple?

La pauta general es que desea modelar su dominio en un idioma específico de dominio.

Considere: ¿por qué usamos entero? Podemos representar todos los enteros con cadenas con la misma facilidad. O con bytes.

Si estuviéramos programando en un lenguaje agnóstico de dominio que incluyera tipos primitivos para entero y edad, ¿cuál elegirías?

Lo que realmente se reduce a la naturaleza "primitiva" de ciertos tipos es un accidente de la elección del lenguaje para nuestra implementación.

Los números, en particular, generalmente requieren un contexto adicional. ¡La edad no es solo un número, sino que también tiene dimensiones (tiempo), unidades (años?), Reglas de redondeo. Agregar edades juntas tiene sentido de una manera que agregar una edad a un dinero no.

Hacer distintos los tipos nos permite modelar las diferencias entre una dirección de correo electrónico no verificada y una dirección de correo electrónico verificada .

El accidente de cómo se representan estos valores en la memoria es una de las partes menos interesantes. Al modelo de dominio no le importa un CustomerIdes un int, o una cadena, o un UUID / GUID, o un nodo JSON. Solo quiere las posibilidades.

¿Realmente nos importa si los enteros son big endian o little endian? ¿Nos importa si lo Listque hemos pasado es una abstracción sobre una matriz o un gráfico? Cuando descubrimos que la aritmética de doble precisión es ineficiente, y que necesitamos cambiar a una representación de coma flotante, ¿ debería preocuparse el modelo de dominio ?

Parnas, en 1972 , escribió

En su lugar, proponemos que uno comience con una lista de decisiones de diseño difíciles o decisiones de diseño que probablemente cambien. Cada módulo está diseñado para ocultar tal decisión de los demás.

En cierto sentido, los tipos de valores específicos del dominio que presentamos son módulos que aíslan nuestra decisión de qué representación subyacente de los datos se debe utilizar.

Entonces, la ventaja es la modularidad: obtenemos un diseño donde es más fácil administrar el alcance de un cambio. La desventaja es el costo: es más trabajo crear los tipos a medida que necesita, elegir los tipos correctos requiere adquirir una comprensión más profunda del dominio. La cantidad de trabajo requerida para crear el módulo de valor dependerá de su dialecto local de Blub .

Otros términos en la ecuación pueden incluir la vida útil esperada de la solución (modelado cuidadoso para el software de script que se ejecutará una vez que tenga un retorno de la inversión pésimo), qué tan cerca está el dominio de la competencia central del negocio.

Un caso especial que podríamos considerar es el de la comunicación a través de un límite . No queremos estar en una situación en la que los cambios en una unidad desplegable requieran cambios coordinados con otras unidades desplegables. Por lo tanto, los mensajes tienden a centrarse más en las representaciones, sin tener en cuenta los invariantes o los comportamientos específicos del dominio. No vamos a tratar de comunicar "este valor debe ser estrictamente positivo" en el formato del mensaje, sino comunicar su representación en el cable y aplicar la validación a esa representación en el límite del dominio.


Excelente punto sobre ocultar (o abstraer) aquellas cosas que son difíciles y que probablemente cambien.
user949300 el

4

Estás pensando en la abstracción, y eso es genial. Sin embargo, las cosas que su listado me parecen en su mayoría atributos individuales de algo más grande (no todo lo mismo, por supuesto).

Lo que creo que es más importante es proporcionar una abstracción que agrupe los atributos y les brinde un hogar adecuado, como una clase Persona, para que el programador del cliente no tenga que lidiar con múltiples atributos, sino con una abstracción de nivel superior.

Una vez que tenga esta abstracción, no necesariamente necesita abstracciones adicionales para los atributos que son solo valores. La clase Person puede contener una fecha de nacimiento como una fecha (primitiva), junto con los números de teléfono como lista de cadenas (primitivas) y un nombre como una cadena (primitiva).

Si tiene un número de teléfono con un código de formato, ahora son dos atributos y merecería una clase para el número de teléfono (ya que probablemente también tendría algún comportamiento, como obtener números solo frente a obtener un número de teléfono formateado).

Si tiene un Nombre como atributos múltiples (por ejemplo, prefijos / títulos, primero, último, sufijo) que merecería una clase (ya que ahora tiene algunos comportamientos como obtener el nombre completo como una cadena frente a obtener piezas más pequeñas, título, etc. .).


1

Debe modelar según lo necesite, si simplemente desea algunos datos, puede usar tipos primitivos, pero si desea realizar más operaciones sobre estos datos, debe modelarse en clases específicas, por ejemplo (estoy de acuerdo con @kiwiron en su comentario) un La clase de edad no tiene sentido, pero sería mejor reemplazarla con una clase de Fecha de nacimiento y colocar estos métodos allí.

El beneficio de modelar estos conceptos dentro de las clases es que aplica la riqueza de su modelo, por ejemplo, en su lugar tiene un método que acepta cualquier Fecha, un usuario puede completarlo con cualquier fecha, fecha de inicio de la Segunda Guerra Mundial o fecha de finalización de la Guerra Fría, de estas fechas sigue siendo una fecha de nacimiento válida. puede reemplazarlo con Fecha de nacimiento, por lo que, en este caso, aplica su método para aceptar una Fecha de nacimiento para no aceptar ninguna Fecha.

Para Nombre no veo muchos beneficios, UniqueID también, PhoneNumber un poco, puede pedirle que le dé el país y la región, por ejemplo, porque en general PhoneNumber se refiere a países y regiones, algunos operadores usan Sufijos de números predefinidos para clasificar Mobile , Office ... números, para que pueda agregar estos métodos a esa clase, lo mismo para DomainName.

Para obtener más detalles, le recomiendo que eche un vistazo a Value Objects en DDD (Diseño controlado por dominio).


1

De lo que depende es si tiene un comportamiento (que necesita mantener y / o cambiar) asociado con esos datos o no.

En resumen, si es solo un dato que se arroja sin mucho comportamiento asociado, use un tipo primitivo o, para datos algo más complejos, una estructura de datos (en gran parte sin comportamiento) (por ejemplo, una clase con solo captadores y setters).

Si, por otro lado, encuentra que, para tomar decisiones, está verificando o manipulando estos datos en varios lugares de su código, lugares que a menudo no están cerca unos de otros, luego conviértalos en un objeto apropiado definiendo una clase y poner el comportamiento relevante dentro de él como métodos. Si por alguna razón considera que no tiene sentido definir dicha clase, una alternativa es consolidar este comportamiento en algún tipo de clase de "servicio" que opere con estos datos.


1

Sus ejemplos se parecen mucho más a las propiedades o atributos de otra cosa. Lo que eso significa es que sería perfectamente razonable comenzar solo con lo primitivo. En algún momento necesitará usar un primitivo, por lo que generalmente guardo complejidad para cuando realmente lo necesito.

Por ejemplo, digamos que estoy haciendo un juego y no tengo que preocuparme por el envejecimiento de los personajes. Usar un número entero para eso tiene mucho sentido. La edad podría usarse en verificaciones simples para ver si el personaje tiene permitido hacer algo o no.

Como otros señalaron, la edad puede ser un tema complicado dependiendo de su ubicación. Si necesita conciliar edades en diferentes lugares, etc., tiene mucho sentido presentar una Ageclase que tenga DateOfBirthyLocale como propiedades. Esa clase de edad también puede calcular la edad actual en cualquier marca de tiempo dada también.

Por lo tanto, hay razones para promover un primitivo en una clase, pero son muy específicas de la aplicación. Aquí hay unos ejemplos:

  • Tiene una lógica de validación especial que debe aplicarse en todas partes donde se utiliza el tipo de información (por ejemplo, números de teléfono, direcciones de correo electrónico).
  • Tiene una lógica de formato especial que se utiliza para representar para que los humanos la lean (por ejemplo, direcciones IP4 e IP6)
  • Tiene un conjunto de funciones que se relacionan con ese tipo de objeto.
  • Tiene reglas especiales para comparar tipos de valores similares.

Básicamente, si tiene un montón de reglas sobre cómo se debe tratar un valor que desea que se use de manera consistente en todo su proyecto, cree una clase. Si puede vivir con valores simples porque no es realmente crítico para la funcionalidad, use una primitiva.


1

Al final del día, un objeto es solo un testigo de que algún proceso realmente se ejecutó .

Una primitiva intsignifica que algún proceso puso a cero 4 bytes de memoria, y luego volteó los bits necesarios para representar algún número entero en el rango de -2,147,483,648 a 2,147,483,647; lo cual no es una declaración fuerte sobre el concepto de edad. Del mismo modo, un (no nulo) System.Stringsignifica que se asignaron algunos bytes y se voltearon los bits para representar una secuencia de caracteres Unicode con respecto a alguna codificación.

Por lo tanto, al decidir cómo representar su objeto de dominio, debe pensar en el proceso para el que sirve como testigo. Si FirstNamerealmente debería ser una cadena no vacía, debería ser un testigo de que el sistema ejecutó algún proceso que garantiza que se haya creado al menos un carácter que no sea un espacio en blanco en una secuencia de caracteres que se le entregaron.

Del mismo modo, un Ageobjeto probablemente debería ser testigo de que algún proceso calculó la diferencia entre dos fechas. Como intsgeneralmente no son el resultado de calcular la diferencia entre dos fechas, son ipso facto insuficientes para representar edades.

Por lo tanto, es bastante obvio que casi siempre desea crear una clase (que es solo un programa) para cada objeto de dominio. ¿Entonces, cuál es el problema? El problema es que C # (que me encanta) y Java exigen demasiada ceremonia para crear clases. Pero podemos imaginar sintaxis alternativas que harían que la definición de objetos de dominio sea mucho más sencilla:

//hypothetical syntax
class Age = |start:date - end:date|.Years

(* ML-like syntax *)
type Age(x, y) = datediff(year, x, y)

//C#-like syntax with primary constructors and expression-bodied classes
class Age(DateTime x, DateTime y) => implicit operator int (Age a) => Abs((y - x).Years);

F #, por ejemplo, en realidad tiene una buena sintaxis para esto en forma de patrones activos :

//Impossible to produce an `Age` without first computing the difference of two dates
//But, any pair (tuple) of dates is implicitly converted to an `Age` when needed.

let (|Age|) (x, y) =  (date_diff y x).Days / 365

El punto es que solo porque su lenguaje de programación hace que sea difícil definir objetos de dominio no significa que realmente sea una mala idea hacerlo.


† También llamamos a estas cosas condiciones de publicación , pero prefiero el término "testigo", ya que sirve como prueba de que algún proceso puede ejecutarse.


0

Piensa en lo que obtienes al usar una clase frente a una primitiva. Si usar una clase te puede ayudar

  • validación
  • formateo
  • otros métodos útiles
  • escriba seguridad (es decir, con una PhoneNumberclase, debería ser imposible para mí asignarlo a una cadena o una cadena aleatoria (es decir, "pepino") donde espero un número de teléfono)
  • cualquier otro beneficio

y esos beneficios hacen su vida más fácil, mejoran la calidad del código, la legibilidad y superan el dolor de usar tales cosas, hágalo. De lo contrario, quédate con los primitivos. Si está cerca, haz un juicio.

También tenga en cuenta que a veces una buena nomenclatura puede manejarlo (es decir, una cadena llamada firstNamevs. hacer una clase completa que simplemente envuelve una propiedad de cadena). Las clases no son la única forma de resolver las cosas.

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.