¿Un "trabajo real" legítimo en un constructor?

23

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?

Gurtz
fuente
77
Sin embargo, siempre debes tener cuidado con las declaraciones generales como esa. La regla general es el código de manera clara, funcional, fácil de probar, reutilizar y mantener, cualquiera que sea, que varía según su situación. Si no gana nada más que complejidad de código y confusión al tratar de seguir una "regla" como esa, entonces no era una regla apropiada para su situación. Todos estos "patrones" y características del lenguaje son herramientas; use el mejor para su trabajo específico.
Jason C

Respuestas:

26

Y, además de lo que sugirió Ross Patterson, considere esta posición, que es exactamente lo contrario:

  1. Tome máximas como "No harás ningún trabajo real en tus constructores" con un grano de sal.

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

Enmienda

Supongo que si tiene un método fuera del ModelDefcual crea un ModelDefarchivo XML, necesitará crear una instancia de una estructura de datos de árbol temporal dinámica, llenarla analizando su XML y luego crear su nueva ModelDeftransferencia 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 el String& StringBuilderpair. 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 si StringBuilderimplementa 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?

Mike Nakis
fuente
3
@ RichardLevasseur Solo lo recuerdo como un tema de preocupación y debate entre los programadores de C ++ a principios y mediados de los noventa. Si miras esta publicación: gotw.ca/gotw/066.htm verás que es bastante complicado, y las cosas bastante complicadas tienden a ser controvertidas. No lo sé con certeza, pero creo que a principios de los noventa parte de esas cosas aún no se habían estandarizado. Pero lo siento, no puedo proporcionar una buena referencia.
Mike Nakis
1
@Gurtz Yo pensaría en una clase como "utilidades xml" específicas de la aplicación, ya que el formato del archivo xml (o la estructura del documento) probablemente esté vinculado a la aplicación particular que está desarrollando, independientemente de las posibilidades de reutilice su "ModelDef".
Mike Nakis
1
@Gurtz, entonces, probablemente los haría métodos de instancia de la clase principal "Aplicación", o si eso es demasiado complicado, entonces métodos estáticos de alguna clase auxiliar, de una manera muy similar a lo que sugirió Ross Patterson.
Mike Nakis
1
@Gurtz se disculpa por no abordar específicamente el enfoque de "constructor" anteriormente. Modifiqué mi respuesta.
Mike Nakis
3
@Gurtz Es posible, pero fuera de la curiosidad académica, no importa. No se deje engañar por el "patrón antipatrón". Los patrones son realmente solo nombres para describir técnicas de codificación comunes / útiles a otros convenientemente. Haga lo que necesite hacer, pegue una etiqueta más adelante si necesita describirlo. Está perfectamente bien implementar algo que sea "algo así como el patrón de construcción, de alguna manera, tal vez", siempre que su código tenga sentido. Es razonable centrarse en los patrones cuando se aprenden nuevas técnicas, simplemente no caigas en la trampa de pensar que todo lo que haces debe ser un patrón con nombre.
Jason C
9

Ya da las mejores razones para no hacer este trabajo en el ModelDefconstructor:

  1. No hay nada "ligero" en analizar un documento XML en un árbol de nodos.
  2. No hay nada obvio en una ModelDefque 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.

Ross Patterson
fuente
¡Gracias por la respuesta! En cuanto a la sugerencia de métodos estáticos. ¿Serían estas fábricas estáticas que crean una instancia de ModelDef (de las diversas fuentes xml)? ¿O serían responsables de cargar un objeto ModelDef ya creado? Si es lo último, me preocuparía tener el objeto solo parcialmente inicializado (ya que un ModelDef necesita un árbol de nodos para ser completamente inicializado). Pensamientos?
Gurtz
3
Disculpe por entrometerse, pero sí, lo que Ross quiere decir son métodos estáticos de fábrica que devuelven instancias completamente construidas. El prototipo completo sería algo así 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.
Mike Nakis
1
@ Mike-Nakis, gracias por aclarar. Entonces, en este caso, el método de fábrica estático construiría el árbol de nodos y luego lo pasaría al constructor (posiblemente privado) de ModelDef. Tener sentido. Gracias.
Gurtz
@Gurtz Exactamente.
Ross Patterson
5

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.

wasatz
fuente
Esta es una respuesta fantástica.
jrahhali
0

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 newpalabra clave . Esa definición es fundamentalmente defectuosa porque es específica del dominio. Algunos idiomas no usan la newpalabra 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 sortmé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.

zquintana
fuente