Miré algunas respuestas y busqué en Google, pero no pude encontrar nada útil (es decir, eso no tendría efectos secundarios incómodos).
Mi problema, en resumen, es que tengo un objeto y necesito realizar una larga secuencia de operaciones en él; Lo considero como una especie de línea de montaje, como construir un automóvil.
Creo que estos objetos se llamarían objetos de método .
Entonces, en este ejemplo, en algún momento tendría un CarWithoutUpholstery en el que luego necesitaría ejecutar installBackSeat, installFrontSeat, installWoodenInserts (las operaciones no interfieren entre sí, e incluso podrían hacerse en paralelo). Estas operaciones son realizadas por CarWithoutUpholstery.worker () y producen un nuevo objeto que sería un CarWithUpholstery, en el que luego podría ejecutar cleanInsides (), verificar NoUpholsteryDefects (), y así sucesivamente.
Las operaciones en una sola fase ya son independientes, es decir, ya estoy lidiando con un subconjunto de ellas que puede ejecutarse en cualquier orden (los asientos delanteros y traseros pueden instalarse en cualquier orden).
Mi lógica actualmente usa Reflection para simplificar la implementación.
Es decir, una vez que tengo un CarWithoutUpholstery, el objeto se inspecciona a sí mismo en busca de métodos llamados performSomething (). En ese punto, ejecuta todos estos métodos:
myObject.perform001SomeOperation();
myObject.perform002SomeOtherOperation();
...
mientras revisa errores y esas cosas. Si bien el orden de operación no es importante, he asignado un orden lexicográfico en caso de que descubra que algún orden es importante después de todo. Esto contradice a YAGNI , pero costó muy poco, un tipo simple (), y podría ahorrar un cambio de nombre de método masivo (o introducir algún otro método para realizar pruebas, por ejemplo, una serie de métodos) en el futuro.
Un ejemplo diferente
Digamos que en lugar de construir un automóvil, tengo que compilar un informe de la Policía Secreta sobre alguien y presentarlo a mi Señor Supremo Maligno . Mi objeto final será un ReadyReport. Para construirlo empiezo reuniendo información básica (nombre, apellido, cónyuge ...). Esta es mi Fase A. Dependiendo de si hay un cónyuge o no, es posible que tenga que pasar a las fases B1 o B2 y recopilar datos de sexualidad de una o dos personas. Esto está hecho de varias consultas diferentes a diferentes Evil Minions que controlan la vida nocturna, las cámaras de la calle, los recibos de ventas de la tienda de sexo y lo que no. Y así sucesivamente y así sucesivamente.
Si la víctima no tiene familia, ni siquiera entraré en la fase GetInformationAboutFamily, pero si lo tengo, entonces es irrelevante si primero apunto al padre o la madre o los hermanos (si los hay). Pero no puedo hacer eso si no he realizado un FamilyStatusCheck, que por lo tanto pertenece a una fase anterior.
Todo funciona de maravilla ...
- si necesito alguna operación adicional solo necesito agregar un método privado,
- Si la operación es común a varias fases, puedo heredarla de una superclase,
- Las operaciones son simples y autónomas. Ningún valor de una operación es siempre requerido por cualquiera de los otros (operaciones que no se realizan en una fase diferente),
- los objetos en el futuro no necesitan realizar muchas pruebas ya que ni siquiera podrían existir si sus objetos creadores no hubieran verificado esas condiciones en primer lugar. Es decir, cuando coloque inserciones en el tablero, limpie el tablero y verifique el tablero, no necesito verificar que haya un tablero realmente allí .
- Permite una prueba fácil. Puedo burlarme fácilmente de un objeto parcial y ejecutar cualquier método sobre él, y todas las operaciones son cuadros negros deterministas.
...pero...
El problema surgió cuando agregué una última operación en uno de mis objetos de método, lo que provocó que el módulo general excediera un índice de complejidad obligatorio ("menos de N métodos privados").
Ya llevé el asunto arriba y sugerí que, en este caso, la riqueza de los métodos privados no es una señal de desastre. La complejidad está ahí, pero está ahí porque la operación es compleja y, en realidad, no es tan compleja, solo es larga .
Usando el ejemplo de Evil Overlord, mi problema es que Evil Overlord (también conocido como El que no se le negará ) ha solicitado toda la información dietética, mis Minions dietéticos me dicen que necesito consultar restaurantes, cocinas, vendedores ambulantes, vendedores ambulantes sin licencia, invernadero propietarios, etc., y el malvado (sub) Overlord, conocido familiarmente como Él que tampoco se le negará, se queja de que estoy realizando demasiadas consultas en la fase GetDietaryInformation.
Nota : Soy consciente de que desde varios puntos de vista esto no es un problema en absoluto (ignorando posibles problemas de rendimiento, etc.). Todo lo que sucede es que una métrica específica es infeliz, y hay justificación para eso.
Lo que creo que podría hacer
Aparte de la primera, todas estas opciones son factibles y, creo, defendibles.
- He verificado que puedo ser astuto y declarar la mitad de mis métodos
protected
. Pero estaría explotando una debilidad en el procedimiento de prueba, y aparte de justificarme cuando me atrapan, no me gusta esto. Además, es una medida provisional. ¿Qué pasa si se duplica el número de operaciones requeridas? Improbable, pero ¿entonces qué? - Puedo dividir arbitrariamente esta fase en AnnealedObjectAlpha, AnnealedObjectBravo y AnnealedObjectCharlie, y hacer que un tercio de las operaciones se realicen en cada etapa. Tengo la impresión de que esto realmente agrega complejidad (N-1 más clases), sin ningún beneficio, excepto pasar una prueba. Por supuesto, puedo sostener que un CarWithFrontSeatsInstalled y un CarWithAllSeatsInstalled son etapas lógicamente sucesivas . El riesgo de que Alpha requiera más tarde un método Bravo es pequeño, e incluso menor si lo juego bien. Pero aún.
- Puedo agrupar diferentes operaciones, remotamente similares, en una sola.
performAllSeatsInstallation()
. Esta es solo una medida provisional y aumenta la complejidad de la operación individual. Si alguna vez necesito hacer las operaciones A y B en un orden diferente, y las he empaquetado dentro de E = (A + C) y F (B + D), tendré que separar E y F y barajar el código . - Puedo usar una variedad de funciones lambda y esquivar el cheque por completo, pero eso me parece torpe. Sin embargo, esta es la mejor alternativa hasta ahora. Se libraría de la reflexión. Los dos problemas que tengo es que probablemente me pedirán que reescriba todos los objetos del método, no solo el hipotético
CarWithEngineInstalled
, y aunque eso sería una muy buena seguridad laboral, realmente no es tan atractivo; y que el verificador de cobertura de código tiene problemas con lambdas (que son solucionables, pero aún así ).
Entonces...
- ¿Cuál crees que es mi mejor opción?
- ¿Hay alguna forma mejor que no haya considerado? ( tal vez sea mejor que me limpie y pregunte directamente ¿qué es? )
- ¿Es este diseño irremediablemente defectuoso, y mejor admito la derrota y la zanja, esta arquitectura por completo? No es bueno para mi carrera, pero ¿sería mejor escribir código mal diseñado a largo plazo?
- ¿Mi elección actual es en realidad la One True Way, y necesito luchar para obtener métricas (y / o instrumentación) de mejor calidad? Para esta última opción, necesitaría referencias ... No puedo simplemente saludar a @PHB mientras murmuro Estas no son las métricas que estás buscando . No importa cuánto me gustaría poder
Respuestas:
La larga secuencia de operaciones parece que debería ser su propia clase, que luego necesita objetos adicionales para realizar sus tareas. Parece que estás cerca de esta solución. Este largo procedimiento puede dividirse en múltiples pasos o fases. Cada paso o fase podría ser su propia clase que expone un método público. Desearía que cada fase implemente la misma interfaz. Luego, su línea de montaje realiza un seguimiento de una lista de fases.
Para construir sobre el ejemplo de ensamblaje de su automóvil, imagine la
Car
clase:Voy a dejar de lado las implementaciones de algunos de los componentes, como
IChassis
,Seat
yDashboard
en aras de la brevedad.El
Car
no es responsable de construirse a sí mismo. La línea de ensamblaje es, así que encapsulemos eso en una clase:La distinción importante aquí es que CarAssembler tiene una lista de objetos de fase de ensamblaje que se implementan
IAssemblyPhase
. Ahora está tratando explícitamente con métodos públicos, lo que significa que cada fase puede probarse por sí misma, y su herramienta de calidad de código es más feliz porque no está metiendo demasiado en una sola clase. El proceso de montaje también es muy simple. Simplemente recorra las fases y llame alAssemble
pasar en el automóvil. Hecho. LaIAssemblyPhase
interfaz también es muy simple con un solo método público que toma un objeto Car:Ahora necesitamos que nuestras clases concretas implementen esta interfaz para cada fase específica:
Cada fase individualmente puede ser bastante simple. Algunos son solo un revestimiento. Algunos podrían ser simplemente cegadores al agregar cuatro asientos, y otro tiene que configurar el tablero antes de agregarlo al automóvil. La complejidad de este proceso prolongado se divide en múltiples clases reutilizables que son fácilmente comprobables.
Aunque dijiste que el orden no importa en este momento, podría serlo en el futuro.
Para terminar esto, veamos el proceso para ensamblar un automóvil:
Puede subclase CarAssembler para ensamblar un tipo específico de automóvil o camión. Cada fase es una unidad dentro de un todo más grande, lo que facilita la reutilización del código.
Si decidir que lo que escribí necesita ser reescrito fue una marca negra en mi carrera, entonces estaría trabajando como cavador de zanjas en este momento, y no debería seguir ninguno de mis consejos. :)
La ingeniería de software es un proceso eterno de aprendizaje, aplicación y reaprendizaje. El hecho de que decida reescribir su código no significa que vaya a ser despedido. Si ese fuera el caso, no habría ingenieros de software.
Lo que ha esbozado no es One True Way, sin embargo, tenga en cuenta que las métricas de código tampoco son One True Way. Las métricas de código deben verse como advertencias. Pueden señalar problemas de mantenimiento en el futuro. Son pautas, no leyes. Cuando su herramienta de métricas de código señala algo, primero investigaría el código para ver si implementa correctamente los principios SOLID . Muchas veces, refactorizar el código para hacerlo más SÓLIDO satisfará las herramientas de métricas de código. A veces no. Debe tomar estas métricas caso por caso.
fuente
No sé si esto es posible para usted, pero estaría pensando en usar un enfoque más orientado a los datos. La idea es que capture alguna expresión (declarativa) de reglas y restricciones, operaciones, dependencias, estado, etc. Entonces sus clases y código son más genéricos, orientados a seguir las reglas, inicializando el estado actual de las instancias, realizando operaciones para cambiar el estado, mediante el uso de la captura declarativa de reglas y cambios de estado en lugar de utilizar el código más directamente. Uno podría usar declaraciones de datos en el lenguaje de programación, o algunas herramientas DSL, o un motor de reglas o lógica.
fuente
De alguna manera, el problema me recuerda a las dependencias. Desea un automóvil con una configuración específica. Al igual que Greg Burghardt, iría con objetos / clases para cada paso / elemento / ...
Cada paso declara lo que agrega a la mezcla (qué es
provides
) (puede ser varias cosas), pero también lo que requiere.Luego, define la configuración final requerida en alguna parte, y tiene un algoritmo que analiza lo que necesita y decide qué pasos deben ejecutarse / qué partes deben agregarse / qué documentos deben recopilarse.
Los algoritmos de resolución de dependencia no son tan difíciles. El caso más simple es cuando cada paso proporciona una cosa, y no hay dos cosas que proporcionen la misma cosa, y las dependencias son simples (no 'o' en las dependencias). Para los casos más complejos: solo mire una herramienta como debian apt o algo así.
Finalmente, construir su automóvil se reduce a los posibles pasos que hay y dejar que CarBuilder descubra los pasos necesarios.
Sin embargo, una cosa importante es que debe encontrar una manera de informar al CarBuilder sobre todos los pasos. Intenta hacer esto usando un archivo de configuración. Agregar un paso adicional solo necesitaría una adición al archivo de configuración. Si necesita lógica, intente agregar un DSL en el archivo de configuración. Si todo lo demás falla, estás atascado definiéndolos en el código.
fuente
Un par de cosas que mencionas se destacan como 'malas' para mí
"Mi lógica actualmente usa Reflection para simplificar la implementación"
Realmente no hay excusa para esto. ¿De verdad estás usando una comparación de cadenas de nombres de métodos para determinar qué ejecutar? si cambia el orden, ¿tendrá que cambiar el nombre del método?
"Las operaciones en una sola fase ya son independientes"
Si son verdaderamente independientes entre sí, sugiere que su clase ha asumido más de una responsabilidad. Para mí, ambos ejemplos parecen prestarse a algún tipo de single
enfoque que le permitiría dividir la lógica de cada operación en su propia clase o clases.
De hecho, imagino que no son realmente independientes, imagino que cada uno examina el estado del objeto y lo modifica, o al menos realiza algún tipo de validación antes de comenzar.
En este caso puedes:
Tire OOP por la ventana y tenga servicios que operan con los datos expuestos en su clase, es decir.
Service.AddDoorToCar (Automóvil)
Tenga una clase de constructor que construya su objeto reuniendo primero los datos necesarios y devolviendo el objeto completado, es decir.
Car = CarBuilder.WithDoor (). WithDoor (). WithWheels ();
(la lógica withX puede dividirse nuevamente en subconstructores)
Adopte un enfoque funcional y pase funciones / delegados a un método de modificación
Car.Modify ((c) => c.doors ++);
fuente