Golang - Bridge Pattern

Golang - Bridge Pattern

In this article, we will explore the "Bridge Pattern" as it applies to Golang. The Bridge Pattern is one of the design patterns from the famous "Gang of Four" book, which describes the solution to common problems in software design. We will also provide a code example and highlight the main differences between the Bridge Pattern and the Adapter Pattern.

What is the Bridge Pattern?

The Bridge Pattern is a structural design pattern that decouples an abstraction from its implementation so that both can vary independently. This pattern is useful when we have an abstraction that can have multiple implementations, and we want to be able to switch between them at runtime. The Bridge Pattern provides a way to organize the code so that the implementation details are hidden from the client.

In software engineering, we often encounter situations where we need to separate an abstraction from its implementation. For example, when designing a user interface, we might have a button that can be rendered in different styles, such as a flat button or a 3D button. These buttons have the same functionality, but their appearance is different. The Bridge Pattern provides a way to separate the functionality of the button from its appearance so that we can change the appearance without affecting the functionality.

Code Example

Let's illustrate the Bridge Pattern with an example. Suppose we have a Shape interface and two implementations of this interface: Circle and Square. Also, suppose we have two Renderer interfaces that implement the rendering of these shapes: VectorRenderer and RasterRenderer. To implement the Bridge Pattern, we create a bridge between the Shape interface and the Renderer interface. This allows us to switch between different renderers without changing the Shape interface.

type Renderer interface {
    RenderCircle(radius float32)
    RenderSquare(side float32)
}

type Shape interface {
    Draw()
}

type Circle struct {
    x, y, radius float32
    renderer Renderer
}

func (c *Circle) Draw() {
    c.renderer.RenderCircle(c.radius)
}

type Square struct {
    side float32
    renderer Renderer
}

func (s *Square) Draw() {
    s.renderer.RenderSquare(s.side)
}

type VectorRenderer struct {}

func (v *VectorRenderer) RenderCircle(radius float32) {
    fmt.Printf("Drawing a circle of radius %f in VectorRenderer\\\\n", radius)
}

func (v *VectorRenderer) RenderSquare(side float32) {
    fmt.Printf("Drawing a square of side %f in VectorRenderer\\\\n", side)
}

type RasterRenderer struct {}

func (r *RasterRenderer) RenderCircle(radius float32) {
    fmt.Printf("Drawing a circle of radius %f in RasterRenderer\\\\n", radius)
}

func (r *RasterRenderer) RenderSquare(side float32) {
    fmt.Printf("Drawing a square of side %f in RasterRenderer\\\\n", side)
}

In this example, we have the Shape interface and two implementations of this interface: Circle and Square. Both Circle and Square have a renderer field that implements the Renderer interface. The Renderer interface has two methods: RenderCircle and RenderSquare. We also have two implementations of the Renderer interface: VectorRenderer and RasterRenderer.

Now we can create objects of Circle and Square and pass a renderer to each of them. For example:

circle := &Circle{x: 0, y: 0, radius: 5, renderer: &VectorRenderer{}}
square := &Square{side: 10, renderer: &RasterRenderer{}}

We can call the Draw method on each of these objects, and they will use the renderer that was passed to them.

circle.Draw() // Drawing a circle of radius 5.000000 in VectorRenderer
square.Draw() // Drawing a square of side 10.000000 in RasterRenderer

Main Differences between Bridge Pattern and Adapter Pattern

The Bridge Pattern and Adapter Pattern are both structural design patterns that deal with decoupling the client code from the implementation details. However, they differ in their approach.

The Adapter Pattern is used when we want to adapt an existing interface to meet the needs of the client. We do this by creating a new interface that the client can use, and implementing this interface by adapting the existing interface. In other words, the Adapter Pattern changes the existing interface to make it usable by the client.

On the other hand, the Bridge Pattern is used when we want to decouple an abstraction from its implementation so that both can vary independently. We do this by creating a bridge between the abstraction and the implementation. In other words, the Bridge Pattern creates a new layer of abstraction between the client and the implementation.

Advantages of the Bridge Pattern

The Bridge Pattern has several advantages over other design patterns:

  1. Separation of concerns: The Bridge Pattern separates the abstraction from its implementation, which makes it easier to modify and maintain the code.

  2. Encapsulation: The Bridge Pattern encapsulates the implementation details, which makes the code more secure and less error-prone.

  3. Flexibility: The Bridge Pattern allows for the creation of new implementations without affecting the abstraction.

  4. Reusability: The Bridge Pattern promotes code reuse by allowing different abstractions to use the same implementation.

  5. Testability: The Bridge Pattern makes the code more testable by allowing the implementation to be mocked.

Conclusion

The Bridge Pattern is a useful design pattern that allows us to decouple an abstraction from its implementation. This pattern is particularly useful when we have an abstraction that can have multiple implementations, and we want to be able to switch between them at runtime. In this article, we provided a code example of how to implement the Bridge Pattern in Golang and highlighted the main differences between the Bridge Pattern and the Adapter Pattern. We also discussed the advantages of the Bridge Pattern, such as separation of concerns, encapsulation, flexibility, reusability, and testability. The Bridge Pattern is a powerful tool in the software engineer's toolbox and should be considered when designing software systems.


References

Did you find this article valuable?

Support Matthias Bruns by becoming a sponsor. Any amount is appreciated!