1. Definition
The Bridge Design Pattern decouples an abstraction from its implementation, enabling the two to vary independently. This pattern involves splitting large classes or closely related sets of classes into two separate hierarchies — abstraction and implementation — which can be developed independently of each other.
2. Problem Statement
Let’s assume you have a hierarchy of classes representing graphical shapes like Circle, Rectangle, and Triangle. If you need to support multiple rendering engines for drawing these shapes (e.g., OpenGL, DirectX), you might end up with a combinatorial explosion of subclasses like OpenGLCircle, DirectXCircle, OpenGLRectangle, and so on.
3. Solution
The Bridge pattern suggests that you divide the monolithic class (or a set of closely related classes) into two separate hierarchies: one for the abstraction, and the other for the implementation. In the given scenario, you can have a separate hierarchy for rendering engines and shapes. The Shape class would have a reference to one of the rendering engine objects. This reference acts as a bridge between the Shape and Renderer hierarchies.
4. Real-World Use Cases
1. Cross-platform windowing systems where each OS provides different windowing operations.
2. Device drivers where operations might vary based on the device type.
3. Drawing and rendering toolkits with support for multiple backend renderers.
5. Implementation Steps
1. Identify the orthogonal dimensions in your classes. In our case, it's Shapes and Renderers.
2. Split the monolithic class into separate class hierarchies.
3. Create a bridge interface that captures the relationship between the abstraction and its implementation.
4. Implement concrete classes that follow the bridge interface.
5. Modify the high-level abstraction to use the bridge interface.
6. Implementation in Kotlin
// Step 3: Bridge Interface (Renderer)
interface Renderer {
fun renderShape(name: String)
}
// Step 4: Concrete Implementations
class OpenGLRenderer : Renderer {
override fun renderShape(name: String) {
println("Rendering $name using OpenGL")
}
}
class DirectXRenderer : Renderer {
override fun renderShape(name: String) {
println("Rendering $name using DirectX")
}
}
// High-Level Abstraction
abstract class Shape(private val renderer: Renderer) {
abstract val name: String
fun draw() {
renderer.renderShape(name)
}
}
class Circle(renderer: Renderer) : Shape(renderer) {
override val name = "Circle"
}
class Rectangle(renderer: Renderer) : Shape(renderer) {
override val name = "Rectangle"
}
fun main() {
val opengl = OpenGLRenderer()
val directx = DirectXRenderer()
val circle = Circle(opengl)
circle.draw()
val rectangle = Rectangle(directx)
rectangle.draw()
}
Output:
Rendering Circle using OpenGL Rendering Rectangle using DirectX
Explanation:
1. We first define a Renderer interface which acts as our bridge between the Shape hierarchy and the Renderer hierarchy.
2. Then, we provide concrete implementations for different renderers, i.e., OpenGLRenderer and DirectXRenderer.
3. Our high-level abstraction, the Shape class, captures the essence of various shapes and contains a reference to a renderer.
4. Concrete subclasses of Shape like Circle and Rectangle provide specific implementations.
5. In the main function, we demonstrate how to use this setup to render a circle using OpenGL and a rectangle using DirectX.
7. When to use?
The Bridge pattern should be applied when:
1. You want to avoid permanently binding an abstraction to its implementation.
2. Both the abstractions and their implementations should be independently extensible.
3. Changes in the implementation of an abstraction should not impact its clients.
4. You want to hide implementation details from the client.
By following the Bridge pattern, you can ensure that the complexity is distributed across multiple classes rather than being piled up in a monolithic class, leading to a cleaner and more maintainable codebase.
Comments
Post a Comment