The Bridge pattern is a structural design pattern that decouples an abstraction from its implementation, allowing them to vary independently. On the other hand, multiple inheritance allows a class to inherit from multiple parent classes, where the child class inherits the behavior and characteristics of all parent classes.
There are certain scenarios where using the Bridge pattern would be more advantageous than using multiple inheritance. One such scenario is when we have a hierarchy of abstractions and multiple implementations, and we want to be able to vary them independently.
Suppose, we have a drawing application that can draw different shapes like Circle, Rectangle, Triangle, etc. The application supports two types of rendering - on screen and in a file. We could have a hierarchy of shapes and a hierarchy of renderers, resulting in 4 possible shape/renderer combinations.
With the Bridge pattern, we can create an abstraction for the shape hierarchy and another abstraction for the renderer hierarchy, and then bridge them. The bridge interface will then be responsible for drawing the shape in the specified renderer. The advantage of this approach is that we can add new shapes and renderers without affecting existing code.
On the other hand, if we were to use multiple inheritance, we would have to create multiple classes for each shape/renderer combination, resulting in a lot of duplication of code. This approach becomes unmanageable for larger hierarchies and combinations.
Here is an example demonstrating the use of the Bridge pattern in a drawing application:
// Abstraction for shape
abstract class Shape {
protected Renderer renderer;
public Shape(Renderer renderer) {
this.renderer = renderer;
}
public abstract void draw();
}
// Concrete implementation of shape
class Circle extends Shape {
public Circle(Renderer renderer) {
super(renderer);
}
@Override
public void draw() {
renderer.renderCircle();
}
}
// Abstraction for renderer
interface Renderer {
void renderCircle();
void renderRectangle();
void renderTriangle();
}
// Concrete implementation of renderer for screen
class ScreenRenderer implements Renderer {
@Override
public void renderCircle() {
// code to render circle on screen
}
@Override
public void renderRectangle() {
// code to render rectangle on screen
}
@Override
public void renderTriangle() {
// code to render triangle on screen
}
}
// Concrete implementation of renderer for file
class FileRenderer implements Renderer {
@Override
public void renderCircle() {
// code to render circle in file
}
@Override
public void renderRectangle() {
// code to render rectangle in file
}
@Override
public void renderTriangle() {
// code to render triangle in file
}
}
// Client code
public class DrawingClient {
public static void main(String[] args) {
Renderer screenRenderer = new ScreenRenderer();
Renderer fileRenderer = new FileRenderer();
Shape circle = new Circle(screenRenderer);
Shape rectangle = new Rectangle(fileRenderer);
circle.draw();
rectangle.draw();
}
}
In the above example, the Shape abstraction is bridged with the Renderer interface to draw the shapes in different renderers, i.e., screen and file. This approach is more flexible and scalable, as we can add new shapes and renderers without affecting existing code.