Patrones de diseño

Observaciones contradictorias

En uno de sus últimos artículos Martin Fowler reflexiona sobre la necesidad de que determinados sistemas sean capaces de manejar información contradictoria.

Se presenta el caso del historial médico de un paciente donde se tienen dos notas con los resultados de los análisis de sangre hechos por dos hospitales diferentes. La nota del primer hospital indica que el paciente presenta un tipo de sangre A mientras que la nota del segundo hospital dice que el tipo de sangre del paciente es B.

Podríamos concluir que uno de los dos hospitales se ha equivocado en sus análisis y solicitar un nuevo análisis que nos confirme uno de los dos tipos de sangre. Puede que en este caso no tuviera demasiada importancia el desechar el análisis que hemos confirmado equivocado, sin embargo, podrían existir situaciones donde la contradicción no esté tan clara o incluso pueda deberse a que se haya producido un cambio muy poco probable en el estado de la propiedad bajo consideración. Además, una observación incorrecta podría haber tenido importantes implicaciones en base a decisiones tomadas cuando se pensaba que era cierta, ya que dichas decisiones podrían ser ahora incorrectas. Si eliminamos el valor incorrecto del sistema se pierde el vinculo con esas decisiones y se dificulta su rastreo.

En el caso de un sistema tradicional se hubiera asociado la propiedad ‘Tipo de sangre’ al paciente como un atributo de su clase que solo pudiera presentar un valor en un momento dado, sin embargo, en el articulo se propone un ‘patrón’ para manejar este tipo de información contradictoria de forma genérica y que denomina ‘observación’.

La clase observación guarda todo el estado relativo a una observación dada, como la fecha de su realización o la probabilidad de que sea cierta; puede referirse tanto a la existencia de una cualidad como a su ausencia, algo importante en situaciones donde se desconozca o no sea posible determinar el valor de una propiedad pero el hecho de poder descartar determinados valores amplíe la probabilidades de tomar una decisión correcta.

Se me ocurre que, tal vez entonces, en los casos en que esto sea aplicable, se deba definir una nueva abstracción a un nivel por encima de la ‘observación’, estando dicha abstracción formada por un conjunto de observaciones y por la lógica necesaria para tomar las decisiones apropiadas sobre dicho conjunto en el dominio del problema que se pretenda resolver. Esta nueva abstracción sustituiría a los tradicionales atributos o propiedades de las clases y podría presentarse de tal forma que fuera ‘transparente’ a las entidades que no necesiten más detalles que el valor de la propiedad, y a la vez presentase un interface más o menos complejo para las entidades que deban tomar decisiones en base al conjunto de observaciones.

Patrones de diseño

Incluso aunque aparentemente dos proyectos no tengan ninguna relación entre ellos, a menudo se encuentra que algunos de los problemas que se deben resolver son muy similares entre sí.

Un patrón de diseño es una guía que nos indica una posible solución a la hora de resolver uno de esos problemas recurrentes. Existen multitud de patrones de diseño que proporcionan soluciones a multitud de problemas comunes en el desarrollo de software.

Los patrones de diseño proporcionan varios beneficios a la hora de diseñar una aplicación software:

  • Reducen la complejidad. Un patrón de diseño se suele describir como una serie de elementos, tipicamente clases, y sus interacciones entre ellos. Dichos elementos se proporcionan como una guía por lo que la implementación de un mismo patrón puede variar de un sistema a otro.  Por lo tanto, la utilización de patrones de diseño nos proporciona un nivel de abstracción superior al nivel de clases y estructuras de datos reduciendo la complejidad que tenemos que manejar al pensar sobre el sistema.
  • Facilitan la comunicación. Cuando transmitimos a otra persona, por ejemplo, que en determinada parte del sistema se ha utilizado el patrón estrategia, dicha persona podrá entender rápidamente la clase de problema que resuelve esa parte del sistema y estará en condiciones de poder entender la estructura del código que la implementa rápidamente.
  • Reducen errores. A menudo el desarrollo de software se califica como un wicked problem, que de forma sencilla podría entenderse como un problema que no es comprendido hasta que no se ha formulado una solución. Los patrones de diseño acumulan la sabiduría de años en el intento de resolver esos problemas.
  • Facilitan el diseño. A la hora de enfrentarse a un problema de diseño, si se dispone de un catalogo de patrones, se puede evaluar cuales de ellos podrían servir para resolver dicho problema proporcionando un punto de partida al diseño.

Algunos de los patrones más comunes son:

  • Singleton. Proporciona un acceso global a una clase de la cual existe únicamente una instancia.
  • Estrategia. Define un conjunto de algoritmos o comportamientos fácilmente intercambiables. Permite que dichos algoritmos se puedan intercambiar independientemente del código que les utiliza.
  • Observador. Permite mantener a múltiples objetos en sincronía proporcionando un mecanismo para que los objetos puedan notificar los cambios que se produzcan en ellos a otros objetos que puedan estar interesados en dichas notificaciones.
  • Fachada (Facade). Proporciona un interface unificado a un conjunto de interfaces en un subsistema. Una fachada define un interface de un nivel de abstracción superior por lo que hace que el subsistema sea más fácil de utilizar.
  • Adaptador. Convierte el interface a una clase a otro tipo de interface, normalmente porque el cliente espera ese otro tipo de interface y de otra forma sería incompatible.

Se puede encontrar un completo listado de patrones, así como un buen artículo sobre los patrones de diseño en la Wikipedia.

Como se menciona en dicho artículo, los patrones de diseño adquirieron enorme popularidad a partir de la publicación del libro 'Design Patterns' escrito por el grupo conocido como Gang of Four (GoF) referenciado en las fuentes de este artículo.

Fuentes | Code Complete By Steve McConnell | Gamma, Erich, et al. Design Patterns. Reading, MA: Addison-Wesley, 1995

Introducción al contenedor Unity

Unity es un contenedor de inyección de dependencias que facilita el desarrollo de aplicaciones proporcionando un sistema para gestionar tanto la construcción de objetos como las dependencias entre los diferentes componentes de una aplicación.

Supongamos que hemos definido  un interface a través del cual gestionaremos todo el sistema de registro de eventos de nuestra aplicación.

    public interface ILoggingService

    {

        void Log(string message);

    }

 Y a su vez una clase que implementa dicho interface y que en este caso simplemente enviara el mensaje a la consola del sistema.

    public class ConsoleLoggingService: ILoggingService

    {

        public void Log(string message)

        {

            Console.WriteLine("Console: " + message);

        }

    }

 El funcionamiento básico del contenedor es sencillo, en primer lugar registraremos en el contenedor los tipos de objetos que deseamos que gestione. Al hacerlo, además, indicaremos al contenedor cual debe ser el ciclo de vida del objetos de ese tipo. En el caso que nos ocupa registraremos el interface ILoggingService en el contenedor mapeando dicho interface al tipo ConsoleLoggingService de tal forma que cuando solicitemos al contenedor un objeto implementando el interface ILoggingService nos devuelva un objeto del tipo ConsoleLoggingService. Como además deseamos que en la aplicación solo exista una única instancia del sistema de eventos (Singleton), indicaremos al contenedor que se encargue de controlar la vida del objeto, de esta forma la primera vez que solicitemos el interface se creará el objeto pero las veces sucesivas se nos devolvera una referencia al mismo objeto. El siguiente fragmento de código muestra el proceso de registro utilizando uno de los métodos que nos brinda el contenedor.

    IUnityContainer container = new UnityContainer();

    container.RegisterType<ILoggingService,

            ConsoleLoggingService>(new ContainerControlledLifetimeManager());

Veamos como funciona la inyección de dependencias. El contenedor nos proporciona varias formas de realizar la inyección de dependencias: a través del constructor, a través de una propiedad ó a través de un método. En el caso general, las inyección requiere que marquemos el constructor, la propiedad o el método con un atributo que indicará al contenedor que debe realizar la inyección de un objeto. El siguiente fragmento de código muestra la utilización del atributo InjectionConstructor para indicar al contenedor que debe inyectar un objeto con el interface ILoggingService a la hora de construir un objeto del tipo MainApplicationModule.

    public class MainApplicationModule

    {

        ILoggingService mLoggingService;

 

        [InjectionConstructor]

        public MainApplicationModule(ILoggingService loggingService)

        {

            mLoggingService = loggingService;

            mLoggingService.Log("Inicializado el módulo principal de la aplicación");

        }

    }

 Nótese que en este caso, al existir solo un constructor, el contenedor inyectará las dependencias en el constructor de forma autómatica aunque no hayamos marcado el constructor, sin embargo, dicho atributo es útil para indicar que constructor utilizar en el caso de que existan varios. Si tenemos una clase con varios constructores y ninguno de ellos está marcado con el atributo de inyección, el contenedor utilizará aquel que tenga más parámetros, lanzando una excepción en el caso de que exista más de un constructor que cumpla la propiedad de tener el máximo número de parámetros.

    1     static void Main(string[] args)

    2     {

    3         using (IUnityContainer container = new UnityContainer())

    4         {

    5             container.RegisterType<ILoggingService,

    6                 ConsoleLoggingService>(new ContainerControlledLifetimeManager());

    7             MainApplicationModule mainModule = container.Resolve<MainApplicationModule>();

    8             Console.ReadLine();

    9         }

   10     }

 En la linea 7 del anterior fragmento de código, solicitamos al contenedor que nos devuelva un objeto del tipo MainApplicationModule. Obsérvese que en este caso no ha sido necesario registrar el objeto con el contenedor previamente. La utilidad de crear un objeto a través del contenedor de un tipo que no ha sido previamente registrado es que el contenedor evaluará e inyectará las dependencias de dicho objeto al generarle.

Después de la ejecución de la linea 7 aparecerá en la consola del sistema la siguiente línea indicando que se ha realizado el proceso de inyección de la dependencia ILoggingService correctamente:

Console: Inicializado el módulo principal de la aplicación

Las ventajas de la utilizacion del patron de diseño de inyección de dependencias se hacen evidentes a médida que el número de componentes de la aplicación aumenta, permitiendonos mantener un acoplamiento débil entre dichos componentes y facilitando el mantenimiento posterior de la aplicación al unificar el sistema de construcción y cableado de objetos.

Puede que surja la pregunta de que tipo de objetos debemos instanciar a través del contenedor. En mi experiencia, los componentes que forman parte de la arquitectura o flujo principal de la aplicación, esto es, servicios, vistas, presentadores, módelos, etc, son buenos candidatos para ser incluidos en el contenedor.

Finalizaremos este artículo enumerando dos ventajas instantaneas de utilizar un contenedor para estructurar nuestra aplicación:

  1. Si en lugar de mostrar los eventos del sistema por la consola decidimos registrarles en una base de datos, podríamos definir un nuevo tipo DatabaseLoggingService implementando el interface ILoggingService y registrar dicho tipo en lugar de ConsoleLoggingService. Con esa sencilla modificación la aplicación pasaría a registrar los eventos en una base de datos.
  2. El contenedor introduce el concepto de 'cadena de montaje' y nos permite extender las acciones a realizar a la hora de crear un objeto. Un uso típico es el 'EventBroker' o sistema de gestión de eventos que permite marcar eventos de los tipos con el atributo de publicación y métodos con el atributo de suscripción, encargandose el contenedor a la hora de construir los objetos de cablear los publicadores con los suscriptores facilitando la implementación del patron de diseño observador.

 

Distribuir contenido