lunes, 17 de marzo de 2014

Patrones estructurales: Composite

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

Composite

"Compone objetos en estructuras de tipo árbol para representar jerarquías del tipo parte-todo. Permite que el cliente trate de forma uniforme tanto un objeto como una composición de objetos".

Son muchas las situaciones en las que nos encontramos ante estructuras de datos de tipo árbol como se explica en el enunciado del patrón Composite. Un problema típico con este tipo de estructuras es que debemos tratar de forma diferente a los nodos hoja (los nodos que están al final de la rama, sin hijos) de los nodos que no lo son.



El problema

Vamos a volver al ejemplo del editor gráfico 3DWorlds del post sobre el patrón Bridge. Este editor gráfico nos permite dibujar figuras 3D proyectándolas con diferentes perspectivas. 

Bien, una funcionalidad típica en este tipo de software es la agrupación de elementos. Nuestros usuarios quieren poder agrupar varias figuras y además quieren tratar ese grupo como si fuera un elemento más del dibujo. Quieren poder mover ese grupo, cortar, pegar, cambiar el tamaño (resize), moverlo adelante y atrás en el eje Z, agruparlo a otras figuras o grupos, etc. En definitiva, queremos tratar una agrupación de figuras igual que si fuera una figura cualquiera.

Dejando a un lado las funcionalidades que queremos aplicar y centrándonos en la parte estructural del problema, lo que básicamente tenemos es que cada elemento en nuestro editor gráfico puede ser una figura o bien un grupo de elementos. Cada grupo puede a su vez estar compuesto por un numero indeterminado de figuras, de otros grupos de elemento, o ambos.

De forma gráfica podría representarse así:


Este árbol representa un gráfico con tres cubos y dos pirámides, pero algunas de las figuras están agrupadas.

En este punto la idea original era mostrar un código que soportara esta estructura sin aplicar ningún patrón (o más bien aplicando de forma intensiva como dirían mis compañeros Lucio e Iván el patrón "i-efe" :o) ). Pero sinceramente, después de ponerme un par de veces, es un código tan "feo", lleno de sentencias "if", frágil, etc. que si me lo permitís ni siquiera voy a ponerlo.

Así que vamos a ver directamente la solución que propone el patrón Composite tanto para soportar la estructura del árbol como para permitir que el cliente trate a todos los nodos de la misma manera independientemente de que sean figuras o grupos. Esto va a facilitar mucho la tarea al cliente. Después aplicaremos el patrón para resolver el problema anterior y veremos como quedaría la implementación.

La solución

Veamos el diagrama UML del patrón Composite y vamos a analizarlo:


Participantes en el patrón:
  • Component (crearemos la clase Glyph): 
    • Define una interfaz común para todos los nodos del árbol, tanto para grupos como para figuras.
    • Define una interfaz para acceder y gestionar los elementos hijos.
  • Leaf (Pyramid, Cube): Representan objetos hoja del árbol. Son objetos que no tienen hijos.
  • Composite (Grupo, crearemos la clase GlyphGroup): 
    • Define el comportamiento para objetos que tienen hijos.
    • Almacenan hijos de tipo Component.
    • Implementan las operaciones relacionadas con la gestión de hijos de la clase Component.
  • Client: Manipula los objetos de la composición a través de la clase Component.
Lo primero que debemos observar es que el patrón Composite define una interfaz común para todos los elementos de la composición. Esta interfaz (la clase Component) permite al cliente tratar a todos los elementos del árbol de forma uniforme. En nuestro ejemplo de la aplicación 3DWorlds que describíamos al principio de post no tenemos aún esta clase. Crearemos la clase Glyph (Glifo) hará el papel de la clase Component y  representa cualquier cosa que pueda pintarse en nuestro editor gráfico (Pirámide, Cubo o Grupo).

Vamos a ver como implementaríamos estos conceptos para resolver nuestro problema. Partiremos de la implementación que ya teníamos del post del patrón Birdge y haremos los cambios oportunos para permitir agrupar elementos.
/// 
/// Clase que representa cualquier cosa que pueda dibujarse 
/// en el editor gráfico.
/// 
public abstract class Glyph
{

    /// 
    /// Agrega un hijo. 
    /// Por defecto no hace nada, la clase GlyphGroup
    /// sobreescribirá este método con lógica.
    /// Los objetos hoja no lo sobreescriben, una hoja
    /// no puede agregar hijos.
    /// 
    /// 
    public virtual void Add(Glyph child) { }

    /// 
    /// Elimina un hijo. 
    /// Por defecto no hace nada, la clase GlyphGroup
    /// sobreescribirá este método con lógica.
    /// Los objetos hoja no lo sobreescriben, una hoja
    /// no puede eliminar hijos.
    /// 
    /// 
    public virtual void Remove(Glyph child) { }

    /// 
    /// Obtiene los hijos.
    /// Este método es una variación de la definición original
    /// del método que sería Glyph GetChild(int index)
    /// En nuestro caso esta variante puede que sea más util,
    /// aunque si es necesario podemos implementar las dos.
    /// La implementación de este método devuelve un enumerable vacío.
    /// La clase GlypGroup lo sobreescribirá para devolver sus hijos.
    /// 
    /// 
    public virtual IEnumerable<Glyph> GetChildren() { return Enumerable.Empty<Glyph>(); }



    /// 
    /// Metodo abstracto que implmentaran las clases hijas.
    /// En nuestro caso esta es la operación específica de 
    /// negocio, las anteriores son exclusivamente para 
    /// gestionar los hijos del composite.
    /// 
    public abstract void Draw();

    /// 
    /// Método que permite mover un glifo basándose 
    /// en desplazamientos para los ejes X, Y, Z
    /// 
    /// 
    /// 
    /// 
    public abstract void Move(double offsetX, double offsetY, double offsetZ);
}

/// 
/// Clase abstracta define la interfaz de figuras que podemos dibujar
/// Tambien define métodos de dibujo de alto nivel
/// 
public abstract class Figure : Glyph
{
    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);
    }
}

/// 
/// Composite. Clase que agrupa objetos de tipo Glyph
/// y que el cliente puede manejar igual que si fuera 
/// un Glyph simple.
/// 
public class GlyphGroup : Glyph
{
    private List<Glyph> internalChildren;

    public GlyphGroup()
    {
        internalChildren = new List<Glyph>();
    }


    public override void Add(Glyph child)
    {
        // Por simplicidad omitimos comprobaciones
        // previas a agregar el glifo. Como que no
        // exista ya en el listado.
        internalChildren.Add(child);
    }

    public override void Remove(Glyph child)
    {
        internalChildren.Remove(child);
    }

    public override IEnumerable<Glyph> GetChildren()
    {
        return internalChildren;
    }

    /// 
    /// GlyphGroup no tiene ninguna representación propia,
    /// el método draw simplemente llama al método Draw
    /// de todos sus hijos.
    /// 
    public override void Draw()
    {
        Console.WriteLine("Inicio de grupo");
        foreach (var glyph in internalChildren)
        {
            glyph.Draw();
        }
        Console.WriteLine("Fin de grupo");
    }

    public override void Move(double offsetX, double offsetY, double offsetZ)
    {
        foreach (var glyph in internalChildren)
        {
            glyph.Move(offsetX, offsetY, offsetZ);
        }
    }
}

/// 
/// 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; }

    public void Move(double offsetX, double offsetY, double offsetZ)
    {
        // Asignamos un nuevo Point3D a las propiedades por que Point3D es un objeto
        // por valor no por referencia.
        // Si hicieramos Point1.Offset(offsetX, offsetY, offsetZ) no modificaría la
        // localización del punto por que la propiedad devuelve una copia del punto 
        // ver: http://msdn.microsoft.com/en-us/library/system.windows.media.media3d.point3d.offset.aspx

        Point1 = new Point3D(Point1.X + offsetX, Point1.Y + offsetY, Point1.Z + offsetZ);
        Point2 = new Point3D(Point2.X + offsetX, Point2.Y + offsetY, Point2.Z + offsetZ);
        Point3 = new Point3D(Point3.X + offsetX, Point3.Y + offsetY, Point3.Z + offsetZ);
        Point4 = new Point3D(Point4.X + offsetX, Point4.Y + offsetY, Point4.Z + offsetZ);
    }
}


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);
        // Para nuestro ejemplo sacaremos a la consola las coordenadas del punto
        Console.WriteLine(string.Format("Linea desde(X,Y,Z): {0},{1},{2} hasta:  {3},{4},{5}",
            point1.X, point1.Y, point1.Z, point2.X, point2.Y, point2.Z));
    }

    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);

        // Para nuestro ejemplo sacaremos a la consola las coordenadas del punto
        Console.WriteLine(string.Format("Punto en (X,Y,Z): {0},{1},{2}",
            point.X, point.Y, point.Z));
    }

    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.
        convertedPoint = point;
        return convertedPoint;
    }
}

/// 
/// 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()
    {
        Console.WriteLine("Inicio de pirámide");

        // 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);

        Console.WriteLine("Fin de pirámide");
    }

    public override void Move(double offsetX, double offsetY, double offsetZ)
    {
        // Aquí iría la lógica de borrado del la figura
        // de la posición actual. La omitimos por simplicidad

        // Calculamos las nuevas coordenadas de la pirámide
        internalBase.Move(offsetX, offsetY, offsetZ);
        internalVertex.Offset(offsetX, offsetY, offsetZ);
    }
}

class Program
{
    static void Main(string[] args)
    {
        object drawingContext = new object();

        // ******************************************
        // Dibujamos tres piramides con una perspectiva
        // de un punto de fuga
        // ******************************************
        var perspective1Point = new OnePointPerspective(drawingContext);

        // Pirámide 1
        // Creamos la base de una piramide
        var @base = new Rectangle3D
        {
            Point1 = new Point3D { X = 100, Y = 100, Z = 0 },
            Point2 = new Point3D { X = 100, Y = 100, Z = 100 },
            Point3 = new Point3D { X = 0, Y = 100, Z = 100 },
            Point4 = new Point3D { X = 100, Y = 0, Z = 0 },
        };
        var vertex = new Point3D { X = 100, Y = 200, Z = 50 };

        var pyramid = new Pyramid(perspective1Point, @base, vertex);

        // Pirámide 2
        // Creamos la base de una piramide
        var @base2 = new Rectangle3D
        {
            Point1 = new Point3D { X = 100, Y = 100, Z = 0 },
            Point2 = new Point3D { X = 100, Y = 100, Z = 100 },
            Point3 = new Point3D { X = 0, Y = 100, Z = 100 },
            Point4 = new Point3D { X = 100, Y = 0, Z = 0 },
        };
        var vertex2 = new Point3D { X = 100, Y = 200, Z = 50 };

        var pyramid2 = new Pyramid(perspective1Point, @base2, vertex);


        // Pirámide 1
        // Creamos la base de una piramide
        var @base3 = new Rectangle3D
        {
            Point1 = new Point3D { X = 100, Y = 100, Z = 0 },
            Point2 = new Point3D { X = 100, Y = 100, Z = 100 },
            Point3 = new Point3D { X = 0, Y = 100, Z = 100 },
            Point4 = new Point3D { X = 100, Y = 0, Z = 0 },
        };
        var vertex3 = new Point3D { X = 100, Y = 200, Z = 50 };

        var pyramid3 = new Pyramid(perspective1Point, @base3, vertex);

        // Agrupamos las pirámides 1 y 2
        var group = new GlyphGroup();
        group.Add(pyramid);
        group.Add(pyramid2);

        // Dibujamos el grupo y la pirámide 3
        group.Draw();

        pyramid3.Draw();

        // Movemos el grupo
        group.Move(-5000, 1000, 8000);
        group.Draw();

        // Movemos la pirámide 3
        pyramid3.Move(-300, 500, 679);
        pyramid3.Draw();

        Console.ReadLine();
    }
}

Está es la salida en la consola del código:



Aunque sólo sea un ejemplo muy simple, hay varias ventajas interesantes en las que debemos fijarnos.

  • Mediante la clase GlyphGroup hemos conseguido poder estructurar una jerarquía de tipo arbol de objetos de tipo Glyph. Podemos agregar objetos básicos Figure (en nuestro ejemplo dos objetos Pyramid) a un GlyphGroup y el cliente trata al grupo como si fuera un objeto básico más. Para dibujarlo el cliente sólo debe llamar al método Draw del grupo o al método Move si quiere moverlo. En el ejemplo no lo hemos hecho, pero también podemos agregar grupos como hijos de otro grupo ya que GlypGroup es también de tipo Glyph. Esto nos da infinitas opciones de agrupación.
  • El cliente no necesita distinguir si está tratando con un grupo o con una figura. Esto hace que el código del cliente sea mucho más simple que una alternativa basada en interminables sentencias if o switch. Como consecuencia si agregamos nuevos tipos de grupos o nuevas figuras el cliente no necesita modificarse.
Claro que casi nada en la vida es blanco o negro, y el patrón Composite igualmente tiene ciertas limitaciones que debemos conocer:
  • Ya hemos dicho que con Composite tenemos prácticamente infinitas opciones de composición de objetos. Esto sin embargo puede ser una desventaja en contextos donde existen limitaciones en los tipos o en la forma en que los objetos pueden componerse. Supongamos que por especificación no pudiésemos agrupar pirámides con cubos.
  • Otro inconveniente del patrón son los métodos de gestión de hijos (de esto hablaremos un poco más adelante). En principio no en una herencia una clase no debería implementar método o propiedades que no sean útiles para todos sus hijos. Sin embargo la clase Glyph implementa métodos como Add para agregar un hijo que es completamente innecesario en las hojas del arbol ya que las hojas no tienen hijos.

Consideraciones al implementar el patrón Composite

Existen una serie de consideraciones que debemos tener en cuenta a la hora de implementar un patrón Composite. Vamos a ver algunas de ellas:

Referencias explícitas al padre
En la implementación que hemos visto podemos navegar desde los nodos padre a los nodos hijos, pero no al contrario. Los nodos no tienen una referencia al padre. 
Tener una referencia al padre simplifica enormemente tanto poder moverse hacia arriba en el árbol de nodos como la operación de eliminar nodos. Además nos ayuda a soportar en nuestro árbol de nodos un patrón Cadena de responsabilidad (Chain of Resposability que veremos en un post en la serie de patrones de comportamiento).

La clase más habitual donde mantener una referencia al padre es en la clase Component (Glyph), ya que tanto los nodos Composite como las hojas heredarán la referencia y las operaciones para manejarla.

Es fundamental que se manejamos una referencia al padre nos ocupemos de que siempre se cumpla el invariante de que todos los hijos de un nodo (GlypGroup) tienen como padre al nodo (GlypGroup) que los tiene a ellos como hijos. La mejor forma de conseguir esto es que sea el propio padre quien asegure este invariante en las operaciones para gestionar hijos. Los métodos Add y Remove de GlypGroup los podríamos implementar de la siguiente forma:

public override void Add(Glyph child)
{
    // Aseguramos el invariante
    // De esta formar todos los hijos del 
    // GlypGroup tendrán al GlypGroup como padre.
    child.Parent = this;

    internalChildren.Add(child);
}

public override void Remove(Glyph child)
{
    // Eliminamos el padre del glifo a eliminar
    child.Parent = null;
    internalChildren.Remove(child);
}

Maximizar la interfaz de Component (Glyph)
Ya sabemos que uno de los objetivos de patrón Composite es conseguir que el cliente maneje una estructura de tipo árbol sin que tenga de distinguir entre nodo "rama" y nodos hoja. La mejor forma de conseguir esto ya que el cliente trata con la interfaz de la clase Glyph es tratar de que la interfaz de la clase Glyph defina el mayor número posible de operaciones comunes de los nodos hoja y los nodos "rama".

Como ya he mencionado anteriormente esto puede suponer un problema en el sentido en que definimos en la clase Glyph métodos como Add o Remove que en principio no son relevantes para las clases hoja (Pyramide). Desde un punto de vista estrictamente conceptual este problema desaparece si consideramos a los objetos hojas como un tipo de Component (GlyphGroup) que no devuelve hijos.

Balance entre transparencia y seguridad


Al implementar el patrón Composite deberemos hacer un balance entre una implementación transparente y una implementación segura.

A pesar de lo que acabamos de ver cabe preguntarse si es más correcto tener las operaciones de gestión de hijos en la clase Composite (Glyph) o en la clase Component (GlyphGroup). Realmente es aceptable tanto una opción como la otra. Es una decisión que tendremos que tomar nosotros para cada contexto donde apliquemos el patrón Composite y en el fondo no se trata más que hacer un balance entre transparencia y seguridad teniendo en cuenta lo siguiente:

  • Definir las operaciones de gestión de hijos (Add, Remove, GetChildre, etc) en la clase Composite (Glyph) nos aporta transparencia. Nos permite desde el cliente podemos tratar a todos los nodos de la misma forma como vemos en el ejemplo de nuestra implementación. A cambio perdemos seguridad, porque permitimos que el cliente pueda intentar hacer cosas sin sentido como intentar agregar un hijo a una hoja (pirámide).
  • Definir las operaciones de gestión de hijos en la clase Component (GlypGroup) nos aporta mayor seguridad. Cualquier intento por parte del cliente de hacer algo inapropiado como agregar un hijo a una hoja dará una excepción en tiempo de compilación.. Por otra parte, con esta aproximación perderemos transparencia ya que Composite (Glyph) y Component (GlyphGroup) tienen interfaces diferentes y obligamos al cliente a tratarlas de forma distinta.

En nuestra implementación nos hemos decantado por la transparencia frente a la seguridad. Para nuestro caso si el cliente intenta agregar un hijo a una pirámide no se producirá ejecutará ninguna operación y para nuestro diseñador 3D no será nada crítico.

Con la información vista hasta ahora si necesitas creo que no debería haber ningún problema para implementar un patrón Composite donde prime la seguridad frente a la transparencia.

Usos conocidos

El patrón Composite es ampliamente usado y podemos encontrar ejemplos de su implementación en prácticamente cualquier framework. En .NET podemos verlo en clases como las siguientes:

  • System.Windows.Forms.Control y sus clases derivadas. 
  • System.Xml.XmlNode y derivadas.
  • System.Web.UI.Control


Hasta  aquí el patrón Composite. Ojalá la espera haya valido la pena. En el próximo post hablaremos sobre otro patrón estructural Decorator.

No hay comentarios:

Publicar un comentario en la entrada