Implementation

For the implementation part, we will implement the persistence layer for our example using the Bridge design pattern.

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:

Class Diagram — Implementation of the Bridge design pattern

The EntityBase is an abstract class which 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 which 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 which 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 which 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 which 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 which 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 which stores the id field and is extended by all of the entity classes.

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.

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.

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.

IRepository

An interface which 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.

Concrete repositories

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

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

IStorage

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

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.

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.

Example

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

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.

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.

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.