jueves, 26 de julio de 2007

¿Cómo asegurar la evolución de un sistema?

A medida que se adquiere más experiencia desarrollando sistemas, uno se va dando cuenta lo importante que es la evolución de los mismos y por lo tanto asegurarse de no cometer errores cuando estos evolucionan.
La mejor manera que he encontrado para resolver este problema es, como siempre, delegarle esta responsabilidad a los objetos que forman parte del sistema. Siempre es mejor que la computadora trabaje mucho a que nosotros tengamos que hacerlo puesto que a ella no le cuesta, no protesta por hacerlo y no se equivoca!
Veamos un ejemplo donde es necesario adaptar objetos de una jerarquía de clases de una manera especial. En particular, supongamos que tenemos reportes en nuestro sistema donde hay que mostrar información de eventos (que están en una jerarquía) de una manera muy particular, porque el usuario así lo requiere (siempre caprichosos estos tipos!!).
Una manera de solucionar este problema es adaptar cada evento a lo que el usuario desea ver, de tal manera que cada evento puede generar una o más líneas en el reporte y con información que en principio no es polimórfica entre los eventos. Por lo tanto, se genera una jerarquía de objetos que adaptarán los eventos que están en otra jerarquía. El problema es que ahora los objetos deben decidir que adapter corresponde a cada evento. Una solución es la siguiente: (Los nombres están puestos solo para el ejemplo, no son buenos nombres para un modelo real):
Jerarquía de Eventos:
Event
..EventA
..EventB
..EventC
..EventD

Jerarquía de Adapters de eventos para las líneas del reporte:
ReportXLineAdapter
..LineAdapter1
..LineAdapter2

Luego, cuando hay que crear una adapter se le pide a ReportXLineAdapter el adapter que handlea el evento que se quiere adaptar, enviándole el mensaje #adapterFor: que hace lo siguiente:
ReportXLineAdapter class>>adaptersFor: anEvent

^self allSubclasses select: [ :anAdapterClass | anAdapterClass canHandle: anEvent ].

LineAdapter1 class>>canHandle: anEvent

^anEvent class = EventA

LineAdapter2 class>>canHandle: anEvent

^anEvent class = EventB or: [ anEvent class = EventC ]

Si estuviéramos usando el paradigma estructurado, ¿cómo se haría? ¿Qué están reemplazando estos objetos cuando colaboran entre sí?. Están reemplazando la estructura sintáctica de control de flujo conocida como "case" o "switch". Pero el problema que tiene esta implementación es que no tiene implementado el famoso "default" de dicha estructura de control de flujo.
Este problema se hace perceptible al evolucionar el sistema. Si se agrega un nuevo evento (lo podría hacer cualquier programador) seguramente no sabrá que hay que adaptar ese evento para el reporte, es más, quizá este programador no tenga ni idea que ese reporte fue hecho de esta manera.
Los tecnócratas metodologos dirían que con un buen proceso esto se debe resolver... que si se agrega un nuevo evento hay que utilizar la matriz de trazabilidad que nos indicará qué hay que modificar para que ese reporte funcione, etc. etc. etc., o sea, se nota que nunca hicieron un sistema porque tener un proceso que haga eso es carísimo (sí, CMM cuesta), imposible de mantener y además el programador debe mantener esa matriz de trazabilidad en primera instancia. Pero si puede mantener esa matriz de trazabilidad ¿por qué no modifica el código y listo? ¿para qué dar tanta vuelta?, en fin, un absurdo total.
Lo mejor es hacer que los objetos se "protegan" a sí mismos de la evolución. Por lo tanto, se debería modificar #adaptersFor: para que sepa que eventos él sabe que no serán handleados y si no hay adapter para un evento que debe ser handleado, significa algo anduvo mal.
La modificación sería así:
ReportXLineAdapter class>>adaptersFor: anEvent

| adapters |

adapters := self allSubclasses select: [ :anAdapterClass | anAdapterClass canHandle: anEvent ].
(adapters isEmpty and: [ (notHandledEvents includes: anEvent class) not ]) ifTrue: [
self error: 'Decidir como administrar el event ', anEvent class name ].

^adapters

Por otro lado, se escribiría un test que justamente verificaría que esta decisión de diseño tomada para solucionar este problema se la respete mientras evoluciona el sistema. Para ello se puede utilizar el framework de SUnit, es más, nosotros lo utilizamos para crear varios tipos de tests, no solo unitarios; lo usamos para test de estándares de código, de calidad de código, para test de user story y de arquitectura. No tenemos una categoría para este tipo de test, seguramente caería dentro de los test unitarios del reporte, pero por ahí sería bueno crear una categoría para controlar estas decisiones de micro-arquitectura. En fin, el test sería algo así:

testAllEventsAreHandledByReportXAdapter

| eventClasses |

"Me quedo con los eventos concretos"
eventClasses := Event allSuclasses select: [ :aClass | aClass subclasses notEmpty ] .
eventClasses do: [ :aClass | self shouldnt: [ ReportXLineAdapter xxx: aClass new ] raise: Error ]

De esta manera si se crea un evento y la jerarquía de adapters no se modifica para tenerlo en cuenta, este test fallará y nos enteraremos antes de enviárselo al usuario, que bueno no!. Fijensé que este tipo de problemas no lo soluciona ningún sistema de tipos estático, porque no tiene que ver con tipos sino con minas... eh, lapsus momentaneous, con decisiones de diseño de más alto nivel.
Moraleja: Siempre escribir código pensando en la evolución del sistema, o sea, los objetos deben prevenirse ellos mismos de la evolución y el cambio de sus compañeros. En este caso lo hicimos de dos maneras: la primera constructiva (no se me ocurre mejor nombre) y la segunda preventiva.
La constructiva la logramos modificando el mensaje #adaptersFor: para que construya correctamente los adapters para el evento, en caso contrario genera una excepción.
La preventiva la logramos escribiendo el test que verifica que se cumpla con la decisión de diseño de utilizar este idiom para resolver este problema. Esta opción solo se puede hacer con lenguajes de objetos que permitan trabajar sobre el metamodelo como Smalltalk. Esta opción no se puede realizar con lenguajes que no permitan trabajar sobre el metamodelo o lo hagan de manera limitada (como Java o .Net. ¿Cómo se obtienen la subclasses de una clase en Java o .Net?)

No tuve tiempo de pensar en otra solución para este problema, pero hay algo que me dice que debe haber una mejor opción... En fin, el ejemplo es válido de cualquier manera para pensar en la evolución de un sistema y como asegurarse de no introducir errores.

3 comentarios:

Gaboto dijo...

Muy buena idea, la verdad que una vez me enfrente con un problema similar (lamentablemente en un lenguaje de los que nombras, sin reflexión posta) y lo que terminé haciendo fue que cada event supiera cual es su adapter. No me convenció demasiado la idea pero no se me ocurrió nada mejor.
Che muy bueno tu blog, a veces entro y me pongo a leer (desordenadamente como podrás ver) y siempre se aprende algo nuevo. Podrías editar un libro de todo esto después je.
Un comentario (de buena leche y para que lo corrijas si podes), fijate que en una parte de este articulo pusiste “desición”, es “decisión”.
Saludos.

Hernan Wilkinson dijo...

Que tal,
no puedo hacer un comentario sobre tu solución porque no tengo mucha info para hacerlo... lo único que puedo decirte es que si por el momento te solucionó el problema, 10 puntos, luego habrá tiempo para mejorarlo si es necesario o cuando te caiga la ficha de como hacerlo mejor.
Gracias por lo que me decís del blog! Hace rato tengo ganas de escribir un libro, de hecho lo empecé, pero no tengo tiempo físico para dedicarle.
Respecto de los errores de ortografía, tenés razón, es algo en lo que tengo que poner más atención...

Muchas gracias por el comentario!
Hernan.

Anónimo dijo...

Who knows where to download XRumer 5.0 Palladium?
Help, please. All recommend this program to effectively advertise on the Internet, this is the best program!