Según ¿ Cuándo la obsesión primitiva no es un código de olor? , Debería crear un objeto ZipCode para representar un código postal en lugar de un objeto String.
Sin embargo, en mi experiencia, prefiero ver
public class Address{
public String zipCode;
}
en lugar de
public class Address{
public ZipCode zipCode;
}
porque creo que la última requiere que me mueva a la clase ZipCode para comprender el programa.
Y creo que necesito moverme entre muchas clases para ver la definición si todos los campos de datos primitivos fueron reemplazados por una clase, que se siente como si sufriera el problema del yoyo (un antipatrón).
Entonces, me gustaría mover los métodos ZipCode a una nueva clase, por ejemplo:
Antiguo:
public class ZipCode{
public boolean validate(String zipCode){
}
}
Nuevo:
public class ZipCodeHelper{
public static boolean validate(String zipCode){
}
}
para que solo el que necesite validar el código postal dependa de la clase ZipCodeHelper. Y encontré otro "beneficio" de mantener la obsesión primitiva: mantiene la clase como su forma serializada, si la hay, por ejemplo: una tabla de direcciones con una columna de cadena zipCode.
Mi pregunta es, ¿es "evitar el problema del yo-yo" (moverse entre las definiciones de clase) una razón válida para permitir la "obsesión primitiva"?
fuente
Respuestas:
La suposición es que no necesita ingresar a la clase ZipCode para comprender la clase Address. Si ZipCode está bien diseñado, debería ser obvio lo que hace simplemente leyendo la clase Address.
Los programas no se leen de principio a fin; por lo general, los programas son demasiado complejos para hacer esto posible. No puede tener todo el código de un programa en mente al mismo tiempo. Por lo tanto, utilizamos abstracciones y encapsulaciones para "fragmentar" el programa en unidades significativas, de modo que pueda ver una parte del programa (por ejemplo, la clase Address) sin tener que leer todo el código del que depende.
Por ejemplo, estoy seguro de que no lees el código fuente de String cada vez que te encuentras con String en el código.
Cambiar el nombre de la clase de ZipCode a ZipCodeHelper sugiere que ahora hay dos conceptos separados: un código postal y un asistente de código postal. Entonces, el doble de complejo. Y ahora el sistema de tipos no puede ayudarlo a distinguir entre una cadena arbitraria y un código postal válido, ya que tienen el mismo tipo. Aquí es donde la "obsesión" es apropiada: sugieres una alternativa más compleja y menos segura solo porque quieres evitar un tipo de envoltorio simple alrededor de un primitivo.
El uso de una primitiva está IMHO justificado en los casos en que no hay validación u otra lógica que dependa de este tipo en particular. Pero tan pronto como agregue alguna lógica, es mucho más simple si esta lógica se encapsula con el tipo.
En cuanto a la serialización, creo que suena como una limitación en el marco que está utilizando. Seguramente debería poder serializar un código postal en una cadena o asignarlo a una columna en una base de datos.
fuente
ZipCodeHelper
(que preferiría llamarZipCodeValidator
) bien podría establecer una conexión a un servicio web para hacer su trabajo. Eso no sería parte de la responsabilidad única "mantener los datos del código postal". Hacer que el sistema de tipos no permita códigos postales no válidos aún se puede lograr haciendo que elZipCode
constructor sea el equivalente del paquete privado de Java y llamando eso con unZipCodeFactory
que siempre llama al validador.ZipCode
Sería una "estructura de datos". yZipCodeHelper
un "objeto". En cualquier caso, creo que estamos de acuerdo en que no deberíamos tener que pasar conexiones web al constructor ZipCode.int
como ID permite multiplicar una ID por una ID ...)Foo
y lasFooValidator
clases. Nosotros podríamos tener unaZipCode
clase que valida el formato y unaZipCodeValidator
que golpea un poco de servicio Web para comprobar que un formato correctoZipCode
es en realidad actual. Sabemos que los códigos postales cambian. Pero prácticamente, vamos a tener una lista de códigos postales válidos encapsulados enZipCode
, o en alguna base de datos local.Si puede hacer:
Y el constructor para ZipCode hace:
Luego rompió la encapsulación y agregó una dependencia bastante tonta a la clase ZipCode. Si el constructor no llama,
ZipCodeHelper.validate(...)
entonces tiene lógica aislada en su propia isla sin aplicarla realmente. Puede crear códigos postales no válidos.El
validate
método debe ser un método estático en la clase ZipCode. Ahora el conocimiento de un código postal "válido" se agrupa con la clase ZipCode. Dado que sus ejemplos de código se parecen a Java, el constructor de ZipCode debería lanzar una excepción si se da un formato incorrecto:El constructor verifica el formato y lanza una excepción, evitando así que se creen códigos postales no válidos, y el
validate
método estático está disponible para otro código, por lo que la lógica de verificar el formato se encapsula en la clase ZipCode.No hay "yo-yo" en esta variante de la clase ZipCode. Simplemente se llama Programación Orientada a Objetos adecuada.
Aquí también vamos a ignorar la internacionalización, que puede requerir otra clase llamada ZipCodeFormat o PostalService (por ejemplo, PostalService.isValidPostalCode (...), PostalService.parsePostalCode (...), etc.).
fuente
ZipCode.validate
método es la verificación previa que se puede realizar antes de invocar un constructor que arroja una excepción.Si luchas mucho con esta pregunta, ¿quizás el lenguaje que usas no es la herramienta adecuada para el trabajo? Este tipo de "primitivas de tipo dominio" son trivialmente fáciles de expresar, por ejemplo, en F #.
Allí podría, por ejemplo, escribir:
Esto es realmente útil para evitar errores comunes, como comparar identificaciones de diferentes entidades. Y dado que estas primitivas tipificadas son mucho más livianas que una clase C # o Java, terminarás usándolas.
fuente
ZipCode
?La respuesta depende completamente de lo que realmente quieres hacer con los códigos postales. Aquí hay dos posibilidades extremas:
(1) Todas las direcciones están garantizadas en un solo país. No hay excepciones en absoluto. (Por ejemplo, no hay clientes extranjeros, ni empleados cuya dirección privada está en el extranjero mientras trabajan para un cliente extranjero). Este país tiene códigos postales y se espera que nunca sean muy problemáticos (es decir, no requieren entrada de forma libre) como "actualmente D4B 6N2, pero esto cambia cada 2 semanas"). Los códigos postales se utilizan no solo para direccionar, sino también para validar la información de pago o fines similares. - En estas circunstancias, una clase de código postal tiene mucho sentido.
(2) Las direcciones pueden estar en casi todos los países, por lo que son relevantes docenas o cientos de esquemas de direcciones con o sin códigos postales (y con miles de raras excepciones y casos especiales). Realmente solo se solicita un código "ZIP" para recordar a las personas de países donde se utilizan los códigos postales que no se olviden de proporcionar el suyo. Las direcciones solo se usan para que si alguien pierde el acceso a su cuenta y puede probar su nombre y dirección, se restablecerá el acceso. - En estas circunstancias, las clases de código postal para todos los países relevantes serían un esfuerzo enorme. Afortunadamente no son necesarios en absoluto.
fuente
Las otras respuestas han hablado sobre el modelado de dominio OO y el uso de un tipo más rico para representar su valor.
No estoy en desacuerdo, especialmente dado el código de ejemplo que publicaste.
Pero también me pregunto si eso realmente responde el título de su pregunta.
Considere el siguiente escenario (extraído de un proyecto real en el que estoy trabajando):
Tiene una aplicación remota en un dispositivo de campo que se comunica con su servidor central. Uno de los campos de la base de datos para la entrada del dispositivo es un código postal para la dirección en la que se encuentra el dispositivo de campo. No le importa el código postal (ni ninguna de las demás direcciones). Todas las personas que se preocupan por él están al otro lado de un límite HTTP: resulta que usted es la única fuente de verdad para los datos. No tiene lugar en su modelado de dominio. Solo debe grabarlo, validarlo, almacenarlo y, si lo solicita, mezclarlo en un blob JSON hacia otros puntos.
En este escenario, hacer mucho más que validar la inserción con una restricción de expresión regular SQL (o su equivalente ORM) es probablemente exagerado de la variedad YAGNI.
fuente
RegexValidatedString
, que contiene la cadena en sí y la expresión regular utilizada para validarlo. Pero a menos que cada instancia tenga una expresión regular única (que es posible pero poco probable), esto parece un poco tonto y desperdicia la memoria (y posiblemente el tiempo de compilación de expresiones regulares). Por lo tanto, coloque la expresión regular en una tabla separada y deje una clave de búsqueda en cada instancia para encontrarla (lo que podría decirse que es peor debido a la indirección) o encuentra alguna forma de almacenarla una vez para cada tipo común de valor que comparte esa regla: - p.ej. un campo estático en un tipo de dominio, o método equivalente, como dijo IMSoP.La
ZipCode
abstracción solo podría tener sentido si suAddress
clase no tuviera también unaTownName
propiedad. De lo contrario, tiene la mitad de una abstracción: el código postal designa la ciudad, pero estos dos bits de información relacionados se encuentran en diferentes clases. No tiene mucho sentido.Sin embargo, incluso entonces, todavía no es una aplicación correcta (o más bien una solución para) la obsesión primitiva; que, según tengo entendido, se centra principalmente en dos cosas:
Tu caso no es ninguno. Una dirección es un concepto bien definido con propiedades claramente necesarias (calle, número, código postal, ciudad, estado, país, ...). Hay poca o ninguna razón para dividir estos datos, ya que tiene una única responsabilidad: designar una ubicación en la Tierra. Una dirección requiere todos estos campos para ser significativa. La mitad de una dirección no tiene sentido.
Así es como sabe que no necesita subdividir más: desglosarlo más podría restar valor a la intención funcional de la
Address
clase. Del mismo modo, no necesita unaName
subclase para usar en laPerson
clase, a menos queName
(sin una persona adjunta) sea un concepto significativo en su dominio. Lo cual (generalmente) no es. Los nombres se utilizan para identificar personas, por lo general no tienen valor por sí mismos.fuente
Name
utilizar una subclase en laPerson
clase, a menos que Nombre (sin una persona adjunta) sea un concepto significativo en su dominio ". Cuando tiene una validación personalizada para los nombres, el nombre se ha convertido en un concepto significativo en su dominio; que mencioné explícitamente como un caso de uso válido para usar un subtipo. En segundo lugar, para la validación del código postal, está introduciendo supuestos adicionales, como los códigos postales que necesitan seguir el formato de un país determinado. Estás abordando un tema que es mucho más amplio que la intención de la pregunta de OP.Del artículo:
El código fuente se lee con mucha más frecuencia de lo que se escribe. Por lo tanto, el problema de yoyo, de tener que cambiar entre muchos archivos es una preocupación.
Sin embargo, no , el problema del yoyo se siente mucho más relevante cuando se trata de módulos o clases profundamente interdependientes (que se intercambian entre sí). Es un tipo especial de pesadilla para leer, y es probablemente lo que tenía en mente el entrenador del problema del yoyo.
Sin embargo, sí , ¡es importante evitar demasiadas capas de abstracción!
Por ejemplo, no estoy de acuerdo con la suposición hecha en la respuesta de mmmaaa de que " no es necesario que yo-yo a [(visita)] la clase ZipCode para comprender la clase Dirección". Mi experiencia ha sido que lo haces , al menos las primeras veces que lees el código. Sin embargo, como otros han señalado, no son momentos en que una
ZipCode
clase es apropiada.YAGNI (Ya no lo va a necesitar) es un mejor patrón a seguir para evitar el código Lasagna (código con demasiadas capas): las abstracciones, como los tipos y las clases, están ahí para ayudar al programador, y no deben usarse a menos que sean Una ayuda.
Mi objetivo personal es "guardar líneas de código" (y, por supuesto, los "guardar archivos / módulos / clases" relacionados, etc.). Estoy seguro de que hay algunos que me aplicarían el epíteto de "obsesivo primitivo". Me resulta más importante tener un código que sea fácil de razonar que preocuparse por las etiquetas, los patrones y los antipatrones. La elección correcta de cuándo crear una función, un módulo / archivo / clase, o poner una función en una ubicación común es muy situacional. Apunto más o menos a 3-100 funciones de línea, 80-500 archivos de línea y "1, 2, n" para código de biblioteca reutilizable ( SLOC - sin incluir comentarios o repeticiones; normalmente quiero al menos 1 mínimo de SLOC adicional por línea de obligatorio repetitivo).
La mayoría de los patrones positivos han surgido de los desarrolladores que hacen exactamente eso, cuando los necesitan . Es mucho más importante aprender a escribir código legible que tratar de aplicar patrones sin el mismo problema que resolver. Cualquier buen desarrollador puede implementar el patrón de fábrica sin haberlo visto antes en el caso poco común de que sea el adecuado para su problema. He usado el patrón de fábrica, el patrón de observación y probablemente cientos además, sin saber su nombre (es decir, ¿hay un "patrón de asignación variable"?). Para un experimento divertido, vea cuántos patrones de GoF están integrados en el lenguaje JS , dejé de contar después de aproximadamente 12-15 en 2009. El patrón Factory es tan simple como devolver un objeto de un constructor JS, por ejemplo, no es necesario un WidgetFactory
Entonces, sí , a veces
ZipCode
es una buena clase. Sin embargo, no , el problema del yoyo no es estrictamente relevante.fuente
El problema de yoyo solo es relevante si tienes que voltear de un lado a otro. Eso es causado por una o dos cosas (a veces ambas):
Si puede mirar el nombre y tener una idea razonable de lo que hace, y los métodos y propiedades hacen cosas razonablemente obvias, no necesita mirar el código, simplemente puede usarlo. En primer lugar, ese es el objetivo de las clases: son piezas de código modulares que se pueden usar y desarrollar de forma aislada. Si tiene que mirar cualquier cosa que no sea la API para que la clase vea lo que hace, es, en el mejor de los casos, una falla parcial.
fuente
Recuerde, no hay bala de plata. Si está escribiendo una aplicación extremadamente simple que necesita ser rastreada rápidamente, entonces una cadena simple puede hacer el trabajo. Sin embargo, en el 98% de las veces, un objeto de valor según lo descrito por Eric Evans en DDD, sería el ajuste perfecto. Puede ver fácilmente todos los beneficios que proporcionan los objetos Value al leer.
fuente