Estoy en un proyecto de sistema distribuido escrito en Java donde tenemos algunas clases que corresponden a objetos comerciales muy complejos del mundo real. Estos objetos tienen muchos métodos que corresponden a acciones que el usuario (o algún otro agente) puede aplicar a esos objetos. Como resultado, estas clases se volvieron muy complejas.
El enfoque de la arquitectura general del sistema ha llevado a muchos comportamientos concentrados en algunas clases y muchos escenarios de interacción posibles.
Como ejemplo y para mantener las cosas fáciles y claras, digamos que Robot y Car fueron clases en mi proyecto.
Entonces, en la clase Robot, tendría muchos métodos en el siguiente patrón:
- dormir(); isSleepAvaliable ();
- despierto(); isAwakeAvaliable ();
- caminar (dirección); isWalkAvaliable ();
- disparar (dirección); isShootAvaliable ();
- turnOnAlert (); isTurnOnAlertAvailable ();
- turnOffAlert (); isTurnOffAlertAvailable ();
- recargar(); isRechargeAvailable ();
- apagado(); isPowerOffAvailable ();
- stepInCar (Auto); isStepInCarAvailable ();
- stepOutCar (Auto); isStepOutCarAvailable ();
- auto destrucción(); isSelfDestructAvailable ();
- morir(); isDieAvailable ();
- isAlive (); está despierto(); isAlertOn (); getBatteryLevel (); getCurrentRidingCar (); getAmmo ();
- ...
En la clase Car, sería similar:
- encender(); isTurnOnAvaliable ();
- apagar(); isTurnOffAvaliable ();
- caminar (dirección); isWalkAvaliable ();
- repostar(); isRefuelAvailable ();
- auto destrucción(); isSelfDestructAvailable ();
- choque(); isCrashAvailable ();
- isOperational (); Está encendido(); getFuelLevel (); getCurrentPassenger ();
- ...
Cada uno de estos (Robot y Auto) se implementa como una máquina de estado, donde algunas acciones son posibles en algunos estados y otras no. Las acciones cambian el estado del objeto. Los métodos de acciones se lanzan IllegalStateException
cuando se llama en un estado no válido y los isXXXAvailable()
métodos indican si la acción es posible en ese momento. Aunque algunos son fácilmente deducibles del estado (p. Ej., En el estado de sueño, la vigilia está disponible), otros no (para disparar, debe estar despierto, vivo, tener munición y no viajar en automóvil).
Además, las interacciones entre los objetos también son complejas. Por ejemplo, el automóvil solo puede contener un pasajero Robot, por lo que si otro intenta ingresar, se debe lanzar una excepción; Si el auto choca, el pasajero debe morir; Si el robot está muerto dentro de un vehículo, no puede salir, incluso si el auto está bien; Si el robot está dentro de un automóvil, no puede entrar en otro antes de salir; etc.
El resultado de esto, es como ya dije, estas clases se volvieron realmente complejas. Para empeorar las cosas, hay cientos de escenarios posibles cuando el robot y el automóvil interactúan. Además, gran parte de esa lógica necesita acceder a datos remotos en otros sistemas. El resultado es que las pruebas unitarias se volvieron muy difíciles y tenemos muchos problemas de prueba, uno causando al otro en un círculo vicioso:
- Las configuraciones de los casos de prueba son muy complejas, ya que necesitan crear un mundo significativamente complejo para ejercitarse.
- El número de pruebas es enorme.
- El conjunto de pruebas tarda algunas horas en ejecutarse.
- Nuestra cobertura de prueba es muy baja.
- El código de prueba tiende a escribirse semanas o meses después del código que prueban, o nunca en absoluto.
- Muchas pruebas también se rompen, principalmente porque los requisitos del código probado cambiaron.
- Algunos escenarios son tan complejos que fallan en el tiempo de espera durante la configuración (configuramos un tiempo de espera en cada prueba, en el peor de los casos, 2 minutos de duración e incluso este tiempo de espera, nos aseguramos de que no sea un bucle infinito).
- Los errores se deslizan regularmente en el entorno de producción.
Ese escenario de Robot y Automóvil es una gran simplificación excesiva de lo que tenemos en realidad. Claramente, esta situación no es manejable. Por lo tanto, pido ayuda y sugerencias para: 1, reducir la complejidad de las clases; 2. Simplificar los escenarios de interacciones entre mis objetos; 3. Reduzca el tiempo de prueba y la cantidad de código que se probará.
EDITAR:
Creo que no estaba claro acerca de las máquinas de estado. el robot es en sí mismo una máquina de estados, con estados "durmiendo", "despierto", "recargando", "muerto", etc. El automóvil es otra máquina de estados.
EDITAR 2: en caso de que tenga curiosidad sobre cuál es realmente mi sistema, las clases que interactúan son cosas como Servidor, Dirección IP, Disco, Copia de seguridad, Usuario, Licencia de software, etc. El escenario Robot y Coche es solo un caso que encontré eso sería lo suficientemente simple como para explicar mi problema.
fuente
Respuestas:
El patrón de diseño de estado podría ser útil, si aún no lo está utilizando.
La idea central es que se crea una clase interna para cada estado distinto - por lo que seguir tu ejemplo
SleepingRobot
,AwakeRobot
,RechargingRobot
yDeadRobot
todo habría clases, la implementación de una interfaz común.Los métodos en la
Robot
clase (likesleep()
yisSleepAvaliable()
) tienen implementaciones simples que delegan a la clase interna actual.Los cambios de estado se implementan intercambiando la clase interna actual con una diferente.
La ventaja de este enfoque es que cada una de las clases de estado es dramáticamente más simple (ya que representa solo un estado posible) y puede probarse de forma independiente. Dependiendo de su lenguaje de implementación (no especificado), es posible que todavía tenga que tener todo en el mismo archivo, o puede dividir las cosas en archivos de origen más pequeños.
fuente
No conozco su código, pero tomando el ejemplo del método de "suspensión", asumiré que es algo similar al siguiente código "simplista":
Creo que hay que marcar la diferencia entre las pruebas de integración y las pruebas unitarias . Escribir una prueba que se ejecute en todo el estado de su máquina es ciertamente una gran tarea. Escribir pruebas unitarias más pequeñas que prueben si su método de sueño funciona correctamente es más fácil. En este punto, no tiene que saber si el estado de la máquina se actualizó correctamente o si el "automóvil" respondió correctamente al hecho de que el estado de la máquina fue actualizado por el "robot" ... etc.
Dado el código anterior, me burlaría del objeto "machineState" y mi primera prueba sería:
Mi opinión personal es que escribir pruebas unitarias tan pequeñas debería ser lo primero que debe hacer. Tu escribiste eso:
La ejecución de estas pequeñas pruebas debería ser muy rápida y no debería tener nada que inicializar de antemano como su "mundo complejo". Por ejemplo, si se trata de una aplicación basada en un contenedor IOC (por ejemplo, Spring), no debería necesitar inicializar el contexto durante las pruebas unitarias.
Después de haber cubierto un buen porcentaje de su código complejo con pruebas unitarias, puede comenzar a construir pruebas de integración más complejas y que requieren más tiempo.
Finalmente, esto se puede hacer si su código es complejo (como usted dijo que es ahora) o después de haber refactorizado.
fuente
Estaba leyendo la sección "Origen" del artículo de Wikipedia sobre el Principio de segregación de interfaz , y me recordó esta pregunta.
Citaré el artículo. El problema: "... una clase de trabajo principal ... una clase gorda con multitud de métodos específicos para una variedad de clientes diferentes". La solución: "... una capa de interfaces entre la clase Job y todos sus clientes ..."
Su problema parece una permutación de la que tenía Xerox. En lugar de una clase de grasa, tienes dos, y estas dos clases de grasa están hablando entre sí en lugar de muchos clientes.
¿Puede agrupar los métodos por tipo de interacción y luego crear una clase de interfaz para cada tipo? Por ejemplo: clases RobotPowerInterface, RobotNavigationInterface, RobotAlarmInterface?
fuente