Reducir la complejidad de una clase.

10

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
LSerni
fuente
3
O bien, puede ignorar la advertencia de "complejidad máxima excedida".
Robert Harvey
Si pudiera lo haría. De alguna manera, esta métrica ha adquirido una calidad sagrada por sí misma. Continúo intentando convencer al PTB para que los acepte como una herramienta útil, pero solo como una herramienta. Sin embargo, un próspero ...
LSerni
¿Las operaciones realmente están construyendo un objeto, o simplemente estás usando la metáfora de la línea de ensamblaje porque comparte la propiedad específica que mencionas (muchas operaciones con un orden de dependencia parcial)? Lo importante es que el primero sugiere el patrón de construcción, mientras que el segundo podría sugerir algún otro patrón con más detalles completados.
es el
2
La tiranía de las métricas. Las métricas son indicadores útiles, pero hacerlas cumplir mientras ignora por qué se usa esa métrica no es útil.
Jaydee
2
Explique a la gente de arriba la diferencia entre la complejidad esencial y la complejidad accidental. en.wikipedia.org/wiki/No_Silver_Bullet Si está seguro de que la complejidad que está rompiendo la regla es esencial, entonces si refactoriza por programadores.stackexchange.com/a/297414/51948 posiblemente esté patinando alrededor de la regla y extendiéndose fuera de la complejidad. Si encapsular las cosas en fases no es arbitrario (las fases se relacionan con el problema) y el rediseño reduce la carga cognitiva para los desarrolladores que mantienen el código, entonces tiene sentido hacerlo.
Fuhrmanator

Respuestas:

15

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 Carclase:

public class Car
{
    public IChassis Chassis { get; private set; }
    public Dashboard { get; private set; }
    public IList<Seat> Seats { get; private set; }

    public Car()
    {
        Seats = new List<Seat>();
    }

    public void AddChassis(IChassis chassis)
    {
        Chassis = chassis;
    }

    public void AddDashboard(Dashboard dashboard)
    {
        Dashboard = dashboard;
    }
}

Voy a dejar de lado las implementaciones de algunos de los componentes, como IChassis, Seaty Dashboarden aras de la brevedad.

El Carno es responsable de construirse a sí mismo. La línea de ensamblaje es, así que encapsulemos eso en una clase:

public class CarAssembler
{
    protected List<IAssemblyPhase> Phases { get; private set; }

    public CarAssembler()
    {
        Phases = new List<IAssemblyPhase>()
        {
            new ChassisAssemblyPhase(),
            new DashboardAssemblyPhase(),
            new SeatAssemblyPhase()
        };
    }

    public void Assemble(Car car)
    {
        foreach (IAssemblyPhase phase in Phases)
        {
            phase.Assemble(car);
        }
    }
}

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 al Assemblepasar en el automóvil. Hecho. La IAssemblyPhaseinterfaz también es muy simple con un solo método público que toma un objeto Car:

public interface IAssemblyPhase
{
    void Assemble(Car car);
}

Ahora necesitamos que nuestras clases concretas implementen esta interfaz para cada fase específica:

public class ChassisAssemblyPhase : IAssemblyPhase
{
    public void Assemble(Car car)
    {
        car.AddChassis(new UnibodyChassis());
    }
}

public class DashboardAssemblyPhase : IAssemblyPhase
{
    public void Assemble(Car car)
    {
        Dashboard dashboard = new Dashboard();

        dashboard.AddComponent(new Speedometer());
        dashboard.AddComponent(new FuelGuage());
        dashboard.Trim = DashboardTrimType.Aluminum;

        car.AddDashboard(dashboard);
    }
}

public class SeatAssemblyPhase : IAssemblyPhase
{
    public void Assemble(Car car)
    {
        car.Seats.Add(new Seat());
        car.Seats.Add(new Seat());
        car.Seats.Add(new Seat());
        car.Seats.Add(new Seat());
    }
}

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:

Car car = new Car();
CarAssembler assemblyLine = new CarAssembler();

assemblyLine.Assemble(car);

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.


¿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?

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.

¿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?

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.

Greg Burghardt
fuente
1
Sí, mucho mejor que tener una clase de dios.
Bћовић
Este enfoque funciona, pero principalmente porque puede obtener los elementos para el automóvil sin parámetros. ¿Qué pasa si necesita pasarlos? Luego puede tirar todo esto por la ventana y repensar completamente el procedimiento, porque realmente no tiene una fábrica de automóviles con 150 parámetros, eso es una locura.
Andy
@DavidPacker: incluso si debe parametrizar un montón de cosas, también puede encapsular eso en algún tipo de clase de configuración que pase al constructor de su objeto "ensamblador". En este ejemplo de automóvil, el color de la pintura, el color interior y los materiales, el paquete de acabado, el motor y mucho más se pueden personalizar, pero no necesariamente tendrá 150 personalizaciones. Probablemente tendrá una docena más o menos, que se presta a un objeto de opciones.
Greg Burghardt
Pero solo agregar la configuración desafiaría el propósito del bucle for hermoso, lo cual es una vergüenza tremenda, porque tendrías que analizar la configuración y asignarla a los métodos adecuados que a cambio te darían los objetos adecuados. Entonces, probablemente terminarías con algo bastante similar a lo que era el diseño original.
Andy
Veo. En lugar de tener una clase y cincuenta métodos, en realidad tendría cincuenta clases lean de ensamblador y una clase de orquestación. Al final, el resultado parece el mismo, también en cuanto al rendimiento, pero el diseño del código es más limpio. Me gusta. Será un poco más incómodo agregar operaciones (tendré que configurarlas en su clase, además de informar a la clase de orquestación; no veo una manera fácil de salir de esta necesidad), pero aún así me gusta mucho. Permítame un par de días para explorar este enfoque.
LSerni
1

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.

Erik Eidt
fuente
Algunas de las operaciones se implementan realmente de esta manera (como listas de reglas). Para seguir con el ejemplo del automóvil, cuando instalo una caja de fusibles, luces y enchufes, en realidad no utilizo tres métodos diferentes, sino solo uno, que generalmente encaja en los enchufes eléctricos de dos clavijas, y toma la Lista de tres Listas de fusibles , luces y enchufes. Lamentablemente, la gran mayoría de los otros métodos no son fácilmente susceptibles a este enfoque.
LSerni
1

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.

Sjoerd Job Postmus
fuente
0

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

MyObject.AddComponent(IComponent component) 

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:

  1. Tire OOP por la ventana y tenga servicios que operan con los datos expuestos en su clase, es decir.

    Service.AddDoorToCar (Automóvil)

  2. 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)

  1. Adopte un enfoque funcional y pase funciones / delegados a un método de modificación

    Car.Modify ((c) => c.doors ++);

Ewan
fuente
Ewan dijo: "Tire OOP por la ventana y tenga servicios que operen con los datos expuestos en su clase" --- ¿Cómo no está orientado a objetos?
Greg Burghardt
?? no es, no es una opción OO
Ewan
Estás utilizando objetos, lo que significa que está "orientado a objetos". El hecho de que no pueda identificar un patrón de programación para su código no significa que no esté orientado a objetos. Los principios SÓLIDOS son una buena regla para seguir. Si implementa algunos o todos los principios SOLID, está orientado a objetos. Realmente, si está utilizando objetos y no solo pasando estructuras a diferentes procedimientos o métodos estáticos, está orientado a objetos. Hay una diferencia entre "código orientado a objetos" y "buen código". Puede escribir código orientado a objetos incorrectos.
Greg Burghardt
es 'no OO' porque está exponiendo los datos como si fuera una estructura y operando en una clase separada como en un procedimiento
Ewan
tbh solo agregué el comentario principal para tratar de detener el inevitable "¡eso no es OOP!" comentarios
Ewan