La importancia de un buen diseño para garantizar la extensibilidad de un sistema

71

Cuando una empresa, grupo de desarrolladores o alguien en particular decide comenzar a desarrollar un sistema dado, uno de los primeros puntos a resolver es el saber qué será necesario desarrollar. Como en otras profesiones, es importante saber de antemano qué se necesitará hacer, para, en base a ello, planear y diseñar el producto final.

En este caso, por ejemplo, un arquitecto debe de saber qué busca diseñar para poder realizar un diseño que se adapte y cumpla las especificaciones planteadas. ¿Es una casa o un edificio de oficinas? ¿Será una torre o un puente? ¿De cuantos pisos o que tan largo? ¿Qué tipo de suelo hay en la zona? ¿Qué tantas personas o vehículos deberá soportar?

Como se observa, en este ejemplo de un arquitecto, es importante conocer de antemano qué se busca construir con todos sus detalles y pormenores. Esto es así pues una parte del diseño puede depender de otras, o su funcionamiento puede afectar aquellas que estén a su alrededor.

Lo mismo puede considerarse en el diseño y programación de un sistema. Sin embargo, en el terreno del diseño y programación de sistemas, existen diferentes tipos de metodologías, las cuales pueden definir diferentes formas de abordar un sistema a desarrollar.

En el caso de esta entrada, se considerará como ejemplo la Metodología Ágil. Esta metodología, a diferencia de otras, no espera que se definan todos los requisitos del sistema desde un inicio. Esto es, que el cliente (o quien defina qué se necesita que haga el sistema) puede definir un conjunto limitado de características, bajo las cuales el sistema es desarrollado en un tiempo corto. Al completar, el cliente puede analizar lo desarrollado, proveer comentarios y proponer nuevas características a agregar al sistema (o recomendar características a ser removidas o cambiadas).

Esta metodología tiene sus ventajas y desventajas, así como otras metodologías. En el lado positivo, se tiene, entre otras cosas, que el cliente tiene un rol más activo durante el desarrollo, así como que puede modificar el estado del sistema si éste no está tomando el camino que pudiera haber deseado desde un inicio. En el lado negativo, que más concierne a esta entrada, es que el diseño y desarrollo se puede volver más complejo para el o los desarrolladores involucrados. Esta dificultad es justamente la que exploramos en esta entrada.

Cuando a un desarrollador se le proporcionan todos los requisitos de un sistema desde un inicio, éste puede tomarse el tiempo en diseñar todos los componentes que formarán al sistema final. Desde un inicio, se puede analizar el comportamiento de un componente en relación con otros, su integración y la información que ha de transferirse entre partes. Es decir, así como un arquitecto, es posible diseñar todo el producto antes de comenzar a construirlo.

Sin embargo, cuando a un desarrollador no se le proporcionan todos los detalles de qué ha de desarrollarse, se comienza a entrar en un terreno sinuoso. Si bien no imposible, el desarrollo del sistema se comienza a volver propenso a errores. La siguiente lista ejemplifica algunas situaciones que, personalmente, he visto que se presentan en este ambiente y metodología:

  • Un componente diseñado y desarrollado para cumplir una función dada, termina siendo insuficiente, teniendo que rediseñarlo o reescribirlo por completo, o bien, se tiene que agregar un nuevo componente que apoye al anterior con funcionalidad que (normalmente por tiempo) no pudo ser agregado en el componente original.
  • Un componente cuyo diseño inicial se planteaba para ser pequeño y concreto termina extendiéndose más allá de lo esperado al agregarle funcionalidades que inicialmente no se consideraron, rompiendo por completo sus criterios de diseño.
  • Uno o varios componentes terminan creciendo de forma descontrolada, pues parte de su funcionalidad no puede ser trasladada de forma lógica a otros componentes sin primero rediseñar parte del sistema.

Estas situaciones se pueden presentar en cualquier tipo de proyecto por muy variadas razones. Normalmente, todo se resume a tres condiciones que pueden llevar a estos:

  1. La solución ideal involucraría re-diseñar una parte considerable del sistema para poder dejarlo bien diseñado. Suele no hacerse por restricciones de tiempo.
  2. El rediseño de ciertos componentes deja de ser una alternativa pues de éstos dependen otros, los cuales también necesitarían rediseñarse. Este es un efecto en cadena de dependencias que suele no resolverse, igualmente, por limitantes de tiempo.
  3. Es más sencillo agregar un nuevo componente que realice una función parcial, o extender uno ya existente con funcionalidad que no le corresponde.

De esta forma, suele darse un efecto bola de nieve en cuanto a las restricciones o problemas con el diseño. Ya sea por problemas de tiempo o por aplicar el típico “Se arreglará luego”, los problemas crecen cada vez hasta volverse en algo inmanejable, siendo, en su momento, necesario rediseñar por completo el sistema.

Pero entonces, ¿cómo se podría combatir o resolver este problema? ¿Hay forma de resolverlos o prevenirlos?

Si el problema ya está presente en un sistema, no es trivial encontrar una solución. Si bien no imposible, se requeriría una gran inversión de tiempo para llevar a cabo un análisis detallado y a fondo del sistema actual para, por un lado, encontrar errores y problemas de diseño, y por otro, para plantear cómo hacer los cambios requeridos de una forma adecuada, posiblemente escalada, y sin necesitar reescribir todo el sistema.

Si el problema no se ha presentado aún, es importante tomar medidas para evitarlo y mitigar la posibilidad de que se presente en un futuro.

A continuación describo algunas recomendaciones que pudieran ser de utilidad para aquellos desarrolladores que quieran intentar evitar los problemas, más que remediarlos.

Diseños atómicos: Un problema frecuente que se presenta en el diseño de sistemas grandes es el empleo de componentes demasiado complejos. Este tipo de componentes suelen ser los primeros en experimentar problemas de extensibilidad al usar un diseño con Metodología Ágil o similar.

Estos componentes superan normalmente las dos mil líneas de código y su funcionamiento suele ser crítico. Así mismo, es bastante probable que estos componente aglutinan grandes bloques de lógica que no pudiera ser sencillo separar otros mas pequeños. Es por esto que se recomendaría buscar un diseño atómico de componentes. Con esto, hago referencia a emplear componentes pequeños. Componentes sencillos y de funcionalidad concreta. Es cierto que pudieran existir bloques de lógica que necesitasen estar en un mismo componente, pero en esos casos se recomendaría que se intente delegar funcionalidad a componentes genéricos más pequeños, de tal forma que no toda la funcionalidad resida dentro del componente grande.

Personalmente, recomendaría que ningún componente superarse las 500 líneas de código (entendiéndose por componente una clase o una función). De esta forma, sería más sencillo reemplazarlos en un futuro, extenderlos o modificarlos.

Componentes genéricos: Otro problema frecuente ocurre cuando se ha planteado una funcionalidad en un componente, pero, eventualmente se vuelve necesario contar con otro componente que haga casi lo mismo, pero con algunos detalles diferentes. En este caso, para evitar esta situación, se recomendaría procurar que los componentes puedan operar con datos genéricos, no tan especializados.

Un ejemplo sencillo sería el de una función que se encarga de buscar duplicados en una lista. En este caso, si la función se diseña para operar sobre valores enteros, pudiera ser un reto operar, eventualmente, con datos definidos para el sistema (como instancias de clases). De esta forma, lo recomendable sería que la lista sea diseñada para operar con diferentes tipos de datos, apoyándose quizás de funciones auxiliares que realicen la tarea de comparación de valores. Es claro que un diseño así se vuelve más elaborado y tardado, pero el ahorro futuro en reescritura o repetición de código valdrá la pena.

Separación de interfaz y lógica: Un problema frecuente que he observado es durante el diseño de las interfaces de usuario de los sistemas. Suelen prepararse componentes que despliegan al usuario los controles del mismo (varios botones, entradas de texto, listas desplegables, etc), pero, la lógica o el código que define qué hacer con la información que el usuario ha ingresado se encuentra embebida en los componentes gráficos mismos. De esta forma, si una funcionalidad (como el cómo validar un texto, por ejemplo) necesita aplicarse en otra parte del sistema, suele recurrirse a copiar el código, repitiendo la funcionalidad y exponiendo al producto a errores.

De esta forma siempre he recomendado que los componentes gráficos no administren información, sino que sean solamente un medio para mostrarla u obtenerla. Así, los componentes visuales dejan de volverse clave e indispensables en el manejo de información, y la lógica que la procesa puede preocuparse sólo de eso, de procesar los datos obtenidos sin entrar en detalle de cómo se visualizará ni de dónde viene.

En este caso particular, suelo recomendar el modelo MVC (Model-View-Controller, Modelo-Vista-Controlador), donde se hace una clara distinción entre aquellos componentes que solo muestran u obtienen información (View), aquellos que se encargan de administrar la información desde o hacia una fuente (como lo pudiera ser una base de datos o un archivo de configuración, siendo estos el Model), y aquellos componentes que no se preocupan ni de dónde viene la información ni cómo se almacena, pero que se encargan de llevar a cabo la lógica de procesamiento y toma de decisiones (Controller).

Independizar a los componentes: Una situación que igualmente me ha sido posible constatar en mi carrera profesional, es aquella en que un componente depende directamente de otros. En este caso, suele ser mediante referencias (como apuntadores en lenguaje C o C++), o, como normalmente lo refiero, que un componente conoce a sus vecinos.

Esta situación promueve la dependencia entre componentes e incrementa la complejidad del diseño del sistema. Una forma, de entre muchas posibles, de resolverlo, es el procurar, tanto como sea posible, que los componentes no se usen entre sí. Esto es, que si un componente genera como resultado una lista de datos, debería de aprovecharse todo mecanismo que provea el lenguaje para intentar que ese componente simplemente “retorne” esos datos, y que sea un intermediario el que tome ese resultado y se lo dé al siguiente componente que los requiera.

Es evidente que la inclusión de un intermediario entre componentes pudiera parecer contraproducente, pero, a la larga, suele ser más práctico y sencillo, pues la lógica de los componentes involucrados suele volverse más sencilla, y el intermediario no tiene porqué ser tan grande.

De esta forma, con estos sencillos consejos, bien aplicados y empleando toda la habilidad con que disponga el desarrollador, sería posible lograr un diseño inicial que garantice la extensibilidad del mismo, logrando con ello que en metodologías como la Ágil, el sistema inicial pueda realmente llegar a madurar y ser completado sin necesidad de pasar por una etapa de rediseño o reescritura.

Es claro que mis consejos y observaciones no son universales ni aplican para todas la situaciones que pudieran presentarse, pero es importante comenzar a identificar los problemas frecuentes que nos llevan a tener problemas en el diseño y desarrollo de un sistema.

Siempre será mejor prevenirlos que tener que corregirlos en una etapa tardía de desarrollo.

COMPARTIR
Artículo anteriorA la sombra Mozart
Artículo siguienteMatemáticas o Muerte
Ingeniero en Computación, especialista en desarrollo de software de sistemas con orientación a consola y aplicaciones gráficas de alto rendimiento. Actualmente laboro en la empresa Altomobile desarrollando aplicaciones para escritorio multiplataforma (Microsoft Windows y Apple Mac OS X). Entusiasta del Software Libre, así como el compartir conocimientos de forma gratuita con todos aquellos deseosos de recibirla.