Cidenet+Blog+Desarrollo limpio+Bridge

Bridge

¿Qué es?, ¿Qué problema resuelve?

Bridge es un patrón de diseño estructural que te permite dividir una clase grande, o un grupo de clases estrechamente relacionadas, en dos jerarquías separadas (abstracción e implementación) que pueden desarrollarse independientemente la una de la otra.

Este patrón es útil cuando:

 

  • Se quiere evitar un acoplamiento permanente entre una abstracción y su implementación. Por ejemplo, en el caso donde la implementación debe cambiarse en tiempo de ejecución.
  • Tanto las abstracciones y sus implementaciones deberían ser extensibles por medio de herencia. En este caso, este patrón permite combinar las diferentes abstracciones y sus implementaciones, y extenderlas independientemente.
  • Los cambios en la implementación de una abstracción no deberían tener impacto sobre los clientes.

Diagrama

Diagrama Bridge

 

  • Abstraction: 
            – Define la interfaz de la abstracción.
            – Mantiene una referencia a un objeto de tipo Implementor.
  • RefinedAbstraction: Implementa la interfaz definida por Abstraction.
  • Implementor: Define la interfaz para clases de implementación; esta interfaz no tiene que corresponder exactamente a la interfaz de Abstraction. Usualmente esta interfaz provee solo operaciones primitivas y Abstraction define operaciones de alto nivel basadas en las primitivas.
  • ConcreteImplementor: Implementa la interfaz Implementor y define una implementación concreta.

Ejemplo

Suponga que se desea construir una aplicación que necesite guardar archivos tanto localmente  como en S3. Se debe tener en cuenta que la aplicación genera diferentes tipos de archivos, tales como PDF y archivos de Excel. A pesar de que siempre se generan archivos, su construcción puede ser diferente. Por ejemplo, para construir un Excel se pueden usar librerías como Apache Poi, mientras que para construir un PDF, se pueden usar librerías de Jasper. Sin embargo, si el proceso de creación tiene partes en común, se puede utilizar el patrón Template Method que se revisará en un futuro boletín.

Al utilizar el patrón Bridge, en este caso, se separa la generación de un archivo de su forma de ser almacenado.

Para este caso Abstraction sería una clase que especifica que necesita almacenar un archivo, más no dice cómo. Esta clase tiene una referencia a Implementor. Por medio de este se establece un puente entre la creación y el almacenamiento, el Implementor es llamado en Abstraction para crear un archivo y luego se le delega a los hijos, el dónde almacenar este archivo.

public abstract class StoreService {
        private final FileCreator implementor;

        public StoreService(FileCreator implementor) {
                this.implementor = implementor;

        }

        public String store(String fileName) {
                File file = implementor.createFile(fileName);
                return storeImp(file);
        }
      

        protected abstract String storeImp(File file);

        public FileCreator getImplementor() {
                return implementor;
        }
}

Luego, se crea una implementación para almacenar un archivo localmente y otra para que haga lo propio en AWS. Estas dos clases serían RefinedAbstraction.

public class LocalStoreImpl extends StoreService {

        public LocalStoreImpl(FileCreator implementor) {
        super(implementor);
        }

        @Override
        protected String storeImp(File file) {
                //Acá iría la lógica como tal para almacenar el archivo localmente
                return String.format("%s Stored in file system", file.getName());
        }
}

 

public class AWSStoreImpl extends StoreService {

        public AWSStoreImpl(FileCreator implementor) {
                super(implementor);
        }

        @Override
         protected String storeImp(File file) {
                 //Acá iría la lógica como tal para almacenar el archivo en AWS
                 return String.format("%s stored in aws", file.getName());
         }
}

Posteriormente, se crea la interfaz Implementor, que establece los métodos para generar un archivo.

public interface FileCreator {
        File createFile(String fileName);
}

Seguidamente, se crean las clases que implementan esta interfaz (ConcreteImplementor). Una para crear un archivo de Excel y otra para generar un archivo PDF.

public class ExcelCreatorImplementor implements FileCreator {

        @Override
         public File createFile(String fileName) {
                 //Acá iría la lógica para crea un archivo de Excel, ir a base
                 // de datos a buscar los datos para el archivo, etc
                 return new File(String.format("Excel: %s", fileName));
           }
}
public class PDFCreatorImplementor implements FileCreator {

        @Override
         public File createFile(String fileName) {
                 //Acá iría la lógica para crear un archivo PDF,
                 // ir a base de datos a buscar los datos para el archivo, etc
                 return new File(String.format("PDF: %s", fileName));
          }
}

De este modo, se puede crear un archivo de Excel y almacenarlo localmente o crear un PDF y almacenarlo en S3. En general, es posible cualquier combinación almacenamiento-creación.

@Test
 public void shouldPersistExcelLocally() {
        assertEquals(
                "Excel: File 1 Stored in file system",
                 new LocalStoreImpl(new ExcelCreatorImplementor()).store("File 1")
         );
 }

 @Test
 public void shouldPersistRemotely() {
         assertEquals(
                 "PDF: File 1 stored in aws",
                 new AWSStoreImpl(new PDFCreatorImplementor()).store("File 1")
         );
 }

Con este patrón, si se agrega un nuevo sistema de almacenamiento como Firestore, solo se tiene que crear una implementación nueva para este sistema y no se tienen que modificar los ya existentes.

También se tiene la posibilidad de crear un nuevo tipo de archivo, por ejemplo, un archivo de texto y almacenarlo en los sistemas de almacenamiento existentes sin la necesidad de hacer modificaciones en estos sistemas.

Nótese que para instanciar estas implementaciones, se puede hacer uso de patrones como Factory y Singleton.

El ejemplo completo se encuentra en https://bit.ly/3i4xtFC

Conclusiones

  • Bridge, a diferencia de Adapter, es usado en tiempo de diseño, para permitir que las abstracciones y las implementaciones varíen independientemente.
  • Desacoplando una interfaz de su implementación, se logra que la implementación de la abstracción pueda ser configurada en tiempo de ejecución. Incluso se puede cambiar la implementación de un objeto en tiempo de ejecución.
  • Se puede extender tanto la abstracción como la implementación independientemente.
  • Se pueden ocultar detalles de implementación al código cliente.

Fuentes

https://refactoring.guru/es/design-patterns/bridge

 

Contáctanos

Déjanos tus datos

    Medellín - Colombia

  • Calle 47D #72-29
  • (+57) 4 3222567
  • comunicaciones@cidenet.com.co

    Estados Unidos

  • 1200 Colorado Blvd, Denver Colorado 80220
  • (+1) 7723619239
  • jceballos@cidenet.net
WhatsApp
>