A photo of a bridge at sunset

๐ŸŒ‰ Exploring the Bridge Design Pattern in Ruby


author

Author: David Martinez

Created: December 3, 2024 - Updated: December 4, 2024

Read Time: 15 min

Ruby

Hello there! Today, letโ€™s unravel the mysteries of the Bridge design pattern. ๐ŸŒ‰ We'll dive into its purpose, practical applications, and bring it to life with a Ruby implementation.

Understanding the Bridge Pattern ๐ŸŒŸ

The Bridge is a structural design pattern aimed at decoupling an abstraction from its implementation.

This pattern is especially useful when you need to combine different dimensions of variability or want to avoid a complex inheritance hierarchy.

When to Use the Bridge Pattern?

๐Ÿ”น Use the Bridge pattern when your code needs to work with multiple types of implementations without being tightly coupled to them.

๐Ÿ”น Itโ€™s ideal for scenarios where you anticipate future expansions of both abstractions and their implementations.

๐Ÿ”น Great for scenarios involving cross-platform development, where the same code must work with different systems.

Problem Statement

Imagine a scenario where we need to render various shapes (e.g., circles, rectangles) using different rendering APIs like OpenGL and DirectX.

A naive implementation would require creating separate classes for each combination of shape and rendering API, leading to a combinatorial explosion of classes as more shapes or APIs are introduced.

Naive Solution (Without Pattern)

In this example, we have separate classes for each shape and rendering API.

This approach leads to a large number of classes and tightly couples shapes with rendering APIs.

UML Diagram

In this example:

๐Ÿ”น OpenGLCircle, OpenGLRectangle, DirectXCircle, DirectXRectangle are concrete classes for rendering shapes using specific APIs.

๐Ÿ”น main creates instances of each class and calls the draw method to render the shapes.

class OpenGLCircle
def initialize(x, y, radius)
@x, @y, @radius = x, y, radius
end
def draw
puts "Rendering circle at (#{@x}, #{@y}) with radius #{@radius} using OpenGL"
end
end
class OpenGLRectangle
def initialize(x, y, width, height)
@x, @y, @width, @height = x, y, width, height
end
def draw
puts "Rendering rectangle at (#{@x}, #{@y}) with width #{@width} and height #{@height} using OpenGL"
end
end
class DirectXCircle
def initialize(x, y, radius)
@x, @y, @radius = x, y, radius
end
def draw
puts "Rendering circle at (#{@x}, #{@y}) with radius #{@radius} using DirectX"
end
end
class DirectXRectangle
def initialize(x, y, width, height)
@x, @y, @width, @height = x, y, width, height
end
def draw
puts "Rendering rectangle at (#{@x}, #{@y}) with width #{@width} and height #{@height} using DirectX"
end
end
# Usage
def main
opengl_circle = OpenGLCircle.new(10, 20, 15)
opengl_rectangle = OpenGLRectangle.new(5, 5, 30, 40)
directx_circle = DirectXCircle.new(10, 20, 15)
directx_rectangle = DirectXRectangle.new(5, 5, 30, 40)
opengl_circle.draw
opengl_rectangle.draw
directx_circle.draw
directx_rectangle.draw
end
main
# Run the example
# Output:
# Rendering circle at (10, 20) with radius 15 using OpenGL
# Rendering rectangle at (5, 5) with width 30 and height 40 using OpenGL
# Rendering circle at (10, 20) with radius 15 using DirectX
# Rendering rectangle at (5, 5) with width 30 and height 40 using DirectX

Why This Approach Is Not Scalable? ๐Ÿค”

1๏ธโƒฃ Class Explosion: For every new shape or rendering API, a new class must be created. For n shapes and m rendering APIs, you would need n * m classes.

2๏ธโƒฃ Difficult Maintenance: If a rendering API's behavior changes, multiple classes need to be updated.

3๏ธโƒฃ Limited Extensibility: Adding new shapes or rendering APIs requires significant modifications, violating the Open-Closed Principle.

4๏ธโƒฃ Redundant Code: Similar logic is repeated across classes, leading to poor code reuse.

Optimized Solution (Using Bridge Pattern) ๐ŸŒ‰

The Bridge pattern separates the abstraction (Shape) from the implementation (Renderer). This allows the two to evolve independently, providing flexibility and scalability.

By using a Bridge, you can introduce new shapes or rendering libraries without modifying existing code.

UML Diagram

In this example:

๐Ÿ”น The Abstraction (Shape) defines a base interface for shapes.

๐Ÿ”น The Implementor (Renderer) specifies the rendering interface.

๐Ÿ”น Concrete Implementors (OpenGLRenderer, DirectXRenderer) provide platform-specific implementations.

๐Ÿ”น Refined Abstractions (Circle, Rectangle) extend the base shape with specific functionality.

How does it Work?

1๏ธโƒฃ Define the abstraction and implementation as separate interfaces.

2๏ธโƒฃ Implement the implementation interface in concrete classes (e.g., OpenGLRenderer, DirectXRenderer).

3๏ธโƒฃ Create concrete classes for the abstraction that use the implementation through composition.

4๏ธโƒฃ The abstraction and implementation work together without being tightly coupled.

๐Ÿ’ก Note: This pattern adheres to the Open/Closed Principle, allowing extensions without modifying existing code.

Advantages of Using the Bridge Pattern

๐Ÿ”ฎ Decoupling: Abstraction (shapes) and implementation (rendering APIs) are independent, enabling modifications in one without affecting the other.

๐Ÿ”ฎ Scalability: Adding new shapes or rendering APIs only requires creating new subclasses or implementations, avoiding class explosion.

๐Ÿ”ฎ Reusability: Common functionality is centralized, reducing redundancy.

๐Ÿ”ฎ Open-Closed Principle: The system is open for extension but closed for modification.

By applying the Bridge Pattern, we achieve a flexible and scalable design that can easily adapt to future changes and requirements.

Show me the Code

# Implementor: Rendering interface
class Renderer
def render_circle(x, y, radius)
raise NotImplementedError, "Subclasses must implement this method"
end
def render_rectangle(x, y, width, height)
raise NotImplementedError, "Subclasses must implement this method"
end
end
# ConcreteImplementor: OpenGL implementation
class OpenGLRenderer < Renderer
def render_circle(x, y, radius)
puts "Rendering circle at (#{x}, #{y}) with radius #{radius} using OpenGL"
end
def render_rectangle(x, y, width, height)
puts "Rendering rectangle at (#{x}, #{y}) with width #{width} and height #{height} using OpenGL"
end
end
# ConcreteImplementor: DirectX implementation
class DirectXRenderer < Renderer
def render_circle(x, y, radius)
puts "Rendering circle at (#{x}, #{y}) with radius #{radius} using DirectX"
end
def render_rectangle(x, y, width, height)
puts "Rendering rectangle at (#{x}, #{y}) with width #{width} and height #{height} using DirectX"
end
end
# Abstraction: Base Shape
class Shape
def initialize(renderer)
@renderer = renderer
end
end
# RefinedAbstraction: Circle
class Circle < Shape
def initialize(renderer, x, y, radius)
super(renderer)
@x, @y, @radius = x, y, radius
end
def draw
@renderer.render_circle(@x, @y, @radius)
end
end
# RefinedAbstraction: Rectangle
class Rectangle < Shape
def initialize(renderer, x, y, width, height)
super(renderer)
@x, @y, @width, @height = x, y, width, height
end
def draw
@renderer.render_rectangle(@x, @y, @width, @height)
end
end
# Example Usage
def main
opengl_renderer = OpenGLRenderer.new
directx_renderer = DirectXRenderer.new
circle = Circle.new(opengl_renderer, 10, 20, 15)
rectangle = Rectangle.new(directx_renderer, 5, 5, 30, 40)
circle.draw
rectangle.draw
end
main
# Run the example
# Output:
# Rendering circle at (10, 20) with radius 15 using OpenGL
# Rendering rectangle at (5, 5) with width 30 and height 40 using DirectX

Conclusion ๐Ÿ”–

The Bridge pattern provides a powerful way to decouple abstraction from its implementation.

๐Ÿ”บ By using this pattern, you can add new shapes or rendering techniques without modifying existing classes, promoting flexibility and maintainability.

๐Ÿ”บ The Bridge pattern simplifies the codebase by avoiding tight coupling between abstractions and their implementations.

๐Ÿ”บ It adheres to the Open/Closed Principle, making it easier to extend functionality in the future.

Join the Quest!

๐Ÿ’ป You can find this and other design patterns here ๐Ÿ“š