Bridge - kgleong/software-engineering GitHub Wiki
Motivation
The Bridge design pattern is used to keep the API exposed to clients independent from the actual implementation.
This results in the following advantages:
- Since implementation is independent of the interface, different implementations can be swapped in and out without the client needing to know implementation details.
- Both the interface and implementation can be subclassed and maintain their own class hierarchies.
- Combining different interface and implementation subclasses is simple when they are independent. Coupling the interface to specific implementations causes the number of subclasses to explode when many combinations are required.
Code
public class Abstraction {
// An Implementor instance provides specialized implementation
// details to handle certain use cases.
protected Implementor mImplementor;
// Abstraction class constructors can also take in parameters
// that allow a factory to return the proper implementation.
public Abstraction(Implementor implementor) {
mImplementor = implementor;
}
public void performOperation() {
mImplementor.performOperationImpl();
}
}
public class AbstractionA extends Abstraction {
public AbstractionA(Implementor implementor) {
super(implementor);
}
public void customOperationA() {
System.out.println("customOperationA() method called");
performOperation();
}
}
The Abstraction
class defines common functionality that all subclasses may utilize.
The implementation of this common functionality, however, is delegated to an Implementor
instance.
Different environments, or operating systems may dictate different implementations, and these differences can be captured by separate Implementor
classes.
These Implementor
classes can then be used interchangeably with the same Abstraction
subclass without the client needing to know which implementation is being provided.
Abstraction
subclasses can then define their own methods that utilize superclass methods that pass calls through to the associated Implementor
objects. The subclasses can also directly access the Implementor
objects themselves if necessary.
public interface Implementor {
void performOperationImpl();
}
public class ConcreteImplementor implements Implementor {
public void performOperationImpl() {
System.out.println("Concrete Implementor performing work.");
}
}
Implementor
classes provide the actual logic for a particular scenario. Normally, if only one implementation approach is supplied, the Bridge pattern is overkill.
The Bridge pattern shines when multiple implementations and abstractions are possible, and there is a need to create many combinations of the two.
// Client usage
Implementor implementor = new ConcreteImplementor();
AbstractionA abstractionA = new AbstractionA(implementor);
abstractionA.customOperationA();
// Output:
// customOperationA() method called.
// Concrete Implementor performing work.
Use Case
Many user interfaces contain dialog box widgets. Dialog boxes are windows that interrupt the normal user experience in order to get input or notify the user.
The example below demonstrates how to use the Bridge pattern to decouple different types of dialog boxes from the styles used to render them.
public class Dialog {
protected DialogImpl mDialogImpl;
public Dialog(DialogImpl dialogImpl) {
mDialogImpl = dialogImpl;
}
public void drawLine(Point start, Point end) {
mDialogImpl.drawLine(start, end);
}
}
This is an oversimplified Dialog
class, which relies on a wrapped implementation to provide the ability to render lines to the screen.
Dialog
subclasses will rely on the drawLine()
method to construct their visual components.
// A ChooserDialog requires the user to choose between
// multiple buttons contained within the dialog box.
public class ChooserDialog extends Dialog {
public ChooserDialog(DialogImpl dialogImpl) {
super(dialogImpl);
}
public void draw() {
drawButtons();
}
void drawButtons() {
System.out.println("Drawing chooser buttons.");
// Render buttons by calling drawLine() multiple times
// with the correct start and end points.
drawLine(..., ...);
...
}
}
// An AlertDialog contains a close button that dismisses the
// dialog.
public class AlertDialog extends Dialog {
public AlertDialog(DialogImpl dialogImpl) {
super(dialogImpl);
}
public void draw() {
drawCloseButton();
}
void drawCloseButton() {
System.out.println("Drawing close button.");
// Render close button by calling drawLine() multiple times
drawLine(..., ...);
...
}
}
ChooserDialog
and AlertDialog
both utilize the drawLine()
method in the Dialog
superclass to build the visual representation of themselves.
public interface DialogImpl {
void drawLine(Point start, Point end);
}
public class SolidDialogImpl implements DialogImpl {
public drawLine(Point start, Point end) {
System.out.println("Drawing a solid line.");
}
}
public class DashedDialogImpl implements DialogImpl {
public drawLine(Point start, Point end) {
System.out.println("Drawing a dashed line.");
}
}
The SolidDialogImpl
and DashedDialogImpl
subclasses offer two line type options when rendering dialog boxes.
This is a simple example, but decoupling the actual implementation for rendering a line to the screen can be useful when dealing with different graphics packages or operating systems that require different approaches to displaying UI elements.
// Client usage
DialogImpl solidDialogImpl = new SolidDialogImpl();
ChooserDialog chooserDialog = new ChooserDialog(solidDialogImpl);
chooserDialog.draw();
// Output
// Drawing chooser buttons.
// Drawing a solid line.
// Drawing a solid line.
// Drawing a solid line.
// ...
Creating a ChooserDialog
using solid or dashed lines in this manner is simple when the dialog type and line style are decoupled in this fashion.