Estoy trabajando en un diseño, pero sigo llegando a un obstáculo. Tengo una clase particular (ModelDef) que es esencialmente el propietario de un árbol de nodos complejo construido al analizar un esquema XML (piense en DOM). Quiero seguir buenos principios de diseño (SÓLIDO) y asegurarme de que el sistema resultante sea fácilmente comprobable. Tengo toda la intención de usar DI para pasar dependencias al constructor de ModelDef (para que estas puedan cambiarse fácilmente, si es necesario, durante las pruebas).
Sin embargo, con lo que estoy luchando es con la creación del árbol de nodos. Este árbol estará compuesto completamente por objetos simples de "valor" que no necesitarán ser probados independientemente. (Sin embargo, aún puedo pasar una Fábrica abstracta a ModelDef para ayudar con la creación de estos objetos).
Pero sigo leyendo que un constructor no debe hacer ningún trabajo real (por ejemplo, Falla: el constructor hace un trabajo real ). Esto tiene mucho sentido para mí si "trabajo real" significa la construcción de objetos dependientes de gran peso que luego uno podría querer probar. (Deben pasarse a través de DI).
Pero, ¿qué pasa con los objetos de valor ligero como este árbol de nodos? El árbol tiene que ser creado en alguna parte, ¿verdad? ¿Por qué no a través del constructor de ModelDef (usando, por ejemplo, un método buildNodeTree ())?
Realmente no quiero crear el árbol de nodos fuera de ModelDef y luego pasarlo (a través del constructor DI), porque crear el árbol de nodos analizando el esquema requiere una cantidad significativa de código complejo, código que debe ser probado a fondo . No quiero relegarlo al código "pegado" (que debería ser relativamente trivial y probablemente no se probará directamente).
He pensado poner el código para crear el árbol de nodos en un objeto "constructor" separado, pero dudo en llamarlo "constructor", porque realmente no coincide con el Patrón de constructor (que parece estar más preocupado por eliminar el telescopio) constructores). Pero incluso si lo llamé algo diferente (por ejemplo, NodeTreeConstructor), todavía se siente como un truco solo para evitar que el constructor ModelDef construya el árbol de nodos. Tiene que ser construido en alguna parte; ¿Por qué no en el objeto que lo va a poseer?
Respuestas:
Y, además de lo que sugirió Ross Patterson, considere esta posición, que es exactamente lo contrario:
Tome máximas como "No harás ningún trabajo real en tus constructores" con un grano de sal.
Un constructor no es más que un método estático. Entonces, estructuralmente, realmente no hay mucha diferencia entre:
a) un constructor simple y un montón de métodos complejos de fábrica estática, y
b) un constructor simple y un grupo de constructores más complejos.
Una parte considerable del sentimiento negativo hacia cualquier trabajo real en constructores proviene de un cierto período de la historia de C ++ cuando hubo un debate sobre exactamente en qué estado quedará el objeto si se produce una excepción dentro del constructor, y si el destructor debe ser invocado en tal evento. Esa parte de la historia de C ++ ha terminado, y el problema se ha resuelto, mientras que en lenguajes como Java nunca hubo ningún problema de este tipo para empezar.
Mi opinión es que si simplemente evita usar
new
en el constructor, (como lo indica su intención de emplear la inyección de dependencia), debería estar bien. Me río de declaraciones como "la lógica condicional o en bucle en un constructor es una señal de advertencia de una falla".Además de todo eso, personalmente, eliminaría la lógica de análisis XML del constructor, no porque sea malo tener una lógica compleja en un constructor, sino porque es bueno seguir el principio de "separación de preocupaciones". Por lo tanto, movería la lógica de análisis XML a una clase separada por completo, no a algunos métodos estáticos que pertenecen a su
ModelDef
clase.Enmienda
Supongo que si tiene un método fuera del
ModelDef
cual crea unModelDef
archivo XML, necesitará crear una instancia de una estructura de datos de árbol temporal dinámica, llenarla analizando su XML y luego crear su nuevaModelDef
transferencia de esa estructura como un parámetro de construcción. Por lo tanto, eso podría considerarse como una aplicación del patrón "Constructor". Hay una analogía muy estrecha entre lo que quieres hacer y elString
&StringBuilder
pair. Sin embargo, he encontrado estas preguntas y respuestas que parecen estar en desacuerdo, por razones que no tengo claras: Stackoverflow - StringBuilder y Builder Pattern . Entonces, para evitar un largo debate aquí sobre siStringBuilder
implementa o no el patrón de "constructor", diría que siéntase libre de inspirarse en cómoStrungBuilder
trabaja para encontrar una solución que se adapte a sus necesidades y posponer llamarla una aplicación del patrón "Generador" hasta que se resuelva ese pequeño detalle.Vea esta nueva pregunta: Programadores SE: ¿Es “StringBuilder” una aplicación del Patrón de diseño del generador?
fuente
Ya da las mejores razones para no hacer este trabajo en el
ModelDef
constructor:ModelDef
que diga que solo se puede crear a partir de un documento XML.Parece que su clase debe tener una variedad de métodos estáticos como
ModelDef.FromXmlString(string xmlDocument)
,ModelDef.FromXmlDoc(XmlDoc parsedNodeTree)
, etc.fuente
public static ModelDef createFromXmlString( string xmlDocument )
. Esta es una práctica bastante común. A veces yo también lo hago. Mi sugerencia de que se puede también hacer constructores sólo es un tipo estándar de la respuesta de las minas en situaciones en las que sospecho que los enfoques alternativos son despedidos como "no kosher" sin una buena razón.He escuchado esa "regla" antes. En mi experiencia, es tanto verdadero como falso.
En una orientación de objeto más "clásica" hablamos de objetos que encapsulan el estado y el comportamiento. Por lo tanto, un constructor de objetos debe garantizar que el objeto se inicialice a un estado válido (y señalar un error si los argumentos proporcionados no hacen que el objeto sea válido). Asegurarme de que un objeto se inicialice a un estado válido seguramente me suena a trabajo real. Y esta idea tiene méritos, si tiene un objeto que solo permite la inicialización a un estado válido a través del constructor y el objeto lo encapsula adecuadamente para que cada método que cambia el estado también verifique que no cambie el estado a algo malo ... entonces ese objeto en esencia garantiza que es "siempre válido". Esa es una muy buena propiedad!
Entonces, el problema generalmente llega cuando tratamos de separar todo en pedazos pequeños para probar y burlarnos de cosas. Porque si un objeto está realmente encapsulado correctamente, entonces no puede entrar allí y reemplazar el FooBarService con su FooBarService burlado y usted (probablemente) no puede simplemente cambiar los valores de forma intencionada para adaptarse a sus pruebas (o puede tomar un mucho más código que una simple tarea).
Así obtenemos la otra "escuela de pensamiento", que es SÓLIDA. Y en esta escuela de pensamiento, lo más probable es que sea mucho más cierto que no deberíamos hacer un trabajo real en el constructor. El código SOLID es a menudo (pero no siempre) más fácil de probar. Pero también puede ser más difícil de razonar. Separamos nuestro código en pequeños objetos con una sola responsabilidad y, por lo tanto, la mayoría de nuestros objetos ya no encapsulan su estado (y generalmente contienen estado o comportamiento). El código de validación generalmente se extrae en una clase de validador y se mantiene separado del estado. Pero ahora que hemos perdido la cohesión, ya no podemos estar seguros de que nuestros objetos son válidos cuando los obtenemos y de estar completamenteseguro que siempre debemos validar que las condiciones previas que creemos que tenemos sobre el objeto son verdaderas antes de intentar hacer algo con el objeto. (Por supuesto, en general se valida en una capa y luego se supone que el objeto es válido en las capas inferiores). ¡Pero es más fácil de probar!
Entonces, ¿quién tiene razón?
Nadie realmente Ambas escuelas de pensamiento tienen sus méritos. Actualmente SOLID está de moda y todos están hablando de SRP y Open / Closed y todo ese jazz. Pero el hecho de que algo sea popular no significa que sea la elección de diseño correcta para cada aplicación. Entonces eso depende. Si está trabajando en una base de código que sigue en gran medida los principios de SOLID, entonces sí, el trabajo real en el constructor es probablemente una mala idea. Pero de lo contrario, mire la situación e intente usar su juicio. ¿Qué propiedades obtiene su objeto al trabajar en el constructor, qué propiedades pierde ? ¿Qué tan bien encaja con la arquitectura general de su aplicación?
El trabajo real en el constructor no es un antipatrón, puede ser todo lo contrario cuando se usa en los lugares correctos. Pero debe documentarse claramente (junto con las excepciones que se pueden lanzar, si las hay) y, como con cualquier decisión de diseño, debe ajustarse al estilo general utilizado en la base del código actual.
fuente
Hay un problema fundamental con esta regla y es esto, ¿qué constituye el "trabajo real"?
Puede ver en el artículo original publicado en la pregunta que el autor intenta definir qué es "trabajo real", pero es gravemente defectuoso. Para que una práctica sea buena, debe ser un principio bien definido. Con eso quiero decir que, en lo que respecta a la ingeniería de software, la idea debe ser portátil (independiente de cualquier idioma), probada y comprobada. La mayor parte de lo que se argumenta en ese artículo no se ajusta a ese primer criterio. Aquí hay algunos indicadores que el autor menciona en ese artículo de lo que constituye "trabajo real" y por qué no son malas definiciones.
Uso de la
new
palabra clave . Esa definición es fundamentalmente defectuosa porque es específica del dominio. Algunos idiomas no usan lanew
palabra clave. En última instancia, lo que está insinuando es que no debería estar construyendo otros objetos. Sin embargo, en muchos idiomas, incluso los valores más básicos son en sí mismos objetos. Entonces, cualquier valor asignado en el constructor también está construyendo un nuevo objeto. Eso hace que esta idea se limite a ciertos idiomas, y un mal indicador de lo que constituye "trabajo real".Objeto no completamente inicializado después de que el constructor termina . Esta es una buena regla, pero también contradice varias de las otras reglas mencionadas en ese artículo. Un buen ejemplo de cómo eso podría contradecir a los demás es la pregunta que me trajo aquí. En esa pregunta, a alguien le preocupa usar el
sort
método en un constructor en lo que parece ser JavaScript debido a este principio. En este ejemplo, la persona estaba creando un objeto que representaba una lista ordenada de otros objetos. Para fines de discusión, imagine que tenemos una lista de objetos sin clasificar y que necesitamos un nuevo objeto para representar una lista ordenada. Necesitamos este nuevo objeto porque alguna parte de nuestro software espera una lista ordenada, y llamemos a este objetoSortedList
. Este nuevo objeto acepta una lista sin clasificar y el objeto resultante debe representar una lista de objetos ahora ordenada. Si tuviéramos que seguir las otras reglas mencionadas en ese documento, es decir, sin llamadas a métodos estáticos, sin estructuras de flujo de control, nada más que asignación, entonces el objeto resultante no se construiría en un estado válido que rompa la otra regla de que se inicialice completamente después de que el constructor termine. Para solucionar esto, tendríamos que hacer un trabajo básico para ordenar la lista sin ordenar en el constructor. Hacer esto rompería las otras 3 reglas, haciendo que las otras reglas sean irrelevantes.En última instancia, esta regla de no hacer "trabajo real" en un constructor está mal definida y tiene fallas. Tratar de definir qué "trabajo real" es un ejercicio inútil. La mejor regla en ese artículo es que cuando un constructor termina, debe inicializarse completamente. Hay una gran cantidad de otras mejores prácticas que limitarían la cantidad de trabajo realizado en un constructor. La mayoría de estos pueden resumirse en principios SÓLIDOS, y esos mismos principios no le impedirán trabajar en el constructor.
PD. Me siento obligado a decir que si bien afirmo aquí que no hay nada de malo en hacer un trabajo en el constructor, tampoco es el lugar para hacer un montón de trabajo. SRP sugeriría que un constructor debería hacer el trabajo suficiente para que sea válido. Si su constructor tiene demasiadas líneas de código (muy subjetivo, lo sé), entonces probablemente esté violando este principio, y probablemente podría dividirse en métodos y objetos más pequeños y mejor definidos.
fuente