Las Interfaces y la Arquitectura

En esta ocasión hablaré de un tema relacionado con las Interfaces de Programación de Aplicaciones (API) y con las pruebas que juega un papel fundamental dentro de la arquitectura: las interfaces. Las interfaces son los puntos de contacto que establecen un contrato que permite el intercambio de información entre elementos que forman parte de la arquitectura de un sistema de software. Estos elementos pueden ser lógicos (ej. módulos), dinámicos (ej. objetos) o físicos (ej. nodos de hardware). Recordemos que la arquitectura está formada por estructuras compuestas por elementos conectados entre sí (ver SG27), y es en los puntos de conexión donde se encuentran las interfaces.

Durante el diseño de la arquitectura (ver SG29), el arquitecto considera un subconjunto de requerimientos que se denominan drivers para crear las estructuras que conforman a la arquitectura del sistema. Estos requerimientos incluyen requerimientos funcionales primarios, atributos de calidad y restricciones (ver SG28). Al diseñar la arquitectura, el arquitecto identifica elementos que permiten satisfacer los drivers, junto con las interfaces de estos elementos. La identificación y definición de las interfaces se hace, generalmente, mediante un análisis dinámico de la interacción entre los elementos con el fin de soportar un requerimiento particular. La figura 1 muestra un ejemplo de esto.

Figura 1. Estructuración lógica para soportar el caso de uso CU-1 (llave: UML)

El caso de uso CU-1, que es primario, forma parte de los drivers mientras que los demás casos de uso no. Al momento de diseñar la arquitectura, el arquitecto identifica elementos (en este caso capas y componentes) que permiten soportar el driver. Una vez identificados los elementos, se establecen los mensajes que deben intercambiar instancias de los elementos para soportar el driver. En el ejemplo, el componente ServicioCU1 tiene un método procesa() que recibe dos parámetros p1 y p2 y regresa un valor de retorno retA mientras que el componente Persistencia tiene un método almacena() que recibe un parámetro p3 y regresa un valor de retorno retB. En este caso, el método procesa() forma parte de la interfaz del componente ServicioCU1 mientras que almacena() forma parte de la interfaz del componente Persistencia. Cabe señalar que en general una interfaz tiene varios métodos, a diferencia de este ejemplo simple.

El arquitecto no identifica todos los elementos y sus interfaces, solo lo hace para aquellos que soportan los drivers. Sin embargo, estas decisiones establecen un marco de referencia que permite a los desarrolladores identificar los elementos e interfaces para requerimientos que no son drivers. Considerando el ejemplo previo, se deberían identificar componentes para soportar los demás casos de uso así como sus interfaces.

Permiten realizar la integración del sistema

En general un sistema es desarrollado de forma paralela por un equipo de desarrolladores que se encargan de construir partes individuales que posteriormente serán conectadas. Si el contrato entre las partes está bien definido desde el principio, se reducen los problemas relacionados con la integración. Retomando el ejemplo de la figura 1, supongamos que los componentes ServicioCU1 y Persistencia son desarrollados por distintas personas. Si previo al desarrollo de estos componentes se estableció que el componente Persistencia tiene una interfaz con un método retB almacena(p3), el desarrollador de ServicioCU1 tiene claro cómo interactuar con el componente Persistencia y la integración normalmente se hace sin dificultades. De lo contrario, es posible que la integración se retrase por correcciones necesarias para permitir que los componentes puedan interactuar.

Especificaciones para el diseño detallados de los módulos

La interfaz de un componente sirve también como especificación para realizar su diseño detallado y construcción. Consideremos el ejemplo anterior en el cual la interfaz del componente ServicioCU1 tiene un método retA procesa(p1,p2). El desarrollador puede diseñar y codificar el componente de diversas formas, siempre y cuando su diseño e implementación satisfagan el contrato establecido por la interfaz que, en este caso, es que el componente provea el método procesa().

Pruebas unitarias y de integración

Al desarrollar un sistema es necesario probar sus partes de forma individual, esto es lo que se conoce como prueba unitaria. La manera típica de hacerlo es probando el elemento a través de su interfaz, pues se asume que si un componente satisface el contrato que establece la interfaz entonces funciona correctamente. Retomando el ejemplo de la figura 1, si se quiere probar el componente Persistencia , habría que invocar el método almacena() y corroborar si el valor de retorno ret2 es lo que se espera en base al valor del parámetro p3.

Por otro lado, las interfaces permiten realizar prueba unitaria de elementos que dependen de otros al soportar la sustitución de elementos de los que se depende. Por ejemplo, si se quiere probar el componente ServicioCU1 del ejemplo anterior, no es necesario que se disponga del componente Persistencia. Basta con crear una implementación de la interfaz que debe implementar el componente Persistencia y que regrese los valores esperados. Esto es lo que se conoce como un “mock” (sustituto). Cabe señalar que para lograr esto es necesario hacer uso de alguna primitiva del lenguaje que permita especificar interfaces de forma independiente de su implementación.

Finalmente, las pruebas de integración, como su nombre lo indica, se enfocan en probar que los elementos se conectan de forma adecuada y para ello juegan un papel fundamental las interfaces y su correcta definición.

Conexión con sistemas externos

Es cada vez más común que los sistemas de software no sean entidades que trabajan de forma individual. En la actualidad, un sistema de software frecuentemente hace uso de funcionalidades provistas por otros sistemas, o bien proporciona funcionalidades para que sean usadas por otro sistema.

Para permitir la interacción entre sistemas, es necesario establecer interfaces que establezcan un contrato sobre la manera en que se intercambia la información. Es común hoy en día que esas interfaces se describan usando un lenguaje tal como WSDL (Web Services Description Language), si la comunicación entre sistemas se realiza mediante servicios web.

Interfaz de Programación de Aplicaciones (API)

Las interfaces de programación de aplicaciones (API) generalmente están relacionadas con librerías o frameworks (ver SG38). En lenguajes de desarrollo orientado a objetos tales como Java, existe una API asociada con el lenguaje que proporciona una gran cantidad de clases para propósitos diversos, como pueden ser el manejo de estructuras de datos. En este caso, el API se usa creando instancias de las clases que son parte del API, o bien, heredando y extendiendo a las mismas.

A continuación se describen algunos aspectos de importancia que se deben considerar en relación con las interfaces.

Alta cohesión y bajo acoplamiento. Este es el principio fundamental del diseño de interfaces. La alta cohesión se refiere a que cada componente haga una sola cosa, mientras que el bajo acoplamiento busca que al modificar un elemento el cambio no se propague hacia otros elementos. El bajo acoplamiento se logra encapsulando detalles de implementación. Esto significa que la interfaz de un elemento no debe exponer detalles internos del mismo, tales como las estructuras de datos en las cuales se almacena el estado, ya que estos detalles deben poder cambiarse sin necesidad de que esto afecte a los clientes de la interfaz. Por lo anterior, la interfaz de un elemento se debe diseñar de forma cuidadosa para no exponer detalles de implementación, ya que el tener interfaces no garantiza el bajo acoplamiento.

Programación defensiva. Cuando una interfaz es expuesta para que sea usada por un sistema externo o para que se construya un programa haciendo uso de ella, es necesario tomar precauciones adicionales en relación con los valores de entrada de los métodos de la interfaz. Lo anterior es parte de lo que se conoce como “programación defensiva” e implica, entre otras cosas, validar todas las entradas y considerar posibles situaciones inesperadas. En las interfaces internas, es posible relajar un poco este aspecto si se tiene seguridad de que no se recibirán valores que podrían causar problemas.

Aspectos de calidad de servicio. En algunos casos, además de la “firma” de la interfaz que está compuesta por elementos sintácticos (nombre de métodos, tipo de parámetros y valores de retorno), es necesario especificar como parte de la interfaz aspectos relacionados con la calidad de servicio. Un ejemplo de ello sería que la ejecución de un método tiene que completarse en un tiempo establecido.

Conclusión

Las interfaces tanto internas como externas juegan un papel fundamental en el desarrollo de un sistema de software. La definición de las interfaces está intrínsecamente relacionada con el diseño de la arquitectura, y una definición deficiente de las interfaces tiene muchas repercusiones negativas en el desarrollo del sistema. El no definir las interfaces de forma oportuna impacta negativamente en la integración del proyecto y en la realización de pruebas del mismo, lo cual tiene repercusiones en el tiempo de desarrollo y la calidad del sistema.

Por todo lo anterior, es necesario poner especial cuidado de identificar y definir correctamente las interfaces entre los elementos del sistema.

Bio

El Dr. Humberto Cervantes es profesor-investigador en la UAM-Iztapalapa. Además de realizar docencia e investigación dentro de la academia en temas relacionados con arquitectura de software, realiza consultoría y tiene experiencia en la implantación de métodos de arquitectura dentro de la industria. Ha recibido diversos cursos de especialización en el tema de arquitectura de software en el SEI, y está certificado como ATAM Evaluator y Software Architecture Professional por parte del mismo. www.humbertocervantes.net