Bridge Pattern with Real world example
bridge-pattern

Why do we need Bridge pattern?

Check out conversation between two programmers.

Programmer 1: Hey, we're designing a new shipping service for our e-commerce website, but I'm concerned about the complexity of our code. We need to make sure that we can add new shipping providers easily without constantly changing our existing code.

Programmer 2: Yeah, that's a good point. We could use the bridge pattern to solve this problem.

Programmer 1: What's the bridge pattern?

Solution - Bridge Pattern

The bridge pattern is a design pattern that enables the separation of an abstraction from its implementation. This allows the two to vary independently, which can be useful in situations where there are multiple abstractions and/or multiple implementations, and where changes to one might impact the other.

In other words, the Adapter pattern is used when we have two interfaces that are incompatible with each other and we want to use an object that implements one of the interfaces with the other interface. Instead of modifying the existing objects, we can create a new object that adapts the existing object's interface to the required interface.

In the bridge pattern, the abstraction provides the interface or behavior that clients interact with, while the implementation provides the concrete functionality that the abstraction relies on. The abstraction and implementation are connected through a "bridge" that encapsulates the implementation and provides an abstraction-specific interface for the client to use.

Using the bridge pattern can help to manage complexity, increase flexibility, and improve maintainability in larger software systems. However, it can also increase development time and introduce additional complexity if not used appropriately.

Implementation

Lets take below example using Bridge Pattern

In this example, the IShippingService interface represents the abstraction, which is implemented by the DomesticShippingService and InternationalShippingService classes. The IShippingProvider interface represents the implementation, which is implemented by the UPSProvider and FedExProvider classes. The DomesticShippingService and InternationalShippingService classes take an IShippingProvider object as a constructor parameter, which is used to calculate the shipping cost based on the weight and destination of the package. This allows us to vary the implementation (i.e. the shipping provider) independently from the abstraction (i.e. the shipping service). Note that this is just one example implementation of the bridge pattern in C#. The specific details of the implementation may vary depending on the specific requirements of your project.


// Abstraction
public interface IShippingService
{
    void ShipPackage(double weight);
}

// Refined Abstraction
public class DomesticShippingService : IShippingService
{
    private IShippingProvider _shippingProvider;

    public DomesticShippingService(IShippingProvider shippingProvider)
    {
        _shippingProvider = shippingProvider;
    }

    public void ShipPackage(double weight)
    {
        double shippingCost = _shippingProvider.CalculateShippingCost(weight, "USA");
        Console.WriteLine("Shipping package domestically via {0} for a cost of {1:C}", _shippingProvider.Name, shippingCost);
    }
}

// Refined Abstraction
public class InternationalShippingService : IShippingService
{
    private IShippingProvider _shippingProvider;

    public InternationalShippingService(IShippingProvider shippingProvider)
    {
        _shippingProvider = shippingProvider;
    }

    public void ShipPackage(double weight)
    {
        double shippingCost = _shippingProvider.CalculateShippingCost(weight, "Canada");
        Console.WriteLine("Shipping package internationally via {0} for a cost of {1:C}", _shippingProvider.Name, shippingCost);
    }
}

// Implementation
public interface IShippingProvider
{
    string Name { get; }
    double CalculateShippingCost(double weight, string destination);
}

// Concrete Implementation
public class UPSProvider : IShippingProvider
{
    public string Name => "UPS";

    public double CalculateShippingCost(double weight, string destination)
    {
        // Calculate shipping cost for UPS based on weight and destination
        return 10.0;
    }
}

// Concrete Implementation
public class FedExProvider : IShippingProvider
{
    public string Name => "FedEx";

    public double CalculateShippingCost(double weight, string destination)
    {
        // Calculate shipping cost for FedEx based on weight and destination
        return 12.0;
    }
}


Components of Bridge pattern

Abstraction: The IShippingService interface in the example represents the abstraction, which defines the interface for the higher-level abstraction that clients interact with.

Refined Abstraction: The DomesticShippingService and InternationalShippingService classes in the example extend the abstraction and provide more specific behavior or functionality. These refined abstractions may contain references to one or more instances of the implementation. cases.

Implementation: The IShippingProvider interface in the example represents the implementation, which defines the interface for the lower-level implementation classes. cases.

Concrete Implementation: The UPSProvider and FedExProvider classes in the example implement the IShippingProvider interface and provide specific functionality that can be used by the refined abstraction. cases.

In this example, the abstraction is represented by the IShippingService interface, which is implemented by the DomesticShippingService and InternationalShippingService classes. These refined abstractions contain references to one or more instances of the implementation, which is represented by the IShippingProvider interface. The concrete implementations of the IShippingProvider interface are represented by the UPSProvider and FedExProvider classes. By using the bridge pattern, the shipping service can be easily extended to support new shipping providers without affecting the existing code.

Challenges & Limitations

  • Increased complexity: The bridge pattern introduces an additional layer of abstraction, which can make the code more complex and harder to understand.
  • Increased number of classes: The bridge pattern requires the creation of additional classes, which can increase the number of classes in the codebase.
  • Increased development time: The creation of additional classes and interfaces required by the bridge pattern can increase the development time of the system.
  • Limited benefits for small systems: The bridge pattern is most useful for larger systems with complex hierarchies of abstractions and implementations. For smaller systems, the added complexity of the bridge pattern may not be worth the benefits.
  • Potential for over-engineering: The bridge pattern can be overused, leading to unnecessary complexity and decreased maintainability.
  • Tight coupling: While the bridge pattern aims to decouple the abstraction from the implementation, if not implemented properly, it can lead to tight coupling between the abstraction and the implementation.

Overall, the bridge pattern can be a useful tool for managing complexity and increasing flexibility in larger systems. However, it should be used judiciously and only when the benefits outweigh the costs.

Summary

  • The bridge pattern allows for separation of abstraction and implementation, enabling them to vary independently.
  • The basic components of the bridge pattern include the abstraction, refined abstraction, implementation, and concrete implementation.
  • The bridge pattern can help manage complexity and increase flexibility in larger systems.
  • However, the bridge pattern can also lead to increased development time and complexity, especially for smaller systems.
  • If not implemented properly, the bridge pattern can lead to tight coupling between the abstraction and implementation.
  • As with any design pattern, the bridge pattern should be used judiciously and only when the benefits outweigh the costs.