Design patterns are proven solutions to software design problems. They help improve code quality, promote reusability, and increase maintainability. We use them to save time and produce quality, extensible and flexible code. In this article, we are going to introduce the Bridge design pattern.
The Bridge pattern is a structural design pattern that separates abstractions from implementations so that they can be independent. This pattern allows for greater flexibility and interchangeability between different implementations of an abstraction.
In this pattern, there are two distinct hierarchies: an abstraction hierarchy and an implementation hierarchy. The interface abstraction hierarchy defines the top level that client code uses to interact with the abstraction, while the implementation hierarchy provides the concrete entities of the same abstraction.
The Bridge pattern is useful when there are multiple implementations of the same abstraction and we want to change them at runtime without affecting the client code. By separating the abstraction and implementation hierarchies, the Bridge pattern helps reduce dependencies between components and making it easier to maintain and modify both hierarchies independently.
Below is an example of how to implement the Bridge pattern in C#:
// Abstraction
public abstract class Shape
{
protected IRenderer renderer;
public Shape(IRenderer renderer)
{
this.renderer = renderer;
}
public abstract void Draw();
}
public class Circle : Shape
{
private float radius;
public Circle(IRenderer renderer, float radius) : base(renderer)
{
this.radius = radius;
}
public override void Draw()
{
renderer.RenderCircle(radius);
}
}
public class Square : Shape
{
private float side;
public Square(IRenderer renderer, float side) : base(renderer)
{
this.side = side;
}
public override void Draw()
{
renderer.RenderSquare(side);
}
}
// Implementation
public interface IRenderer
{
void RenderCircle(float radius);
void RenderSquare(float side);
}
// Concrete Implementation
public class VectorRenderer : IRenderer
{
public void RenderCircle(float radius)
{
Console.WriteLine($"Drawing a circle of radius {radius} using vector graphics.");
}
public void RenderSquare(float side)
{
Console.WriteLine($"Drawing a square of side {side} using vector graphics.");
}
}
// Concrete Implementation
public class RasterRenderer : IRenderer
{
public void RenderCircle(float radius)
{
Console.WriteLine($"Drawing a circle of radius {radius} using raster graphics.");
}
public void RenderSquare(float side)
{
Console.WriteLine($"Drawing a square of side {side} using raster graphics.");
}
}
// Client code
var vectorCircle = new Circle(new VectorRenderer(), 5);
vectorCircle.Draw(); // Output: Drawing a circle of radius 5 using vector graphics.
var rasterSquare = new Square(new RasterRenderer(), 10);
rasterSquare.Draw(); // Output: Drawing a square of side 10 using raster graphics.
In this example, the Shape class is abstract and has Circle and Square derivatives. The IRenderer interface is the implementation part and has two main implementations, VectorRenderer and RasterRenderer. The Shape class takes an IRenderer object in its constructor and uses it to execute the shape drawing command in its respective implementations. Client code can create different forms with different implementations at runtime, without requiring code changes.
To read about other design patterns, you can use the list below. There is also a code repository on GitHub that includes all the design patterns.
I am Reza Babakhani, a software developer. Here I write my experiences, opinions and suggestions about technology. I hope that what I write is useful for you.
leave a comment