¿Primitiva vs Clase para representar un objeto de dominio simple?

13

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

Ejemplos:

  • Clase de edad vs Integer?
  • clase de nombre vs cadena?
  • UniqueID vs String
  • PhoneNumber class vs String vs Long?
  • clase DomainName vs String?

Creo que la mayoría de los profesionales de OOP definitivamente dirían clases específicas para PhoneNumber y DomainName. Las más reglas acerca de qué las hace válidas y cómo compararlas hacen que las clases simples sean más fáciles y más seguras de manejar. Pero para los tres primeros hay más debate.

Nunca me he topado con una clase de "Edad", pero se podría argumentar que tiene sentido dado que debe ser no negativa (bueno, se 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 usualmente se haría ignorando el caso. Claro que hay métodos para verificar el vacío, hacer comparaciones 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 más de una década.

Quizás estoy pensando demasiado en esto pero realmente me gustaría saber si alguien tiene reglas sobre cuándo elegir la clase vs la primitiva.

    
pregunta noctonura 30.10.2017 - 07:42

7 respuestas

18
  

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

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

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

Si estuviéramos programando en un lenguaje independiente del dominio que incluyera tipos primitivos para enteros y , ¿cuál elegirías?

A lo que realmente se reduce es que la naturaleza "primitiva" de ciertos tipos es un accidente en la elección del idioma 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 dimensión (tiempo), unidades (¿años?), reglas de redondeo! Agregar edades juntas tiene sentido de una manera que agregar una edad a un dinero no lo es.

La diferenciación de los tipos nos permite modelar las diferencias entre 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. El modelo de dominio que no le importa a CustomerId es un int, una cadena, 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 el List que 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 tenemos que cambiar a una representación de punto flotante, ¿debería importarnos el modelo de dominio ?

Parnas, en 1972 , escribió

  

En cambio, 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 valor específicos de dominio que introducimos son módulos que aíslan nuestra decisión de qué representación subyacente de los datos se debe usar.

Por lo tanto, lo positivo es la modularidad: obtenemos un diseño donde es más fácil administrar el alcance de un cambio. El inconveniente es el costo, es más trabajar 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 requerido 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 (un modelado cuidadoso del software de script que se ejecutará una vez que tenga un pésimo retorno de la inversión), qué tan cerca está el dominio de la competencia central de la empresa.

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, mensajes tienden a centrarse más en las representaciones, sin tener en cuenta las 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 de comunicar su representación en el cable y aplicar la validación a esa representación en el límite del dominio.

    
respondido por el VoiceOfUnreason 30.10.2017 - 14:12
4

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

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

Una vez que tenga esta abstracción, no necesariamente necesitará abstracciones adicionales para los atributos que son solo valores. La clase Persona puede contener una fecha de nacimiento como fecha (primitiva), junto con números de teléfono como lista de cadenas (primitivas) y un nombre como 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 tenga algún comportamiento, como obtener solo números y obtener un número de teléfono con formato) .

Si tiene un Nombre como atributos múltiples (p. ej., prefijos / títulos, primer, último, sufijo) que merecerían una clase (como ahora tiene algunos comportamientos, como obtener el nombre completo como una cadena en lugar de piezas más pequeñas, título etc ..).

    
respondido por el Erik Eidt 30.10.2017 - 12:34
1

Debería modelar como necesite, si simplemente desea algunos datos, puede usar tipos primitivos, pero si desea realizar más operaciones con estos datos, debe modelarse en clases específicas, por ejemplo (estoy de acuerdo con @kiwiron en su comentario) una clase Age no tiene sentido, pero sería mejor reemplazarla con una clase Birthdate y poner estos métodos allí.

El beneficio de modelar estos conceptos dentro de las clases es que hace cumplir la riqueza de su modelo; por ejemplo, en lugar de tener un método que acepta cualquier fecha, un usuario puede completarla con cualquier fecha, fecha de inicio de la Segunda Guerra Mundial o guerra fría. fecha de finalización, de estas fechas sigue siendo una fecha de nacimiento válida. puede reemplazarlo con Birthdate, por lo que, en este caso, aplicará su método para aceptar una fecha de nacimiento y no aceptar ninguna fecha.

Para Firstname no veo mucho beneficio, UniqueID también, PhoneNumber un poco, puede pedirle que le proporcione el país y la región, por ejemplo, porque en general PhoneNumber se refiere a países y regiones, algunos operadores usan un número predefinido. Sufijos para clasificar los números de Mobile, Office ..., para que pueda agregar estos métodos a esa clase, igual para DomainName.

Para más detalles, le recomiendo que eche un vistazo a los objetos de valor en DDD (Diseño impulsado por dominio).

    
respondido por el La VloZ Merrill 30.10.2017 - 11:18
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 solo se trata de un dato que se transmite sin mucho comportamiento asociado con él, use un tipo primitivo o, para datos un poco más complejos, una estructura de datos (en gran medida sin comportamiento) (por ejemplo, una clase con sólo 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, los lugares a menudo no están cerca uno del otro, luego lo convierten en un objeto adecuado al definiendo una clase y poniendo el comportamiento relevante dentro de ella como métodos. Si, por alguna razón, siente que no tiene sentido definir una clase de este tipo, una alternativa es consolidar este comportamiento en algún tipo de clase de "servicio" que funcione con estos datos.

    
respondido por el Filip Milovanović 30.10.2017 - 11:45
1

Sus ejemplos se parecen mucho más a propiedades o atributos de otra cosa. Lo que esto significa es que sería perfectamente razonable comenzar solo con lo primitivo. En algún momento, necesitarás usar un primitivo, así 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 perfecto sentido. La edad podría usarse en controles simples para ver si el personaje tiene permitido hacer algo o no.

Como han señalado otros, la edad puede ser un tema complicado dependiendo de su ubicación. Si tiene la necesidad de conciliar edades en diferentes entornos locales, etc., entonces tiene mucho sentido introducir una clase Age que tenga DateOfBirth y Locale como propiedades. Esa clase de Edad también puede calcular la edad actual en cualquier marca de tiempo dada.

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

  • Tiene una lógica de validación especial que debe aplicarse en todos los lugares en que se usa 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 usa para renderizar para que los humanos la lean (por ejemplo, las direcciones IP4 e IP6)
  • Tiene un conjunto de funciones que se relacionan todas con ese tipo de objeto
  • Tiene reglas especiales para comparar tipos de valores similares

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

    
respondido por el Berin Loritsch 30.10.2017 - 14:07
1

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

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

Por lo tanto, cuando decida cómo representar su objeto de dominio, debe pensar en el proceso por el cual sirve como testigo. Si un FirstName realmente debe ser una cadena no vacía, entonces debe ser 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.

De forma similar, un objeto Age probablemente debería ser testigo de que algún proceso calculó la diferencia entre dos fechas. Como ints generalmente no es el resultado de calcular la diferencia entre dos fechas, son ipso facto insuficientes para representar edades.

Entonces, es bastante obvio que casi siempre se 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 la tarea de definir objetos de dominio sea incómodo no significa que realmente sea una mala idea hacerlo.

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

    
respondido por el Rodrick Chapman 01.11.2017 - 05:38
0

Piensa en lo que obtienes al usar una clase frente a una primativa. Si usar una clase puede conseguirte

  • validación
  • formato
  • otros métodos útiles
  • escriba safety (es decir, con una clase PhoneNumber , 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 que tu vida sea más fácil, mejora la calidad del código, la legibilidad y superan el dolor de usar tales cosas, hazlo. De lo contrario, quédate con los primativos. Si está cerca, haz un llamado a juicio.

También se da cuenta de que, en ocasiones, una buena denominación solo puede manejarlo (es decir, una cadena llamada firstName en lugar de crear una clase completa que simplemente envuelva una propiedad de cadena). Las clases no son la única manera de resolver cosas.

    
respondido por el Becuzz 30.10.2017 - 13:40

Lea otras preguntas en las etiquetas