informaticaPC

Patrones de Diseño Software

Bridge

Según el libro de GoF este patrón de diseño permite desacoplar una abstracción de su implementación, de manera que ambas puedan variar de forma independiente.

Supongamos que tenemos una clase abstracta en la que se define un método que deberá implementar cada clase que herede de ella: ¿cómo haríamos si una clase hija necesitase implementarlo de forma que realizase acciones diferentes dependiendo de determinadas circunstancias?.

En dichos casos nos resultaría útil el patrón Bridge (puente) ya que 'desacopla una abstracción' (un método abstracto) al permitir indicar (durante la ejecución del programa) a una clase qué 'implementación' del mismo debe utilizar (qué acciones ha de realizar).

El diagrama UML de este patrón es el siguiente:

Patrón Bridge en UML

Veamos un ejemplo de uso de este patrón en el que creamos un sistema para elaborar lasagna, pudiendo ésta ser de carne o de verduras.

Main.java (Cliente según el diagrama anterior):

package estructurales.bridge.bridge01;

public class Main
{
    public static void main(String[] args)
    {
        // Crear un objeto de tipo 'AbstraccionRefinada' indicándole un 'ImplementadorConcreto'
        ElaborarAlimento lasagna = new ElaborarLasagna( new Carne() );
        lasagna.obtener();

        // Ahora le indicamos que use otra implementación para obtener la de verduras
        lasagna.setImplementador( new Verduras() );
        lasagna.obtener();
    }
}

ElaborarAlimento.java (Abstraccion según el diagrama anterior):

package estructurales.bridge.bridge01;

public abstract class ElaborarAlimento
{
    // Referencia al Implementador
    IElaborar implementador;

    String nombre;

    // --------------------------

    public IElaborar getImplementador()
    {
        return this.implementador;
    }

    // --------------------------

    public void setImplementador( IElaborar implementador )
    {
        this.implementador = implementador;
    }

    // --------------------------

    // Método a implementar por las clases que hereden
    public abstract void obtener();
}

ElaborarLasagna.java (una AbstraccionRefinada según el diagrama anterior):

package estructurales.bridge.bridge01;

public class ElaborarLasagna extends ElaborarAlimento
{
    public ElaborarLasagna( IElaborar implementador )
    {
        this.setImplementador( implementador );
    }

    // ------------------------

    @Override
    public void obtener()
    {
        System.out.println("Preparando lasagna...");

        this.getImplementador().procesar();
    }
}

IElaborar.java (Implementador según el diagrama anterior):

package estructurales.bridge.bridge01;

public interface IElaborar
{
    public void procesar();
}

Carne.java (un ImplementadorConcreto según el diagrama anterior):

package estructurales.bridge.bridge01;

public class Carne implements IElaborar
{
    public Carne() {
    }

    // --------------------------

    @Override
    public void procesar()
    {
        // Operaciones necesarias
        // ...

        System.out.println("\tlasgana de carne elaborada");
    }
}

Verduras.java (un ImplementadorConcreto según el diagrama anterior):

package estructurales.bridge.bridge01;

public class Verduras implements IElaborar
{
    public Verduras() {
    }

    // --------------------------

    @Override
    public void procesar()
    {
        // Operaciones necesarias
        // ...

        System.out.println("\tlasgana de verduras elaborada");
    }
}

Al ejecutarlo obtendríamos como resultado:

Ejemplo

EXPLICACIÓN:

  • En el programa principal se crea una instancia de ElaborarAlimento (de tipo AbstraccionRefinada) indicándole la implementación que debe usar, para luego utilizarla a la hora de elaborar el producto.
  • A continuación se cambia de implementación y se obtiene otro producto.
  • Como puedes observar, la clase abstracta ElaborarAlimento (de tipo Abstraccion) aparte de definir un método que deberán implementar las que hereden de ella contiene una referencia a otra de tipo Implementador.
  • En dicho método las clases hijas acceden al implementador (que puede ser cambiado en tiempo de ejecución), llamando al método definido en la interface IElaborar.

Vamos ahora con otro ejemplo en el que simulamos una cadena de elaboración y envasado de alimentos.

Main.java (Cliente según el diagrama anterior):

package estructurales.bridge.bridge02;

public class Main
{
    public static void main(String[] args)
    {
        // Crear un objeto de tipo 'AbstraccionRefinada' indicándole un 'ImplementadorConcreto'
        ElaborarPasta ravioli = new ElaborarRavioli( new Cocinar() );

        // Realizar el proceso de que se trate (cocinar)
        ravioli.obtener();

        // Cambiar de implementador y realizar el proceso (envasar)
        ravioli.setImplementador( new Envasar() );
        ravioli.obtener();

        System.out.println("-------------");

        ElaborarPasta lasagna = new ElaborarLasagna( new Cocinar() );
        lasagna.obtener();

        lasagna.setImplementador( new Envasar() );
        lasagna.obtener();
    }
}

ElaborarPasta.java (Abstraccion según el diagrama anterior):

package estructurales.bridge.bridge02;

public abstract class ElaborarPasta
{
    // Referencia al Implementador
    IElaborar implementador;

    String nombre;

    // --------------------------

    public IElaborar getImplementador()
    {
        return this.implementador;
    }

    // --------------------------

    public void setImplementador( IElaborar implementador )
    {
        this.implementador = implementador;
    }

    // --------------------------

    // Método a implementar por las clases que hereden
    public abstract void obtener();
}

ElaborarRavioli.java (una AbstraccionRefinada según el diagrama anterior):

package estructurales.bridge.bridge02;

public class ElaborarRavioli extends ElaborarPasta
{
    public ElaborarRavioli( IElaborar implementador )
    {
        this.setImplementador( implementador );
    }

    // ------------------------

    @Override
    public void obtener()
    {
        System.out.println("Preparando raviolis...");

        this.getImplementador().procesar();
    }
}

ElaborarLasagna.java (una AbstraccionRefinada según el diagrama anterior):

package estructurales.bridge.bridge02;

public class ElaborarLasagna extends ElaborarPasta
{
    public ElaborarLasagna( IElaborar implementador )
    {
        this.setImplementador( implementador );
    }

    // ------------------------

    @Override
    public void obtener()
    {
        System.out.println("Preparando lasagna...");

        this.getImplementador().procesar();
    }
}

IElaborar.java (Implementador según el diagrama anterior):

package estructurales.bridge.bridge02;

public interface IElaborar
{
    public void procesar();
}

Cocinar.java (un ImplementadorConcreto según el diagrama anterior):

package estructurales.bridge.bridge02;

public class Cocinar implements IElaborar
{
    public Cocinar() {
    }

    // --------------------------

    @Override
    public void procesar()
    {
        // Operaciones necesarias
        // ...

        System.out.println("\tAlimento cocinado");
    }
}

Envasar.java (un ImplementadorConcreto según el diagrama anterior):

package estructurales.bridge.bridge02;

public class Envasar implements IElaborar
{
    public Envasar() {
    }

    // --------------------------

    @Override
    public void procesar()
    {
        // Operaciones necesarias
        // ...

        System.out.println("\tAlimento envasado");
    }
}

Al ejecutarlo obtendríamos como resultado:

Ejemplo

EXPLICACIÓN:

  • En este caso existen dos implementaciones comunes (Cocinar y Elaborar) que se utilizarán tanto en ElaborarLasagna como en ElaborarRavioli (el proceso a seguir es diferente en cada caso).
  • En este punto te preguntarás: al tratarse de métodos comunes ¿por qué no implementarlos directamente en cada clase?. En este caso ciertamente no es necesario hacerlo de este modo, pero en ciertos casos al dividir así un objeto en dos partes evitamos que se produzcan determinadas jerarquías de clases que al extenderse (agregando una nueva clase, por ejemplo) perderían su lógica y no resultarían mantenibles (al crearse demasiada interdependencia entre ellas).
Primera página Anterior Siguiente Última página
Usamos cookies para ofrecerte una experiencia mejorada, el continuar navegando supone que aceptas su uso