martes, 4 de marzo de 2014

Patrones estructurales: Bridge

Este post corresponde a la serie Patrones de diseño - Patrones estructurales

Bridge

"Desacopla una abstracción de su implementación de forma que ambas pueden variar de manera independiente".

Como ya hemos hecho en otras ocasiones vamos a estudiar detenidamente la definición de este patrón, tratar de entender qué dice y después veremos cómo y dónde aplicarlo.

Cuando tenemos una abstracción que puede tener diferentes implementaciones normalmente usamos herencia para resolver la situación. Es decir, una clase abstracta define la interfaz de la abstracción y las clases hijas concretas implementan cada una hace su propia implementación.



Bien, y ¿qué problema hay en esto? La verdad, es que es lo que hemos visto y hemos hecho tantas y tantas vences ¿no?

No sé si he dicho esto antes o no, si no lo he dicho disculpadme, debí haberlo dicho en el primer post en mayúsculas:

El problema de la herencia es que crea diseños en general bastante rígidos y difícil de modificar. Siempre que sea posible debemos decantarnos por diseños basados en la composición de objetos frente a los basados en la herencia por que serán diseños más flexibles

Pues eso, justo es ese el problema de que las diferentes implementaciones de una abstracción hereden de la abstracción. La rigidez.

La herencia crea un enlace permanente entre la abstracción y las implementaciones de esa abstracción que hace que sea difícil modificar, extender y reutilizar la abstracción y las implementaciones de manera independiente una de otras.

A mí la siguiente cuestión que me viene a la cabeza de inmediato es ¿y por qué iba a querer yo reutilizar o modificar o extender la abstracción de forma independiente a su implementación o viceversa? al fin y al cabo si hago un cambio en una clase padre modifico las clases hijas que heredan de ella y eso es todo ¿no?

En realidad el problema puede ir un poco más allá. Vamos a verlo con un ejemplo:

El problema

Supongamos que vamos a desarrollar un editor gráfico 3D lo llamaremos 3DWorlds. Por simplicidad nuestro editor gráfico es capaz de representar cubos y conos en 3D. Hemos modelado una clase abstracta Figure con un método abstracto Draw y de esta clase heredan Cube y Pyramid.

Este sería el diagrama UML por el momento:



No voy a mostrar el código de este diagrama porque por el momento es muy simple. 
Hasta aquí no vemos un gran problema, aunque las implementaciones (Pyramid y Cube) y la abstracción (Figure) están fuertemente acopladas, este diagrama parece bastante manejable si hay cambios.

Como ya he dicho el problema puede ir un poco más allá. Nuestro editor 3DWorlds es un software serio y soporta diferentes perspectivas a la hora de proyectar los polígonos (para quien como yo no saben muy bien cómo funciona esto de las perspectivas gráficas mirar este artículo de la Wikipedia). Las perspectivas que vamos a soportar son las que tienen en cuenta uno, dos y tres puntos de fuga.

Así que nuestro nuevo diagrama UML con soporte para tres perspectivas diferentes es el siguiente:



Opps! Esto se complica por momentos. Al añadir al modelo implementaciones para tres tipos de perspectivas distintas se han disparado el número de clases que necesitamos. Pero además por cada perspectiva que añadimos a 3DWorlds debemos agregar una implementación para Cube y otra para Pyramid. 

¡¡¡ Y esto para sólo dos figuras y tres perspectivas !!! Pensemos que pasará cuando agreguemos todas las figuras geométricas y perspectivas que 3DWorlds debería tener. La jerarquía de clases combinando figuras y perspectivas será inmanejable.

La idea importante de esto es que la herencia impide que la abstracción y la implementación evolucionen por separado y rápidamente provocan esta explosión de clases.

La solución

El patrón Bridge propone una solución a este problema que como hemos dicho se base en desacoplar la abstracción (Figure) de la implementación (OnePointPerspective, TwoPointPerspective, ThreePointPerspective, etc). 

Antes de continuar veamos cómo es el diagrama UML del patrón Bridge:

Estos son los participantes del patrón:
  • Abstraction (Figure). Define la interfaz de la abstracción y mantiene una referencia al objeto Implementor.
  • RedefinedAbstraction (Cube, Pyramid). Extienden la interfaz definida por la abstracción.
  • Implementor. Define la interfaz de las implementaciones concretas. Esta interfaz no tiene por qué coincidir con la de la abstracción. Lo típico es que la interfaz de Implementor defina operaciones primitivas (DrawLine, DrawPoint, DrawEllipse, etc) y la abstracción defina operaciones de más alto nivel (DrawRectangle, DrawTriangle, etc.) basadas en las operaciones primitivas del Implementor.
  • ConcreteImplementor (OnePointPerspective, etc.) Hace una implementación concreta de la interfaz de Implementor
Vemos cómo sería un extracto de la implementación para nuestro ejemplo:


    /// 
    /// Interfaz para las clases que contienen la implementacion
    /// concreta de cada perspectiva.
    /// Define metodos de bajo nivel para dibujar.
    /// 
    public interface IImplementor
    {
        void DrawLine(Point3D point1, Point3D point2);
        void DrawPoint(Point3D point);
    }

    public class OnePointPerspective : IImplementor
    {
        object internalDrawingContext;

        /// 
        /// Crea una instancia de un implementor para 
        /// perspectivas con un punto de fuga
        /// 
        /// 
        /// Contexto de dibujo. Aquí es object como solo a
        /// modo de referencia. Podria ser del tipo 
        /// System.Windows.Media.DrawingContext usado en WPF
        /// 
        public OnePointPerspective(object drawingContext)
        {
            internalDrawingContext = drawingContext;
        }

        public void DrawLine(System.Windows.Media.Media3D.Point3D point1, System.Windows.Media.Media3D.Point3D point2)
        {
            // Realizamos las transformaciones que puedan ser
            // necesarias en los puntos para la perspectiva
            // con un punto de fuga
            Point3D convertedPoint1 = ConvertPoint(point1);
            Point3D convertedPoint2 = ConvertPoint(point2);

            // Dibujamos la línea.
            // Sería algo así como la siguiente linea

            //internalDrawingContext.DrawLine(convertedPoint1, convertedPoint2);
        }

        public void DrawPoint(System.Windows.Media.Media3D.Point3D point)
        {
            // Realizamos las transformaciones que puedan ser
            // necesarias en el punto para la perspectiva
            // con un punto de fuga
            Point3D convertedPoint = ConvertPoint(point);

            // Dibujamos el punto.
            // Sería algo así como la siguiente linea

            //internalDrawingContext.DrawPoint(convertedPoint);
        }

        private Point3D ConvertPoint(Point3D point)
        {
            Point3D convertedPoint = new Point3D();
            // Aqui realizamos las trasnformaciones necesarias
            // sobre convertedPoint
            // para aplicar la perspectiva de un punto de fuga
            // al punto.
            return convertedPoint;
        }
    }

    public class TwoPointPerspective : IImplementor
    {
        object internalDrawingContext;

        /// 
        /// Crea una instancia de un implementor para 
        /// perspectivas con dos puntos de fuga
        /// 
        /// 
        /// Contexto de dibujo. Aquí es object como solo a
        /// modo de referencia. Podria ser del tipo 
        /// System.Windows.Media.DrawingContext usado en WPF
        /// 
        public TwoPointPerspective(object drawingContext)
        {
            internalDrawingContext = drawingContext;
        }

        public void DrawLine(System.Windows.Media.Media3D.Point3D point1, System.Windows.Media.Media3D.Point3D point2)
        {
            // Realizamos las transformaciones que puedan ser
            // necesarias en los puntos para la perspectiva
            // con dos punto de fuga
            Point3D convertedPoint1 = ConvertPoint(point1);
            Point3D convertedPoint2 = ConvertPoint(point2);

            // Dibujamos la línea.
            // Sería algo así como la siguiente linea

            //internalDrawingContext.DrawLine(convertedPoint1, convertedPoint2);
        }

        public void DrawPoint(System.Windows.Media.Media3D.Point3D point)
        {
            // Realizamos las transformaciones que puedan ser
            // necesarias en el punto para la perspectiva
            // con dos punto de fuga
            Point3D convertedPoint = ConvertPoint(point);

            // Dibujamos el punto.
            // Sería algo así como la siguiente linea

            //internalDrawingContext.DrawPoint(convertedPoint);
        }

        private Point3D ConvertPoint(Point3D point)
        {
            Point3D convertedPoint = new Point3D();
            // Aqui realizamos las trasnformaciones necesarias
            // sobre convertedPoint
            // para aplicar la perspectiva de dos puntos de fuga
            // al punto.
            return convertedPoint;
        }
    }

    /// 
    /// Estructura que representa un triangulo con
    /// puntos 3D
    /// 
    public struct Rectangle3D
    {
        public Point3D Point1 { get; set; }
        public Point3D Point2 { get; set; }
        public Point3D Point3 { get; set; }
        public Point3D Point4 { get; set; }
    }

    /// 
    /// Clase abstracta define la interfaz de figuras que podemos dibujar
    /// Tambien define métodos de dibujo de alto nivel
    /// 
    public abstract class Figure
    {
        private IImplementor internalPerspectiveImplementor;

        protected Figure(IImplementor perspectiveImplementor)
        {
            internalPerspectiveImplementor = perspectiveImplementor;
        }

        /// 
        /// Metodo de alto nivel para dibujar un rectangulo
        /// 
        /// 
        protected void DrawRectangle(Rectangle3D rectangle)
        {
            internalPerspectiveImplementor.DrawLine(rectangle.Point1, rectangle.Point2);
            internalPerspectiveImplementor.DrawLine(rectangle.Point2, rectangle.Point3);
            internalPerspectiveImplementor.DrawLine(rectangle.Point3, rectangle.Point4);
            internalPerspectiveImplementor.DrawLine(rectangle.Point4, rectangle.Point1);
        }

        /// 
        /// Metodo de alto nivel para dibujar un triangulo a partir de 3 puntos
        /// 
        protected void DrawTriangle(Point3D point1, Point3D point2, Point3D point3)
        {
            internalPerspectiveImplementor.DrawLine(point1, point2);
            internalPerspectiveImplementor.DrawLine(point2, point3);
            internalPerspectiveImplementor.DrawLine(point3, point1);
        }

        /// 
        /// Metodo abstracto que implmentaran las clases hijas.
        /// 
        public abstract void Draw();
    }

    /// 
    /// Pirámide con base rectangular
    /// 
    public class Pyramid : Figure
    {

        private Rectangle3D internalBase;
        private Point3D internalVertex;

        /// 
        /// Crea una instancia de una piramide con base rectangular
        /// 
        /// 
        /// Perpectiva
        /// 
        /// Base de la pirámide
        /// Vértice
        public Pyramid(IImplementor perspectiveImplementor, Rectangle3D @base, Point3D vertex)
            : base(perspectiveImplementor)
        {
            internalBase = @base;
            internalVertex = vertex;
        }

        public override void Draw()
        {
            // Dibujamos la base
            DrawRectangle(internalBase);

            // Dibujamos las caras triangulares de la pirámide.
            // Cada cara es un triangulo formado por dos puntos consecutivos de 
            // la base y el vértice
            DrawTriangle(internalBase.Point1, internalBase.Point2, internalVertex);
            DrawTriangle(internalBase.Point2, internalBase.Point3, internalVertex);
            DrawTriangle(internalBase.Point3, internalBase.Point4, internalVertex);
            DrawTriangle(internalBase.Point4, internalBase.Point1, internalVertex);
        }
    }

    /// 
    /// Cubo
    /// 
    public class Cube : Figure
    {

        private Rectangle3D internalFace1;
        private Rectangle3D internalFace2;
        private Rectangle3D internalFace3;
        private Rectangle3D internalFace4;
        private Rectangle3D internalFace5;
        private Rectangle3D internalFace6;

        /// 
        /// Crea una instancia de un cubo a partir de sus 6 caras
        /// 
        /// 
        public Cube(IImplementor perpectiveImplementor,
                                            Rectangle3D face1,
                                            Rectangle3D face2,
                                            Rectangle3D face3,
                                            Rectangle3D face4,
                                            Rectangle3D face5,
                                            Rectangle3D face6)
            : base(perpectiveImplementor)
        {
            internalFace1 = face1;
            internalFace2 = face2;
            internalFace3 = face3;
            internalFace4 = face4;
            internalFace5 = face5;
            internalFace6 = face6;
        }
        public override void Draw()
        {
            // Dibujamos las seis caras del cubo
            DrawRectangle(internalFace1);
            DrawRectangle(internalFace2);
            DrawRectangle(internalFace3);
            DrawRectangle(internalFace4);
            DrawRectangle(internalFace5);
            DrawRectangle(internalFace6);

        }
    }

Lo primero es lo primero. Yo no tengo mucha idea de diseño 3D, así que tanto la implementación de perspectivas como todo lo relacionado con el dibujo 3D puede que esté basado en conceptos no realistas. De cualquier forma, el fin del ejemplo no es mostrar cómo crear un editor 3D propiamente dicho sino que sirve solamente como vehículo para explicar el patrón Bridge.

Lo que podemos comprobar es que ahora la implementación de cómo se pintan las figuras (Implementor concretos) está completamente desacoplada de las propias figuras. Esto nos permite por ejemplo agregar nuevas figuras (Cilindro, Cono, Octaedro, etc.) sin tener que agregar nuevos Implementors. O por otro lado agregar nuevas perspectivas sin tener que modificar nuestras figuras. Es decir, como dice la propia definición del patrón Bridge tanto la definición de la interfaz como la implementación pueden evolucionar de forma independiente.

Otra cosa interesante del Bridge es que oculta al cliente los detalles de cómo se dibujan las figuras. El cliente sólo tiene que invocar el método Draw de la figura.

Es usual encontrar el patrón Bridge en framework de UI. Típicamente los framework multi-plataforma usan alguna variante del patrón Bridge para la UI. Por ejemplo crean la abstracción "Window" y una implementación distinta para cada plataforma soportada (winPhoneWindow, androidWindow, etc) que realizan la implementación concreta en cada plataforma para dibujar una ventana.

Consideraciones antes de implementar el patrón Bridge

Veamos algunas cosas a tener en cuenta a la hora de implementar el patrón Bridge.

  • Evidentemente aplicar el patrón Bridge no tendría ningún sentido si no hubiera varias implementaciones. Es decir si nuestra aplicación 3DWorlds solo puede dibujar con una perspectiva no hay razón para implementar este patrón.
  • Determinar cómo se crean las implementaciones. Hay que decidir la forma en que se van a instanciar las perspectivas. Es posible que queramos asegurarnos que las diferentes figuras de un dibujo se pinten todas con la misma perspectiva. En ese caso quizá queramos instanciar la perspectiva como un Singleton.
  • Es posible reutilizar las implementaciones. En muchos casos las implementaciones pueden ser reutilizadas. En nuestro caso la implementación es el responsable último de realizar el dibujo, podríamos reutilizar la implementación para dibujar un fondo de la imagen a crear un dibujo nuevo.
Pues hasta aquí el patrón Bridge. El próximo post será sobre un patrón ampliamente utilizado. El patrón Composite

11 comentarios:

  1. Este comentario ha sido eliminado por el autor.

    ResponderEliminar
  2. Me está gustando mucho esta serie de posts sobre patrones de diseño.

    Una duda, ¿hasta qué punto un bridge es diferente de un strategy? En el ejempo que has puesto, las distintas implementaciones de perspectivas podrían considerarse estrategias de dibujado.

    Hay veces en las que la frontera entre dónde acaba un patrón y dónde empieza otro me parece muy sutil.

    ResponderEliminar
  3. Hola Juanma.
    Muy buena observación.
    Como bien dices Bridge y Strategy puede parecerse al implementarlos. La diferencia fundamental está en la finalidad de cada uno.

    Mientras que como hemos visto en el post Bridge es un patrón estructural y su objetivo es solventar un problema de estructuras de clases (la explosión del número de clases de la herencia); Strategy es un patrón de comportamiento, y como veremos en su correspondiente post el objetivo es crear una familia de comportamientos intercambiables dinámicamente de forma que cuando sustituimos una estrategia por otra el objeto que contiene las estrategias cambia su comportamiento.


    En resumen, aunque la implementación es muy similar. La finalidad de Bridge y Strategy es completamente diferente.

    Espero que esto conteste a tu pregunta.

    ResponderEliminar
  4. Estoy de acuerdo, se puede ver más como una cuestión de intención que puramente estructural. Aunque el código sea (casi) el mismo, la intención es diferente.

    Sería algo similar a lo que se comenta aquí: http://blog.koalite.com/2013/02/es-peligroso-duplicar-conceptos-no-codigo/

    ResponderEliminar
  5. Exactamente Juanma como te decía la diferencia fundamental es el objetivo con el que aplicamos cada uno.
    Estupendo blog el de Koalite y muy buen post el que me señalas. Sobre esta idea habla también el principio de diseño DRY (Don't Repeat Yourself), no se refiere solamente a código estrictamente hablando, sino a conceptos y conocimiento en general. Echa un vistazo a este post: http://designcodetips.blogspot.com.es/2013/11/design-principles-dry-yagni-kiss.html

    ResponderEliminar
  6. Este comentario ha sido eliminado por el autor.

    ResponderEliminar
  7. Carlos Duque (ChkDuke)5 de marzo de 2014, 17:09

    Como de costumbre, me ha gustado mucho (porque la he entendido, sobre todo), la explicación del patrón Bridge.
    Como me he ido leyendo los comentarios tuyos y de Juanma sobre la diferencia entre Bridge y Strategy, me ha dado por investigar un poco este segundo y me he encontrado este otro post (http://carlospeix.com/2011/04/cul-es-la-diferencia-entre-el-patrn-strategy-y-state/) donde hacen también una comparación de Strategy con el patrón State.
    Curiosamente un conocido me acaba de recomendar el uso de dicho patrón State como una buena forma de cumplir con el principio Open/Close de SOLID, así que, y para eso venía todo este rollo, si es posible realizar peticiones, me encantaría un próximo post sobre el patrón State.

    ResponderEliminar
  8. Hola Carlos.

    Lo primero agradecerte tu participación y, como entiendo por tu comentario que eres también asiduo, también agradecerte que leas este blog.

    Efectivamente; como tú dices y ya hemos discutido Juanma y yo. Hay algunos patrones que son muy parecidos y a veces sólo se diferencian en la finalidad del mismo o la forma de implementarlos.

    State por ejemplo cambia su comportamiento dependiendo del estado interno del objeto, mientras que en Estrategy es el cliente quien establece la estrategia. Como ves son detalles, pero ya sabes... "The Devil is in the detail".

    Con respecto a tu petición. Ya está previsto tanto Strategy como State, pero como sabes son patrones de comportamiento. Así que primero permíteme que acabe con los patrones estructurales y después entraremos con los de comportamiento. Por llevar un orden.

    ResponderEliminar
  9. Carlos Duque (ChkDuke)6 de marzo de 2014, 10:04

    Si, claro, por supuesto, tu sigue con tu orden establecido, que esta muy bien. Si al fin y al cabo, me los voy a acabar leyendo todos...

    ResponderEliminar
  10. La verdad es que el ejemplo que has puesto es poco afortunado, porque para empezar dudo que alguien derive una clase por cada tipo de proyección. La solución dada tampoco me parece buena (al menos, para el ejemplo que pones). Claro, dices " Yo no tengo mucha idea de diseño 3D". Quizá deberías haber buscado otro ejemplo en un campo que sí conozcas, ¿no crees?

    La solución apropiada para el problema que planteas la da la propia biblioteca gráfica (o debería) ya que símplemente definiendo las matrices de transformación y proyección apropiadas obtienes la vista que buscas, usando EXACTAMENTE el mismo código para dibujar el objeto 3D (Es más, puedes usar el mismo código para dibujar CUALQUIER figura 3D, puesto que esta está definida por los datos). Es decir, el método TFigure::Draw simplemente dibuja la figura, sin preocuparse de la proyección ni de la figura. La derivación a TPiramid y TCube no me parece mal, ya que estas pueden servir para definir las figuras al darles propiedades como tamño, color, textura, etc.

    De todas formas, incluso si la biblioteca gráfica no manejara las proyecciones de la forma en que lo hacen prácticamente todas, un patrón Modelo/Vista funcionaría bastante bien.

    En definitiva, no veo la utilidad de este patrón. Lo siento.

    Y también siento el vapuleo, pero es que soy un ferviente activista anti-patrones pro-reinventa-la-rueda.

    ResponderEliminar
    Respuestas
    1. Hola Momar.

      No te discutiré que el ejemplo pueda que no sea el más afortunado. Realmente encontrar ejemplos que no sean los habituales es a veces complicado y es posible que en este me haya equivocado, "mea culpa". Pero no quería poner un ejemplo típico (ventanas en frameworks multi-plataforma) que encontraremos repetido hasta la saciedad.

      Si la solución te parece buena me alegro por que al fin y al cabo como ya digo en el post el ejemplo no es más que un "vehiculo" para explicar el patrón.

      Por otro lado, entiendo que las bibliotecas gráficas soportan toda esa funcionalidad. La cuestión es que la finalidad del post no es explicar cómo usar una biblioteca gráfica sino cómo podría implementarse esa funcionalidad usando el patrón Bridge (No quiero decir que esa funcionalidad esté implementada con Bridge en las librerías gráficas).

      Siento no tener opinión sobre el patrón Modelo/Vista, no lo conozco. Si te soy sincero por el nombre, que yo conozca, me suena a patrones "Model/View/Presenter", "Model/View/Controler" o "Model/View/ViewModel". Pero estos son patrones de aplicación con un nivel de abstracción superior, patrones de más alto nivel que el Bridge y que por lo tanto resuelven problemas de más alto nivel también. Si quieres podrías comentar o poner algún link sobre el patrón Modelo/Vista al que te refieres.

      Con respecto a la utilidad práctica del patrón Bridge tengo que decirte que yo creo que es muy útil. Está claro que como tu dices no es cuestión de "pro-reinventa-la-rueda", sino aplicarlo en el contexto correcto. El contexto más habitual donde lo encuentras suele ser en framework multi-plataforma. La mayoría de ellos usan de una u otra forma alguna variante del patrón.

      Por lo del "vapuleo" no te preocupes, yo no lo veo así. En este post trato de explicar de la forma más simple y clara que puedo estos patrones (entre otras cosas que irán llegando). Tu tienes tu opinión y te agradezco que la expongas por que se trata de eso, de discutir y aprender unos de otros de estos temas.

      Gracias por dar tu opinión en este blog.

      Eliminar