Cidenet+Blog+Clean Dev Practices+Bridge

Bridge

What is it? What problem does it solve?

Bridge is a structural design pattern that allows a large class or a group of closely related classes, to be divided into two separate hierarchies (abstraction and implementation) that can be developed independently of each other.

This pattern is useful when:

  • You want to avoid a permanent coupling between an abstraction and its implementation. For example, in the case where the implementation needs to be changed at runtime.
  • Both abstractions and their implementations should be extensible through inheritance. In this case, this pattern allows combining the different abstractions and their implementations and extending them independently.
  • Changes to the implementation of an abstraction should have no impact on clients.

Diagram

Diagrama Bridge

 

  • Abstraction: 
            – Defines the interface of the abstraction.
            – Holds a reference to an object of type Implementer.
  • RefinedAbstraction: Implements the interface defined by Abstraction.
  • Implementer: Defines the interface for implementation classes; this interface does not have to correspond exactly to the Abstraction interface. Usually, this interface provides only primitive operations and Abstraction defines high-level operations based on the primitive ones.
  • ConcreteImplementer: Implements the Implementer interface and defines a concrete implementation.

Example

Suppose you want to build an application that needs to save files both locally and to S3. It should be noted that the application generates different types of files, such as PDF and Excel files. Although files are always generated, their construction may be different. For example, to build an Excel file you can use libraries like Apache Poi, while to build a PDF, you can use Jasper libraries. However, if the creation process has parts in common, the Template Method pattern can be used which is not the focus of this newsletter but will be reviewed in the future.

Using the Bridge pattern, in this case, you can separate the generation of a file from its storage.

For this case, Abstraction would be a class that specifies that it needs to store a file, but does not say how. This class has a reference to Implementer. Through this, a bridge is established between creation and storage, the Implementer is called in Abstraction to create a file and then the children are delegated where to store this file.

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

Then a deployment is created to store a file locally and another to do its own on AWS. These two classes would be 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());
         }
}

Subsequently, the Implementer interface is created, which sets the methods for generating a file.

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

Next, the classes that implement this interface (ConcreteImplementor) are created. One to create an Excel file and one to generate a PDF file.

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

In this way, you can create an Excel file and store it locally or create a PDF and store it in S3. In general, any combination of storage-creation is possible.

@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")
         );
 }

With this pattern, if you add a new storage system such as Firestore, you only have to create a new implementation for this system without having to modify the existing ones.

You also can create a new type of file, for example, a text file, and store it on existing storage systems without the need to make modifications to these systems.

Note that to instantiate these implementations, you can make use of patterns such as Factory and Singleton.

The complete example of this example and an additional example can be found at https://bit.ly/3i4xtFC

Conclusions

  • Bridge, unlike Adapter, is used at design time, to allow abstractions and implementations to vary independently.
  • By decoupling an interface from its implementation, it is achieved that the abstraction implementation can be configured at run time. You can even change the implementation of an object at run time.
  • Both the abstraction and the implementation can be extended independently.
  • Implementation details can be hidden from the client code.

 

Sources

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

Contact us

Allow us to contact you

    Medellín - Colombia

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

    United States

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