jueves, 28 de noviembre de 2013

Introducción a los patrones de diseño

Realizar diseños orientados a objetos es difícil, y realizar diseños orientados a objetos reutilizables es más difícil aún. Tienes que encontrar los objetos correctos, encapsularlos en clases con la granularidad correcta, definir las interfaces, la herencia y las relaciones entre los objetos.

Una de las cosas que los desarrolladores debemos aprender cuanto antes es que no debemos afrontar la resolución de los problemas siempre desde cero. En lugar de eso debemos buscar soluciones que hayan funcionado a nosotros mismos o a otros en el pasado.



Los patrones son justo eso. Una guía de soluciones probadas a problemas recurrentes en el diseño de software. Pero atención a que digo una guía, no la solución. De hecho es casi imposible que un patrón se aplique tal cual, lo normal es que requiera de diferentes adaptaciones al problema concreto que queremos solucionar. Por lo tanto debemos tomar los patrones de diseño como una guía, como una fuente de inspiración para encontrar la solución a nuestros problemas concretos. Tratar de usar los patrones como si fueran dogmas es probablemente peor que no usarlos en absoluto. Sólo conseguiremos crear código complejo y difícil de mantener. Tampoco debemos olvidar que con o sin patrones nuestros diseños deberían aplicar los principios de diseño que hemos visto en post anteriores.

Por mi experiencia creo que es muy difícil encontrar, cuando diseñamos, un problema "original". Me explico. Cuando afrontamos un problema de diseño, ese problema en concreto, es posible que sea único. Que nadie antes haya resuelto un problema como el nuestro, con todos sus detalles. Y eso será así siempre que miremos nuestro problema con un nivel de abstracción de grano muy fino, con mucho detalle. Sin embargo, si lo enfocamos con un nivel de abstracción mayor la cosa es diferente, no encontraremos una solución concreta a nuestro problema pero sí una guía de cómo otros han solucionado problemas similares.

Veamos un ejemplo de lo que hablo:
Supongamos que tenemos que diseñar un módulo que gestiona nuestros productos y sus categorías como un árbol. Cada categoría puede contener otras categorías o directamente productos, aunque la máxima profundidad de subcategorías es 3. Además necesitamos que seleccionando un nodo de ese árbol podamos aplicar un incremento en el precio de los productos que forman parte de las ramas de ese nodo. Y también debemos poder mover nodos de una rama a otra. Como ejemplo no pondré más funcionalidad a aplicar sobre el árbol, pero imaginemos que es mucha más.

No será fácil encontrar una solución ya creada y probada para resolver este problema en concreto, pero si lo miramos desde un nivel de abstracción un poco superior (de grano más grueso). Podríamos definir nuestro problema así:

Queremos gestionar entidades en forma de árbol, de manera que cada una puede tener una serie de hijos y estos hijos a su vez otros hijos etc. Este árbol tendrá alguna limitación a la hora de componerlo. Además debemos poder aplicar mucha funcionalidad distinta a los nodos del árbol.

Bien, desde este nivel de abstracción el problema se ve distinto. Como veremos en futuros post un patrón Composite nos serviría de guía para crear el árbol, y un patrón Visitor nos permitiría agregar funcionalidad al árbol pero separada del árbol para mantenerlo lo más "limpio" posible.

Vuelvo a recalcar que estos patrones no nos darán una solución para aplicar directamente, sino más bien una guía para solucionar nuestro problema. Los patrones suelen tener un nivel de abstracción superior al problema que queremos solucionar, por lo que tendremos que hacer modificaciones hasta que se adapten a nuestro problema concreto.
Los patrones no son, en sí mismos, una solución a los problemas concretos sino una guía para solucionarlos.

Otra consecuencia importante cuando usamos soluciones basadas en patrones es que cuando hablamos con compañeros de trabajo u otros colegas y les decimos que al final hemos solucionado el problema anterior con un patrón Composite y un Visitor para la funcionalidad, inmediatamente se hacen una buena idea de cómo se ha solucionado el problema sin tener que dar un sin fin de detalles. Es decir, el uso de patrones mejora la comunicación con tu propio equipo.

Bueno, una vez hecha esta introducción de los patrones de diseño vamos a enumerar qué patrones vamos a ver en la serie que ahora empieza y cómo voy a enfocarlos.

Los patrones que veremos se basan en los patrones de diseño expuestos en el libro "Design Patterns - Elements of Reusable Object-Oriented Software" de los conocidos como "Gang of Four".

Los patrones que vamos a ver los dividiremos en tres categorías:

  • Patrones de creación. Estos patrones nos ayudan en el proceso de instanciación de objetos. Los patrones de creación de clases se basan en la herencia para decidir que objeto se crea, mientras que los patrones de creación de objetos delegan la instanciación en otro objeto.
    • Abstract Factory. Provee una interfaz para crear familias de objetos relacionados sin especificar sus clases concretas.
    • Builder. Separa la construcción de un objeto complejo de su representación.
    • Factory Method. Define una interfaz para crear un objeto,  pero delega en las subclases la decisión de qué clase instanciar.
    • Prototype. Especifica los tipos de objetos a crear usando un objeto prototipo y crea nuevos objetos clonando al prototipo.
    • Singleton. Asegura que sólo existe una instancia de una determinada clase y provee un acceso global a esa instancia.
  • Patrones estructurales. Tienen que ver con cómo las clases y los objetos se componen para crear estructuras más grandes y nueva funcionalidad. Agregan una gran flexibilidad frente a estructuras estáticas por su capacidad para cambiar la composición en tiempo de ejecución.
    • Adapter. Convierte la interfaz de una clase en otra interfaz que el cliente espera.
    • Bridge. Desacopla una abstracción de su implementación y ambas pueden evolucionar por separado.
    • Composite. Compone objetos en estructuras de árbol para representar jerarquías del tipo parte-todo.
    • Decorator. Agrega responsabilidades a un objeto de forma dinámica.
    • Facade. Provee una interfaz que unifica un conjunto de interfaces de un subsistema.
    • Flyweight. Permite soportar un gran número de objetos compartiendo su parte común.
    • Proxy. Provee un sustituto de un objeto para controlar el acceso al objeto sustituido.
  • Patrones de comportamiento. Tienen que ver con algoritmos, asignación de responsabilidades y comunicación entre objetos.
    • Chain of responsability. Evita el acoplamiento entre el emisor y el receptor de una solicitud.
    • Command. Encapsula una petición en un objeto.
    • Interpreter. Dado un lenguaje, define una representación para su gramática y un intérprete que usa esa representación.
    • Iterator. Provee una forma de acceder a los elementos de un agregado de forma secuencial sin exponer su representación interna.
    • Mediator. Define un objeto que encapsula cómo un conjunto de objetos interactuan.
    • Memento. Captura y almacena el estado interno de un objeto, sin violar la encapsulación, para poder restaurar ese estado interno de nuevo más adelante.
    • Observer. Define una dependencia de uno a muchos entre objetos de forma que cuando el objeto observado cambia su estado todos los demás dependientes son notificados.
    • State. Permite a un objeto cambiar su comportamiento cuando cambia su estado interno. 
    • Strategy. Define una familia de algoritmos, encapsula cada uno de ellos y los hace intercambiables.
    • Template method. Define el esqueleto de un algoritmo en una operación y delega en las subclases para implementar algunos de los pasos.
    • Visitor. Representa una operación que se aplica a los elementos de una estructura de objetos. Permite agregar operaciones sin cambiar los objetos a los que se les aplica.
En los post de la serie veremos en detalle cada uno de estos patrones. Para estudiarlos seguiremos la siguiente estructura:
  1. El problema. No olvidemos que un patrón es una guía para solucionar un problema. Lo primero que haremos será identificar y estudiar el problema que pretendemos resolver.
  2. El patrón. Estudiaremos el patrón original ofrecido por GoF.
  3. El patrón en .NET cada plataforma y cada lenguaje tiene sus propios modismos, su propia forma de implementar los conceptos. En esta sección veremos cómo implementar el patrón con C#.
  4. Consecuencias. Aquí veremos las consecuencias de aplicar el patrón en un sistema. 
  5. Relaciones con otros patrones habitualmente un problema no se soluciona aplicando un patrón aislado, sino que se usan una combinación de ellos. En esta sección veremos para cada patrón las formas habituales de relacionarse con otros.
Aquí termina la introducción a los patrones de diseño, a partir del próximo post entramos de lleno en materia.

3 comentarios:

  1. Hola Miguel
    Desde mi punto de vista una solución de negocio se concibe desde el diseño usando UML(Casos de uso, Actividades y diagrama de Clases) bueno en mi caso yo uso esos 3 diagramas.
    Lo primero hay que comprender el negocio, para poder reconocer los procesos principales del negocio y plantear una solución y después de todo el analisis reconocer las reglas del negocio.
    Y otra cosa importante una solución de negocio no puede ser solución de otra por ejemplo una solución de inmobiliarias no puede ser solución de una ferretería.
    Después de todo este análisis ya se puede escoger la tecnología(lenguaje de programación) y la arquitectura(los patrones a los cuales te refieres), pq una cosa es patrón de arquitectura que se encarga de la estructura de la aplicación y otra cosa es un patrón de diseño que se implementa en la capa de presentación ya se el caso de MVC, MVP, MVVM, etc.

    ResponderEliminar
  2. Muy bueno el post compañero, voy a seguir leyendo los demás, estoy aprendiendo mucho con esto. Gracias

    ResponderEliminar