Gaining Flexibility by Building Bridges: Analysis and Implementations in Four Different Languages with the Bridge Design Pattern

[tr] Türkçe Oku

2023-06-22

Once upon a time, there were two kingdoms. Despite being very close to each other, a wide and deep river was located between them. This river made it difficult for the kingdoms to interact with each other. To solve this situation, both kingdoms decided to build a bridge.

This bridge became an interface that facilitated communication between the two kingdoms. However, the bridge had different structures and features on each side. One side was made of stone, while the other was made of wood. This allowed each side of the bridge to be modified independently of the other. In other words, changing the structure of one side did not affect the other side. This increased the flexibility and extensibility of the bridge.

This story represents the fundamental idea of the Bridge design pattern. The Bridge pattern allows different parts of an application to be modified independently of each other. This makes the application more flexible and extensible.

Bridge Pattern Class Diagram

C# Example:

interface IDrawAPI
{
    void DrawCircle(int radius, int x, int y);
}

public class RedCircle : IDrawAPI
{
    public void DrawCircle(int radius, int x, int y)
    {
        Console.WriteLine("Drawing Circle[ color: red, radius: " + radius + ", x: " + x + ", " + y + "]");
    }
}

public class GreenCircle : IDrawAPI
{
    public void DrawCircle(int radius, int x, int y)
    {
        Console.WriteLine("Drawing Circle[ color: green, radius: " + radius + ", x: " + x + ", " + y + "]");
    }
}

abstract class Shape
{
    protected IDrawAPI drawAPI;
    protected Shape(IDrawAPI drawAPI)
    {
        this.drawAPI = drawAPI;
    }
    public abstract void Draw();
}

public class Circle : Shape
{
    private int x, y, radius;

    public Circle(int x, int y, int radius, IDrawAPI drawAPI) : base(drawAPI)
    {
        this.x = x;
        this.y = y;
        this.radius = radius;
    }

    public override void Draw()
    {
        drawAPI.DrawCircle(radius,x,y);
    }
}

Java Example:

interface DrawAPI {
   public void drawCircle(int radius, int x, int y);
}

public class RedCircle implements DrawAPI {
   @Override
   public void drawCircle(int radius, int x, int y) {
      System.out.println("Drawing Circle[ color: red, radius: " + radius + ", x: " + x + ", " + y + "]");
   }
}

public class GreenCircle implements DrawAPI {
   @Override
   public void drawCircle(int radius, int x, int y) {
      System.out.println("Drawing Circle[ color: green, radius: " + radius + ", x: " + x + ", " + y + "]");
   }
}

abstract class Shape {
   protected DrawAPI drawAPI;
   protected Shape(DrawAPI drawAPI){
      this.drawAPI = drawAPI;
   }
   public abstract void draw();  
}

public class Circle extends Shape {
   private int x, y, radius;

   public Circle(int x, int y, int radius, DrawAPI drawAPI) {
      super(drawAPI);
      this.x = x;  
      this.y = y;  
      this.radius = radius;
   }

   public void draw() {
      drawAPI.drawCircle(radius,x,y);
   }
}

Go Example:

package main

import "fmt"

type DrawAPI interface {
   drawCircle(radius int, x int, y int) 
}

type RedCircle struct {}



func (r *RedCircle) drawCircle(radius int, x int, y int) {
   fmt.Printf("Drawing Circle[ color: red, radius: %d, x: %d, y: %d]\n", radius, x, y)
}

type GreenCircle struct {}

func (g *GreenCircle) drawCircle(radius int, x int, y int) {
   fmt.Printf("Drawing Circle[ color: green, radius: %d, x: %d, y: %d]\n", radius, x, y)
}

type Shape struct {
   drawAPI DrawAPI
}

type Circle struct {
   x int
   y int
   radius int
   drawAPI DrawAPI
}

func (c *Circle) draw() {
   c.drawAPI.drawCircle(c.radius, c.x, c.y)
}

Rust Example:

trait DrawAPI {
    fn draw_circle(&self, radius: i32, x: i32, y: i32);
}

struct RedCircle;

impl DrawAPI for RedCircle {
    fn draw_circle(&self, radius: i32, x: i32, y: i32) {
        println!("Drawing Circle[ color: red, radius: {}, x: {}, y: {}]", radius, x, y);
    }
}

struct GreenCircle;

impl DrawAPI for GreenCircle {
    fn draw_circle(&self, radius: i32, x: i32, y: i32) {
        println!("Drawing Circle[ color: green, radius: {}, x: {}, y: {}]", radius, x, y);
    }
}

struct Circle {
    x: i32,
    y: i32,
    radius: i32,
    draw_api: Box<dyn DrawAPI>,
}

impl Circle {
    fn new(x: i32, y: i32, radius: i32, draw_api: Box<dyn DrawAPI>) -> Circle {
        Circle { x, y, radius, draw_api }
    }

    fn draw(&self) {
        self.draw_api.draw_circle(self.radius, self.x, self.y);
    }
}

These examples demonstrate how the Bridge design pattern can be implemented. In each one, there is an interface (or trait in Rust) called DrawAPI and two classes (RedCircle and GreenCircle) that implement this interface. The Shape and Circle classes perform drawing operations using this interface. This allows the way in which drawing operations are performed (using RedCircle or GreenCircle) to be independent of the Shape and Circle classes.

There are many reasons to use the Bridge design pattern. Here are the most important ones:

  1. Independence: The Bridge design pattern allows different parts of an application to be modified independently of each other. This means that changing one part does not affect the others.

  2. Flexibility: The Bridge design pattern makes the application more flexible. This means that the application can easily adapt to different requirements and situations.

  3. Extensibility: The Bridge design pattern makes the application extensible. This means that new features or functions can be easily added.

  4. Code Reuse: The Bridge design pattern facilitates code reuse. This means that the same code can be used over and over again in different places, making the code shorter and more understandable.

  5. Separation and Organization: The Bridge design pattern allows for better organization and separation of code. This makes the code easier to read and maintain.

  6. Dynamic Binding: The Bridge design pattern uses dynamic binding. This means that the application can choose different implementations at runtime.



More posts like this

The Dance of Components: The Composite Design Pattern

2023-06-22 | #composite-pattern #design-patterns #structural-patterns

Once upon a time, there was a tree in a forest. This tree was a complex structure with leaves and branches. Each branch could have smaller branches and leaves on it. The tree worked as a whole, holding its branches and leaves together. This tree is an example of the Composite design pattern. The tree (Composite) contains two types of components: branches (also Composite) and leaves (Leaf). Both branches and leaves implement the same interface (Component) recognized by the tree.

Continue reading 


Decorator Design Pattern: The Adornments of Software

2023-06-15 | #decorator-pattern #design-patterns #structural-patterns

Once upon a time, there was an object named “Component”. This object was used to perform a specific function. However, sometimes it was necessary to extend or modify this function. That’s where “Decorators” came into play. Decorators are objects that “decorate” or extend the Component. A Decorator comes on top of a Component and extends or modifies its function. This is used to extend the functionality of the Component without changing it itself.

Continue reading 


From the Language of Code to Fairy Tales: Uniting Different Worlds with the Adapter Pattern

2023-06-15 | #adapter-pattern #design-patterns #structural-patterns

Once upon a time, there were two friends from two different worlds: the Electric Vacuum Cleaner and the Electric Outlet. The Electric Vacuum Cleaner needed energy and wanted to get this energy from the Electric Outlet. However, there was a problem. The plug of the Electric Vacuum Cleaner did not fit the Electric Outlet. They were both produced in different standards and could not communicate directly with each other. In this case, a hero emerged: the Adapter.

Continue reading 