Ya hemos visto la conveniencia de los lenguajes orientados a objetos en lo que refiere al desarrollo de software de buena calidad. En efecto estos lenguajes proporcionan herramientas muy útiles que facilitan el desarrollo e implementación, pero no hay que perder de vista que en definitiva el éxito final depende del desarrollador y no del lenguaje; que de echo un buen programa en C puede ser de mucho mejor calidad que uno malo en C++. El lenguaje es mas un empleado que un socio del programador, esto es: "Trabaja para este y no por este", así que tenemos antes de pasar al diseño en si, que precisar algunos elementos de mas alto nivel que hacen al tipo de software que queremos desarrollar. Algunos ya han sido mencionados al pasar, pero sin irnos por las ramas vale la pena hablar algo mas en detalle de cada uno.
Cuando uno habla de "Biblioteca" enseguida vienen a la cabeza las viejas bibliotecas de algoritmos matemáticos que mantienen su vigencia, aún después de muchos años de haber sido concebidas. Una biblioteca es un conjunto de recursos (erg. estructuras de datos, funciones, etc.) que el usuario puede usar en sus programas. El efecto de incluir una biblioteca es básicamente extender el conjunto de recursos que provee el lenguaje, sin ninguna lógica o jerarquía subyacente, sencillamente agrego componentes nuevos a este.
Una de las grandes ventajas de las bibliotecas es su uso simple cuando existen conceptos bien identificados en el dominio de aplicaciones y el re-uso sea de algoritmos sobre datos simples.
Ejemplos de bibliotecas exitosas son: "Las de Algoritmos Matemáticos" , "Bibliotecas Tecnológicas (erg.: funciones como hora, fecha, etc.)", "De Interfaces de Usuario" o "Estructuras Abstractas de Datos y Algoritmos". En todos los ejemplos anteriores existe un común denominador: "La aplicación es construida sobre la biblioteca en cuestión" o sea el usuario es responsable de diseñar la arquitectura y la biblioteca de proveer componentes estándar de código para ser usados. En una enorme y diversa cantidad de problemas pueden identificarse abstracciones matemáticas tales como: "Ecuaciones Lineales", "Problemas de Optimización", etc. que pueden ser resueltos sin importar demasiado el tipo de problema real en si que hay detrás. Esta es la razón por la cual las bibliotecas matemáticas han tenido tanto éxito; son generales y pueden ser complementadas fácilmente con otras. Pensemos en Matlab que esencialmente es una biblioteca de algoritmos matemáticos construida sobre dos entidades básicas: Matrices y Funciones. Sin duda este y otros paquetes deben su éxito a su enorme flexibilidad y fácil uso, gracias a los cuales rápidamente es posible prototipar diversas aplicaciones en áreas muy disímiles.
Hasta aquí uno podría erróneamente creer que una biblioteca es la mejor inversión en desarrollo de software, basados en la idea que la parte mas compleja de este es la codificación de algoritmos y datos. Antes de argumentar que lo anterior no es siempre cierto, observemos que los ejemplos exitosos no incluyen: "Bibliotecas para Dominios Específicos de Aplicaciones" y es razonable preguntarse... ¿Por Que?. La razones básicas son:
- Una biblioteca solo permite un re-uso limitado de código, ya que este se limita al que conforma la biblioteca en sí y no al que pueda generar el desarrollador durante el uso de la misma.
- Una biblioteca no permite re-usar la lógica subyacente de las aplicaciones y por lo tanto esta debe ser hecha nuevamente en cada aplicación. En otras palabras una biblioteca no permite re-uso de arquitectura
Efectivamente, cuando un equipo de desarrollo trabaja sobre un dominio de aplicación inmediato concreto (un biblioteca de matemáticas también tiene un dominio concreto pero no esta concebida para crear aplicaciones directamente, sino para ser soporte de otras) una biblioteca implica repensar constantemente una gran porción de sus aplicaciones, con la consecuente ineficiencia de esto. El problema de re-uso de código puede sortearse extendiendo el conjunto de recursos de la biblioteca periódicamente, tarea que puede resultar mucho mas compleja de lo previsto, dado el enorme grado de caos al que se puede llegar, con un crecimiento naturalmente desorganizado e inspirado en base a la experiencia de fugaces grupos de trabajo, en la mayoría de los casos inspirados en los problemas concretos de su proyecto.
Existe además el obstáculo insalvable del bajo re-uso de arquitectura, sumamente importante cuando esta en sí es mas compleja que la implementación súper optimizada de un algoritmo concreto.
En resumen; si el tipo de aplicaciones que estamos desarrollando son de naturaleza muy diversa y siempre pueden reducirse a interacciones entre un puñado de objetos bien definidos, seguramente una biblioteca sea la mejor alternativa de desarrollo. Cuando estas no sean las condiciones de trabajo posiblemente convenga estudiar el siguiente punto.
Insistimos que el fracaso de la vieja biblioteca de imágenes, se debió al echo de ser una biblioteca en si; dado que debía funcionar sobre un dominio de aplicaciones bien definido y se esperaba fuera enriquecida con el aporte de proyectos concretos de grupos de estudiantes, con el consecuente desorden en el crecimiento, que termino matándola al tener esta que soportar un tamaño enorme con una estructura débil y rígida.
Según Johnson & Foote, un Framework es: "Un conjunto de clases que incorporan un diseño abstracto para soluciones a una familia de problemas relacionados, y soporta re-uso a un nivel de granularidad mayor que el de las clases". Como la mayoría de las definiciones formales, la anterior deja perfectamente claro de que se esta hablando, para una persona con experiencia en el tema.
Un framework incorpora las partes esenciales de una familia de aplicaciones, incluyendo la lógica misma. Consiste de muchas clases que a diferencia de una biblioteca, colaboran según un orden preestablecido que por un lado la limita en cuanto al tipo de aplicación a desarrollar, al mismo tiempo que hace posible verla como una solución genérica a un conjunto de aplicaciones.
Una biblioteca y un framework están concebidas para distintos usuarios. La primera puede ser usada por cualquiera que necesite el tipo de objetos o algoritmos que esta presta como recursos, mientras que la segunda debe usarse para desarrollar una aplicación concreta del dominio. Insistiremos sobre el uso del framework ya que es este el que deja mas clara la diferencia con una biblioteca.
Es útil pensar en una biblioteca como un gran paquete de piezas (recursos), con las que el usuario arma un rompecabezas determinado (su aplicación), existiendo claro esta muchísimas formas distintas de hacerlo.
Un framework tiene una concepción radicalmente distinta, primero identifica las partes variables de una aplicación a otra del dominio ( en la jerga: Hot Spots ) y luego de definir su interface ( forma en el dibujo ), arma el resto del rompecabezas de modo estático a menos de un vacío en cada Hot Spot, para que el usuario construya o elija uno adecuado a su aplicación concreta.
El equivalente gráfico expone en forma clara la diferencia en el uso de ambos tipos de paquete. Mientras en una biblioteca el programador construye la aplicación propiamente dicha en general el "main" del programa, en un framework este está básicamente establecido y el usuario solo es responsable de codificar las porciones distribuidas a través de la arquitectura que varían de una aplicación a otra. En otras palabras desarrollar en base a una biblioteca equivale a edificar una arquitectura montada sobre esta, mientras que hacerlo en un framework implica trabajar dentro (en general bajo) de este. Como consecuencia una integración consistente de nuevo código al framework, es una integración efectiva y el mismo pasa estar disponible en su uso a los usuarios futuros. Destacamos que:También hay que destacar que el uso de un framework requiere un conocimiento más detallado de la arquitectura, o sea, el usuario de un framework no puede pensar en sentarse a programar sin haber leído previamente y en detalle, la documentación del sistema. A nuestro entender esta es una buena inversión de tiempo para una persona que desarrolle software en un área específica ( como tratamiento de imágenes ), ya que el diseño tiene resueltos la mayoría de los problemas de esta. De todos modos en BICOTI algo fue alivianado a través de las "fachadas".
- Usar un framework es usar su arquitectura, así que esta se re-usa de una aplicación a la siguiente efectivamente.
- Un framework es adecuado para re-usar código si este queda integrado al mismo.
Por último no está de más comentar que existen dos tipos de frameworks:
En el primer caso el programador debe crear clases concretas para un conjunto de objetos de los que solo está definida la interface. Son muy generales, aunque puede ser muy tedioso generar código de una aplicación a otra sobre todo si suelen aparecer porciones en común, usadas de distinto forma. El usuario en general no crea un "main" propiamente dicho. Un ejemplo conocido de este tipo de framework, que suele emplearse en desarrollo de aplicaciones son las MFC's del "Microsoft Visual C++".
- Frameworks caja blanca.
- Frameworks caja negra.
En un framework caja negra, existen muchas alternativas implementadas para los hot spots y su uso se reduce (aunque no se limita), a configurar el framework eligiendo la combinación adecuada al caso. Es importante destacar que aún con pocas implementaciones de hot spots pueden conseguirse muchas aplicaciones distintas, ya que estas dependen del número de combinaciones. De lo anterior este mecanismo de re-uso de código tiene un orden casi exponencial, en contraste con lo que una biblioteca ofrecía. El usuario en general crea un main, donde configura el framework. Esperamos que Bicoti sea un buen ejemplo de un framework caja negra.
En los hechos es una tarea muy difícil desarrollar código orientado a objetos re-usable y en general son los desarrolladores experimentados los que logran resultados exitosos. Es pertinente entonces preguntarse ¿Que diferencia a los desarrolladores experimentados de los novatos?. En realidad existen innumerables factores pero en este punto nos interesa destacar uno en particular: "El desarrollador experimentado no desarrolla sus aplicaciones desde cero, ya que re-usa buenas soluciones previas de otros problemas". A medida que afronta proyectos, el desarrollador va identificando ciertos "Patrones" que pueden ser re-usados.Design Patterns documenta un breve pero importante conjunto de soluciones estándar a problemas comunes de diseño, en un esfuerzo por compartir experiencias entre desarrolladores experimentados, al mismo tiempo que acelera el proceso de aprendizaje de los más novatos.
No hay nada mejor que referirse a la fuente para lograr una idea más cabal de en que consisten. De hecho el uso de componentes estándar descentraliza la documentación permitiendo re-usar esta también, así que nos limitaremos a decir que en nuestra arquitectura identificamos los siguientes patterns dando además algun ejemplo:
Recomendamos al usuario consultar la bibliografía correspondiente (ver referencias bibliográficas: Design Patterns).
- Facade. Provee una interface unificada a un conjunto de interfaces en el sistema. Las fachadas definen una interface de alto nivel que hacen al subsistema más facil de usar. Usamos Fachadas a muchos niveles, como ejemplo está (bicotiImage2DArrayChar). Las fachadas en BICOTI, hacen uso de muchos otros patterns tal como veremos adelante.
- Iterator. Provee una forma de acceder a un agregado de objetos secuencialmente sin exponer su representación subyacente. Un ejemplo típico de uso de los Iteradores es cuando se quiere independizar un algoritmo de la dimesión de la imagen concreta (bicotiImageIterator).
- Abstract Factory. Provee una interface para crear familias de objetos relacionados o dependientes, sin especificar sus clases concretas. Un buen ejemplo son los "Create" de las fachadas.
- Builder. Separa la construcción de un objeto complejo de su representación, así el mismo proceso de construcción puede crear distintas representaciones. La diferencia fundamental con el anterior es que el resultado (producto) se obtiene mediante un proceso de construcción. Nuevamente como ejemplo están las fachadas ya que ciertos objetos se construyen mediante un complejo proceso que involucra varios pasos.
- Factory Method. Define una interface para crear un objeto, pero deja a las subclases decidir cual clase instanciar. Un ejemplo típico es el "CreateIterator" de bicotiImageImplementation.
- Bridge. Desacopla una abstracción de su implementación para que las dos puedan variar independientemente. bicotiImage es un puente entre la imagen abstracta y su implementación además de una fachada. Otro ejemplo es bicotiTransformation ya que de echo se usan implementaciones de imágenes para representarlas y las clases concretas solo dan una interface adecuada en cada caso.
- Observer. Define una dependencia de uno a muchos entre objetos de forma tal, que cuando un objeto cambia su estado, todos los dependientes son notificados y actualizados automáticamente. En realidad por razones de eficiencia, nosotros solo definimos un protocolo de actualización y delegamos al usuario la opción de actualizar o no determinado objeto. Los ejemplos son: la interacción entre los dexeles y propiedades con la implementación (bicotiDexel), o la de esta con la transformación (Transformation).
- Mediator. Define un objeto que encapsula el modo en que un conjunto de objetos interactúa. El mediador mejora la pérdida de conexión, evitando que los objetos se refieran entre sí explícitamente, permitiendo variar su interacción en forma independiente. Su diferencia fundamental con la fachada es que tiene interacción bidireccional con los actores. Nuestra versión del observador es en realidad una mezcla con mediador.
- Strategy. Define una familia de algoritmos, encapsula cada uno, y los hace intercambiables. La estrategia permite a los algoritmos variar independientemente de los clientes que los usan. En bicoti pululan las estrategias ya que fueron una de las formas más usadas para parametrizar los hot spots. Solo para mencionar un par de ejemplos están: bicotiDexelLabeling con sus estrategias de pixel y vecindad, así como bicotiNeighbourFilter con sus estrategias de extrapolación y función de frame.
- Composite. Compone objetos dentro de una estructura de árbol para representar el todo y partes en una jerarquía. La composición le permite a los clientes tratar indistintamente objetos y composiciones de estos. Ejemplo bicotiImageImplementation2DByLayers.
- Template Method. Define el esqueleto de un algoritmo, difiriendo algunos pasos a subclases. El método template deja que las subclases redefinan ciertos pasos de un algoritmo sin cambiar la estructura de este. Ejemplo: ver la función Refresh de los Iteradores (ver código de bicotiImageIterator2D).
- Prototype. Especifica el tipo de objetos a crear, usando una instancia prototípica, y crea nuevos objetos copiando el prototipo. Ejemplo: ver método CreateImageImplementation cuya función es clonar la implementación, permitiendo usar cualquiera de estas como prototipo.
El uso de componentes estándar es la primera forma natural de re-uso que facilita la tarea del desarrollador de software, dándole un perfil más compatible con el resto de las ramas de la ingeniería, en las cuales los ingenieros usan constantemente componentes de uso estándar para llevar a cavo sus proyectos. Lamentablemente en la Ingeniería de Software, este tipo de logros han sido muy limitados. Aparte de las clásicas bibliotecas de C/C++, en bicoti incorporamos componentes estándar de dos tipos. El primero a nivel de diseño de arquitectura son los Dessign Patterns vistos en el punto anterior. A un nivel mucho más bajo hacemos uso de una biblioteca que es parte del estándar desde hace poco tiempo, la STL.Brevemente diremos que esta biblioteca provee un conjunto de estructuras de datos y algoritmos sumamente: flexibles, eficientes y confiables, que facilitan enormemente la implementación de la mayoría de los algoritmos, al mismo tiempo que mejoran la claridad del código y evitan lidiar con problemas molestos, entre otros el manejo y fragmentación de la memoria. Muchas ventajas sin duda; sin embargo la STL tiene un defecto importante, uno debe invertir tiempo en aprender a usarla antes de poder hacer algo productivo con ella.
Existe abundante docuementación al respecto así que no haremos un curso aquí, simplemente citaremos dos papers que nos ayudaron mucho durante nuestro período de aprendizaje.
Como comentario final y tal cual veremos en el siguiente punto, bicoti está construída en base a la stl y en consecuencia es improbable que un usuario pueda hacer algo más que prototipar alguna aplicación, sin previamente haber aprendido las bases en el uso de esta.
- El primero y más conocido es el paper de los creadores mismos de la biblioteca o sea: Alexander Stepanov (Silicon Graphics Inc.) y Meng Lee (Hewlett-Packard Laboratories). Este es un paper sumamente completo, preciso y directo, resultando una excelente fuente para consultas. Lamentablemente lo encontramos pésimo desde el punto de vista didáctico, entre otras cosas porque no muestra un ejemplo.
- El siguiente paper, mucho menos conocido es el de Johannes Weidl. En realidad es más un tutorial que una documentación de la biblioteca y es justamente ahí donde radica su importancia ya que se vuelve un perfecto complemento para el paper anterior.
La stl se volvió estándar reconocido en paralelo con el desarrollo de nuestro proyecto; en consecuencia se decidió re-escribir la porción del código que ya teníamos implementada y funcionando, así como todo el que agregáramos en el futurio. Tiempo mediante, los integrantes del grupo hemos encontrado que esta decisión fue excelente y que el costo ahorrado desde el momento en que fue tomada hacia adelante, compensó con creces el consumido en cambiar lo que estaba hecho. Aprender a usar la stl va mucho más allá de bicoti mismo, es un paso ineludible para cualquier persona con intenciones de programar en C++.