17 - Bridge
Tags: flutter, dart, oop, programming
Published: 2021-05-10 12:37:29

In the last article, I have analysed a structural design pattern that provides a way of changing the skin of an object without changing its guts — Decorator. In this article, I would like to analyse and implement another structural design pattern that tends to be relatively difficult to understand compared to the other design patterns, but at the same time is practical and useful — it is Bridge.

## Table of Contents

* What is the Bridge design pattern?
* Analysis
* Implementation
* Your Contribution

## What is the Bridge design pattern?

![Dog Jumping Over A Bridge](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9sdbdensbx2pn9b4hwu4.jpeg)

**Bridge**, also known as **Handle/Body**, belongs to the category of structural design patterns. The intention of this design pattern is described in the [GoF book](https://en.wikipedia.org/wiki/Design_Patterns):

> Decouple an abstraction from its implementation so that the two can vary independently.

The usual way for an abstraction to have one of several possible implementations is to use inheritance — an abstraction defines the interface while concrete subclasses implement it in different ways. However, this approach is not very flexible since it binds the implementation to abstraction at compile-time and makes it impossible to change the implementation at run-time. What if we want the implementation to be selected and exchanged at run-time?

The Bridge design pattern separates an abstraction from its implementation so that the two can vary independently from each other. In this case, the abstraction uses another abstraction as its implementation instead of using the implementation directly. This relationship between an abstraction and its implementation (well, another abstraction, to be more specific) is called a *bridge — it bridges the abstraction and its implementation, letting them vary independently*.

If the *Abstraction* and *Implementation* terms sound too academic to you, imagine this: abstraction (or interface) is just a high-level layer for some particular entity. This layer is just an interface that is not supposed to do any real work on its own — it should delegate the work to the implementation layer. A good example of this is a GUI (graphical user interface) and OS (operating system). GUI is just a top-level layer for the user to communicate with the operating system, but it does not do any real work by itself — it just passes user commands (events) to the platform. And what is important about this, both GUI and OS could be extended separately from each other, e.g. a desktop application could have different views/panels/dashboards and at the same time support several APIs (could be run on Windows, Linux and macOS) — these two parts could vary independently. Sounds like a Bridge design pattern, right?

## Analysis

The general structure of the Bridge design pattern looks like this:

![Bridge Class Diagram](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/l7i3azdqkfri2vesba6u.png)

* *Abstraction* — defines an interface for the abstraction and maintains a reference to an object of type *Implementation*;
* *Refined abstraction* — implements the *Abstraction* interface and provides different variants of control logic;
* *Implementation* — defines an interface for the implementation classes. An *Abstraction* can only communicate with an *Implementation* object via methods that are declared there;
* *Concrete implementations* — implement the *Implementation* interface and contain platform-specific code.

### Applicability

The Bridge design pattern should be used when you want to divide a monolithic class with several functionality variants. In this case, the pattern allows splitting the class into several class hierarchies which could be changed independently — it simplifies code maintenance, smaller classes minimizes the risk of breaking existing code. A good example of this approach is when you want to use several different approaches in the persistence layer e.g. both database and file system persistence.

The bridge design pattern should also be used when both the abstractions and their implementations should be extensible by subclassing — the pattern allows combining different abstractions and implementation and extending them independently.

Finally, the bridge design pattern is a lifesaver when you need to be able to switch implementations at run-time. The pattern lets you replace the implementation object inside the abstraction — you can inject it via the constructor or just assign it as a new value to a field/property.

## Implementation

![Let's Get To Work](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/j74tn1j5byunyxig11pt.gif)

We will implement the persistence layer for our example using the Bridge design pattern for the implementation part.

Let’s say your application uses the external SQL database (not the local SQLite option in your device, but the cloud one). Everything is fine until the wild connection problems appear. In this case, there are two options: you are not allowing users to use the application and provide a funny *connection lost* screen or you can store the data in some kind of local storage and synchronise the data later when the connection is up again. Obviously, the second approach is more user friendly, but how to implement it?

In the persistence layer, there are multiple repositories for each entity type. The repositories share a common interface — that is our abstraction. If you want to change the storage type (to use the local or cloud one) at run-time, these repositories could not reference the specific implementation of the storage, they should use some kind of abstraction shared between different types of storages. Well, we can build another abstraction (interface) on top of that which is then implemented by the specific storages. Now we connect our repositories’ abstraction with the storages’ interface — *voilà*, that is how the Bridge design pattern is introduced into our application! Let’s check the class diagram first and then investigate some implementation details.

### Class diagram

The class diagram below shows the implementation of the Bridge design pattern:

![Bridge Implementation Class Diagram](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/fab7658k8xok7xkmv1ti.png)

The *EntityBase* is an abstract class that is used as a base class for all the entity classes. The class contains an id property and a named constructor *EntityBase.fromJson* to map the JSON object to the class field.

*Customer* and *Order* are concrete entities that extend the abstract class *EntityBase*. *Customer* class contains *name* and *email* properties, *Customer.fromJson* named constructor to map the JSON object to class fields and a *toJson()* method to map class fields to the corresponding JSON map object. *Order* class contain *dishes* (a list of dishes of that order) and *total* fields, a named constructor *Order.fromJson* and a *toJson()* method respectively.

*IRepository* is an abstract class that is used as an interface for the repositories:

* *getAll()* — returns all records from the repository;
* *save()* — saves an entity of type *EntityBase* in the repository.

*CustomersRepository* and *OrdersRepository* are concrete repository classes that extend the abstract class *IRepository* and implement its abstract methods. Also, these classes contain a storage property of type *IStorage* which is injected into the repository via the constructor.

*IStorage* is an abstract class that is used as an interface for the storages:

* *getTitle()* — returns the title of the storage. The method is used in UI;
* *fetchAll<T>()* — returns all the records of type *T* from the storage;
* *store<T>()* — stores a record of type *T* in the storage.

*FileStorage* and *SqlStorage* are concrete storage classes that extend the abstract class *IStorage* and implement its abstract methods. Additionally, *FileStorage* class uses the *JsonHelper* class and its static methods to serialise/deserialise JSON objects.

*BridgeExample* initialises and contains both — customer and order — repositories which are used to retrieve the corresponding data. Additionally, the storage type of these repositories could be changed between the *FileStorage* and *SqlStorage* separately and at the run-time.

### EntityBase

An abstract class that stores the *id* field and is extended by all of the entity classes.

![entity_base.dart](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8ctithg6qsvy4crpji5x.png)

### Customer

A simple class to store information about the customer: its *name* and *email*. Also, the constructor generates random values when initialising the *Customer* object.

![customer.dart](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/u9dfjz15h1obza39kox3.png)

### Order

A simple class to store information about the order: a list of *dishes* it contains and the *total* price of the order. Also, the constructor generates random values when initialising the *Order* object.

![order.dart](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/yholfukqsrlthmvc2er2.png)

### JsonHelper

A helper classes used by the *FileStorage* to serialise objects of type *EntityBase* to JSON map objects and deserialise them from the JSON string.

![json_helper.dart](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/mc2ef2h5twh1e82zznsx.png)

### IRepository

An interface that defines methods to be implemented by the derived repository classes. Dart language does not support the interface as a class type, so we define an interface by creating an abstract class and providing a method header (name, return type, parameters) without the default implementation.

![irepository.dart](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/24snqdxdl9n3mijvwrub.png)

### Concrete repositories

* *CustomersRepository* — a specific implementation of the *IRepository* interface to store customers’ data.

![customers_repository.dart](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7tq5dw5ghrwf1rdh849d.png)

* *OrdersRepository* — a specific implementation of the *IRepository* interface to store orders’ data.

![orders_repository.dart](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/tujw1admp4h9044vuvcb.png)

### IStorage

An interface that defines methods to be implemented by the derived storage classes.

![istorage.dart](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ji47bf176e5ngbevbppd.png)

### Concrete storages

* *FileStorage* — a specific implementation of the *IStorage* interface to store an object in the storage as a file — this behaviour is mocked by storing an object as a JSON string.

![file_storage.dart](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0xys6hfc5w7wjmgekqj3.png)

* *SqlStorage* — a specific implementation of the *IStorage* interface to store an object in the storage as an entity — this behaviour is mocked by using the Map data structure and appending entities of the same type to the list.

![sql_storage.dart](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/11yrsr8fjzdk3nquvqnn.png)

### Example

First of all, a markdown file is prepared and provided as a pattern’s description:

![Bridge Markdown](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/5s0zo5wv24ki2byplvpg.gif)

*BridgeExample* contains a list of storages — instances of *SqlStorage* and *FileStorage* classes. Also, it initialises *Customer* and *Order* repositories. In the repositories the concrete type of storage could be interchanged by triggering the *onSelectedCustomerStorageIndexChanged()* for the *CustomersRepository* and *onSelectedOrderStorageIndexChanged()* for the *OrdersRepository* respectively.

![bridge_example.dart](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/p33o4g2z878df1sf2ovq.png)

The concrete repository does not care about the specific type of storage it uses as long as the storage implements the *IStorage* interface and all of its abstract methods. As a result, the abstraction (repository) is separated from the implementor (storage) — the concrete implementation of the storage could be changed for the repository at run-time, the repository does not depend on its implementation details.

![Bridge Example](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/qrdp40thnbjlea4addr1.gif)

As you can see in the example, the storage type could be changed for each repository separately and at run-time — it would not be possible by using the simple class inheritance approach.

All of the code changes for the Bridge design pattern and its example implementation could be found [here](https://github.com/MangirdasKazlauskas/flutter-design-patterns/pull/18).

## Your Contribution

💖 or 🦄 this article to show your support and motivate me to write better!
💬 Leave a response to this article by providing your insights, comments or wishes for the next topic.
📢 Share this article with your friends, colleagues on social media.
➕ Follow me on dev.to or any other social media platform.
⭐ Star the [Github](https://github.com/mkobuolys/flutter-design-patterns) repository.