Así como los objetos físicos se mueven a través del tiempo y el espacio, las aplicaciones de software se mueven por diferentes ejes en su ciclo de vida. Concretamente, el mantenimiento de una aplicación requerirá de una serie de adecuaciones y cambios por distintos motivos: cambio de plataforma, cambios en el proceso de negocio, nuevos requerimientos, nueva base de datos. Etcétera. Además, desde el punto de vista del desarrollador, lo ideal es poder reutilizar lo más posible de esfuerzos anteriores. En el ciclo de vida de un sistema, más del 60% del costo es el mantenimiento
La tendencia a requerimientos cada vez más complejos con tiempos de desarrollo cada vez más cortos vuelve el re-uso de código un imperativo.
Teóricamente, la programación orientada a objetos facilita el re uso de código. Los atributos de los lenguajes orientados a objetos que promueven el re-uso de código son:
Abstracción de datos que promueve sistemas modulares.
Herencia que permite que las subclases re-usen código de las superclases.
Poliformismo facilita el re-uso de comportamiento bajo diferentes contextos.
Marcos (frameworks) que son conjuntos de clases abstractas que solucionan familias de problemas relacionados.
Pero, para que se cumpla esta promesa divina de re-uso, el código, o más bien dicho, el diseño, debe ser bueno, muy bueno. ¿Qué es, entonces, un buen diseño? Cada cuál tendrá su idea. A manera de contrapunto empecemos por lo negativo, ¿Qué características tiene un mal diseño? Concediendo que la aplicación cumple con los requerimientos para los que fue diseñada, un diseño puede adolecer de lo siguiente:
Rigidez. La aplicación es difícil de cambiar porque cualquier cambio afecta demasiadas cosas.
Fragilidad. Cuando se hace un cambio, la aplicación deja de funcionar en lugares inesperados.
Inmovilidad. Es difícil utilizar la aplicación como parte de otra aplicación porque las dependencias con el contexto están enmadejadas en el código.
¿Qué es lo que hace un diseño rígido, frágil, e inmóvil? Las interdependencias de sus módulos. Entonces, para que el proceso de mantenimiento no sea la imagen de un perro persiguiendo su cola, se debe tener cuidado de minimizar interdependencias.
Desde el punto de vista de la programación estructurada, un diseño se puede ir construyendo bottom-up o top-down. Es decir, a partir de los componentes o módulos que tengo disponibles voy construyendo módulos más complejos, hasta cubrir los requerimientos, o conversamente, voy dividiendo mis requerimientos entre sub-módulos, hasta que llego a un punto suficientemente concreto para resolverlo de manera independiente. Al final, de manera iterativa, se llega a un diseño de capas jerárquico, donde en un sentido estricto, los únicos componentes que pueden ser completamente independientes del resto son los módulos de más bajo nivel en la jerárquia, es decir los más concretos y específicos, los más ligados al problema particular que se esta resolviendo.
La mecánica establecida para re-usar rutinas de bajo nivel es el uso de librerías. En la practica la productividad de un lenguaje o ambiente de desarrollo esta ligada con la calidad y disponibilidad de librerías. Esto esta bien, pero desde el punto de vista de re-uso de código las dependencias están al revés. Los módulos de alto nivel, que resuelven un proceso complejo de interacción entre la aplicación, sus módulos internos, y el contexto exterior son los que queremos re-usar. De acuerdo a este precepto, un buen diseño debe cumplir con el principio de inversión de dependencias.
El principio de inversión de dependencias
Módulos de alto nivel no deben depender en módulos de bajo nivel. Ambos deben depender de abstracciones.
Abstracciones no deben depender de los detalles. Los detalles deben depender de las abstracciones.
Al concepto de inversión de control se le refiere como el principio de Hollywood:
Do not call us, we call you
Inversión de control es un aspecto clave que diferencia un marco orientado a objetos de una librería. Una librería es un conjunto de funciones, tal vez organizadas dentro de clases, que una aplicación (cliente) llama dentro del código, la función hace lo que tiene que hacer, y regresa el control al cliente. En un marco orientado a objetos, es el marco el que llama al código del cliente. En .Net, por ejemplo, una manera de hacer esto es que el marco defina eventos a los cuales se subscribe el cliente y mediante el uso de delegados, el cliente asigna el compartimiento especifico que requiere.
En términos más generales, Interfaces es la manera de abstraer la interacción entre el cliente y el marco (framework). Una técnica de inversión de control es dependency injection.
La inversión de control es parte de lo que hace la programación de marcos orientados a objetos perturbadora para algunos. Al programar un procedimiento, la atención del programador esta en el flujo de control. Es difícil imaginar como se pudiera entender un programa sin saber la lógica de ejecución. Pero un buen marco abstrae el detalle del control de flujo. El foco de atención esta en los objetos, lo que puede ser al mismo tiempo más y menos tangible que el flujo de control.
En el marco, lo importante son las responsabilidades de cada objeto y la interacción (colaboración) entre ellos. Es una visión más abstracta, más declarativa del mundo, potencialmente más flexible y amplia que el enfoque en procedimientos.
Resumiendo, un buen diseño es aquel que nos permite hace cambios de manera no intrusiva, es decir, añadiendo código en vez de cambiando código. Un buen diseño se puede modificar sin tocar el código existente. O sea, en un desarrollo orientado a objetos el esfuerzo debe estar en el diseño. La programación orientada a objetos debe ser fácil, a costa de un proceso exhaustivo de diseño. He ahí la promesa y el reto fundamental de la orientación a objetos.
Los buenos patrones de diseño no se inventan, se descubren. Desde una perspectiva macro de diseño el principio de inversión de control es fundamental. Desde la perspectiva de las clases en si, ¿Qué principios se deben seguir para facilitar un buen diseño?, o más importante, ¿Qué debemos evitar para no inhibir el potencial del diseño? ¿Cómo garantizar que no haremos daño?
Algunos tips:
Es más fácil reusar un comportamiento agregando un componente que a través de herencia.
Eliminar análisis de casos. En el caso de .Net se pueden usar genéricos.
Reducir el número de argumentos. Sin embargo, al crear un objeto, es preferible exponer todas las dependencias para facilitar pruebas de clases individuales.
Reducir el tamaño de los métodos. El propósito del método debe ser obvio e idealmente el código debe ser auto documentado.
Las jerarquías de clases deben ser profundas y espigadas. Una clase todopoderosa, rodeada de ratoncitos indica un área de oportunidad en el diseño. Cada clase debe tener una responsabilidad principal, de preferencia única, claramente definida.
Minimizar acceso a variables. Una clase debe exponer solo aquello que sea estrictamente necesario para cumplir con su responsabilidad. En .Net se promueve el uso de propiedades en vez de variables públicas, para que la clase tenga mayor control en el uso de sus valores a través de métodos get y set.
La raíz de la jerarquía de clases debe ser abstracta. Es decir, no debe tener ningún (minimizar) detalle de implementación y solo exponer la interfaz de la clase.
Subclases deben ser especializaciones. Usualmente una subclase no debe redefinir métodos de la superclase, solo añadir nuevos.
Dividir clases grandes. Factorizar diferencias de implementación en sub-componentes. Separar métodos que no se comunican entre si.
Evitar el uso implícito de parámetros.
La guía del buen objeto,
- Mantengo un estado consistente todo el tiempo.
- No tengo métodos o variables estáticos.
- Nunca espero o regreso null.
- Fallo pronto.
- Soy fácil de probar, todos los objetos de los que dependo los recibo como parámetros, normalmente durante construcción.
- Los objetos de los que dependo pueden ser substituidos por Mock Objects (no uso dependencias a clases concretas).
- Encadeno constructores multiples a un lugar comun, usando this
- Siempre defino hashCode() junto a equals().
- Prefiero valores inmutables que puede tirar fácilmente.
- Tengo un valor especial para nada, por ejemplo EMPTY_SET para colecciones.
- Disparo una excepción cuando el cliente pide algo que no es razonable, por ejemplo, abrir un archivo que no existe.
- Disparo una excepción cuando no puedo hacer algo que debería poder hacer, por ejemplo error de disco al leer un archivo abierto.
- Solo atrapo excepciones que puedo manejar completamente.
- Solo registró en bitácora información que alguien necesita ver.
Referencias
Johnson and Foote's paper Designing Reusable Classes, Journal of Object-Oriented Programming , 1988.
El libro del Gang of Four
Richard Sweet ,1983.
John Vlissides column for C++ report
http://www.betaversion.org/%7Estefano/
http://www.laputan.org/drc/drc.html
http://www.laputan.org/dfc/discussion.html#Rock
http://www.digibarn.com/friends/curbow/star/XDEPaper.pdf
http://researchweb.watson.ibm.com/designpatterns/
http://www.flexwiki.com/default.aspx
http://www.artima.com/lejava/articles/patterns_practice.html
http://www.artima.com/index.jsp
http://today.java.net/pub/a/today/2004/02/10/ioc.html
http://www.objectmentor.com/resources/articles/dip.pdf
http://www.objectmentor.com/
One important characteristic of a framework is that the methods defined by the user to tailor the framework will often be called from within the framework itself, rather than from the user's application code. The framework often plays the role of the main program in coordinating and sequencing application activity. This inversion of control gives frameworks the power to serve as extensible skeletons. The methods supplied by the user tailor the generic algorithms defined in the framework for a particular application.
Ralph Johnson y Brian Foote