Design patterns are proven solutions for common problems that occur during software design. They provide a description or a template for how to solve such problems. The Gang of Four (GoF) design patterns are considered to be the foundation of all other patterns.
Bridge pattern is a structural pattern that helps us separate the abstraction from its implementation. This pattern has multiple use cases:
- Avoid binding between abstraction and its implementation
- Extend abstraction and implementation with subclasses
- Avoid implementation affecting the clients
Advantages
- Focus on high level logic and hide implementation details
- Introduce new abstractions and implementations independently from each other
- Separate implementation from the interface
Disadvantages
- Potential increase in code complexity
- Double indirection – request is passed from the Abstraction to the Concrete Implementor
Elements of Bridge pattern
- Abstraction
- core of the pattern
- provides high-level control logic
- relies on the implementor to do actual low-level work.
- Implementor
- declares interface for all concrete implementors.
- Refined Abstractions
- provides variants of control logic
- works with concrete Implementors via Implementor.
- Concrete Implementors
- implements the Implementor with concrete implementation.
Problem
Let’s consider the example of views and data resources that should be displayed. Views could be SimpleView and DetailView and resources could be Course, Book, etc.
To represent all the resources in all of the views, a class for each combination of view and resource type would need to be created. In the previously mentioned example those combinations would be CourseSimpleView, CourseDetailView, BookSimpleView and BookDetailView.
This may look normal, but the problem occurs when more resources or views are added. Addition of just one resource and one view causes class count increase from 4 to 9. This is an example where the Bridge pattern can be used to reduce the total number of classes.
Solution
When using the Bridge pattern, adding a new resource or view would require only one additional class. Bridge pattern can be demonstrated using the previously mentioned example. For simplicity, attributes and methods that are not needed for this example are not shown. Some method implementations are simplified, so that focus can be on the concept of this pattern.
Each type of resource can be abstracted with Resource. On the Bridge pattern diagram this element represents Implementor.
Resources are Concrete implementors. In the mentioned example, resources are Course and Book.
public interface Resource { String getName(); String getDescription(); } class Book implements Resource { private String author; private String name; private String coverText; @Override public String getName() { return name + “ written by ” + author; } @Override public String getDescription() { return coverText; } } class Course implements Resource { private String name; private String content; @Override public String getName() { return name; } @Override public String getDescription() { return content; } }
Abstraction of all views can be defined as View. Out of all elements from the Bridge pattern diagram, View represents the Abstraction.
Lastly, SimpleView and DetailView are Refined abstractions. For the example below keep in mind that method implementations are a simplification.
public abstract class View { private Resource resource; public View(Resource resource) { this.resource = resource; } public abstract String show(); } class SimpleView extends View { public SimpleView(Resource resource) { super(resource); } @Override public String show() { return resource.getName(); } } class DetailView extends View { public DetailView(Resource resource) { super(resource); } @Override public String show() { return resource.getName() + resource.getDescription(); } }
Now, different views for different resources can be created.
Resource resource = new Book(); View view = new DetailView(resource); view.show(); // do something with returned value
Using the Bridge design pattern, it is possible to add as many views and as many resources as necessary without having to create multiple classes. For each new view/resource only one additional class needs to be added. New view class would just require extension of View and new resource class would just require implementation of interface Resource.
View and Resource are now separated and can vary independently.
Complex real-world example
A slightly complicated real-world example could be the problem of storing resources in different databases or file systems or even different locations on the file system.
This example consists of:
- Repository as the Abstraction
- Storage as the Implementor
- UserRepository and OrderRepository as Refined Abstractions (Refined Abstractions can be any repository that saves the data on any storage)
- FileStorage, PostgreSQLStorage and CacheStorage as Concrete Implementors (Concrete Implementor can be any store type used by a system to store its resources)
- Resource, User and Order (User and Order are implementations of Resource)
Resources
Resources are introduced for a better understanding of the example.
public interface Resource { // Can be abstract class or interface, depending on what is needed } class User implements Resource { private final String name; private final int age; // Constructor, getters, etc. } class Order implements Resource { private final String orderNumber; private final String address; // Constructor, getters, etc. }
Storages
Storages represent Implementor and Concrete Implementors.
public interface Storage { void store(Resource resource); } class FileStorage implements Storage { @Override public void store(final Resource resource) { // Store resource to a file on file system } } class PostgreSQLStorage implements Storage { @Override public void store(final Resource resource) { // Store resource to PostgreSQL Database } } class CacheStorage implements Storage { @Override public void store(final Resource resource) { // Store resource to cache } }
Repositories
As mentioned above, repositories are Abstraction and Refined Abstractions.
public abstract class Repository<T extends Resource> { final Storage storage; protected Repository(final Storage storage) { this.storage = storage; } public abstract void save(final T resource); } class UserRepository extends Repository<User> { protected UserRepository(final Storage storage) { super(storage); } @Override public void save(final User user) { // Some user-specific logic storage.store(user); } } class OrderRepository extends Repository<Order> { protected OrderRepository(final Storage storage) { super(storage); } @Override public void save(final Order order) { // Some order-specific logic storage.store(order); } }
Application
With Bridge design pattern introduction various resources can be stored in different storage types. For example, to store a User in the PostgreSQL database, the code below is needed.
User user = new User("John Doe", 40); Storage storage = new PostgreSQLStorage(); Repository repository = new UserRepository(storage); repository.save(user);
This concludes the real-world example of Bridge design pattern use and the benefits that are introduced with this pattern.
“Bridge pattern in Java” Tech Bite was brought to you by Dragan Janković, Software Engineer at Atlantbh.
Tech Bites are tips, tricks, snippets or explanations about various programming technologies and paradigms, which can help engineers with their everyday job.