Bridge Design Pattern in C#

This is the seventh post in the Structural Design Patterns series and in this post, we will discuss the Bridge Design Pattern and its implementation using the C# programming language.

The Bridge Design Pattern provides a way to decouple an abstraction from its implementation so that the two can vary independently. The Bridge Design Pattern falls into the Structural Design Pattern category as defined by the Gang of Four (GoF). If you are new to design patterns, I would highly recommend you to read the introductory post “Introduction to Design Patterns“

What is Bridge Design Pattern?

The Bridge Design Pattern helps us to separate an interface (or an abstract class) from its implementer class so that both entities can vary (developed) independently. The pattern provides a way to change the implementation details without the need to changes the abstraction.

Let us take a simple example to understand the pattern. We have custom business logic to process employee data, and this processed employee data will be saved as an XML on a Windows machine and as a JSON file on a UNIX machine. The saving part differs based on the operating system.

As per the Bridge Desing Pattern, we may abstract (decouple) the business processing logic from the saving logic and it will have no knowledge of how the data will be saved. The abstraction contains a reference (via composition) to the implementor. The implementor class (saving of data) details will be provided during the runtime based on the operating system and both abstraction and implementer can be developed independently.

When to consider Bridge Design Pattern

Few of the scenarios when the Bridge Design Pattern can be considered are as follows:

  • When a class has multiple variants of the same functionality. E.g. writing different types of output files for some processed data.
    • The class functionality can be broken down into various classes having a common interface. This not only decouples the functionality type but also avoid creating an unnecessary huge monolithic class that is complex to maintain.
  • When there is a need to invoke separate implementation based on runtime.
    • Continuing from the above example, a processed data needs to be written as an XML file in Windows and as a JSON file in the UNIX system.
  • When there is a need to extend a class to different independent (orthogonal) dimensions.
    • When writing different file formats (XML, JSON), file specific serializer functionality need to be invoked.

Bridge Design Pattern UML

bridge design pattern uml diagram
UML Diagram

The artifacts of the UML diagram are as follows:

  • Abstraction: This is the interface that defines the operation and maintains a reference of the Implementor.
  • RefinedAbstraction: This is a concrete class that implements the Abstraction interface and provides a variant functionality.
  • Implementor: This is the interface that declares the common functionality for all the concrete implementors. The Abstraction uses the reference of it to communicate with the implementation at runtime.
  • ConcreteImplementor: This is the concrete class that implements the Implementor interface and provides the actual functionality.

C# Implementation

Let us build a .NET Core C# console application to implement the Bridge Design Pattern using a very simple requirement.

Requirement Description

A client processes employee salary details and want to save the processed data to an XML or a JSON file. The saving of data to a specific file format will be decided during the runtime and the client can introduce other formats in the near future. The salary processing remains the same and won’t change, but the file format can vary.

Let us build up a simple console application and make use of Bridge pattern to implement the same. The details are as follows:

Requirement Implementation in C#

The artifacts of the application are as follows:

Employee.cs: This class represent an Employee.

/// <summary>
/// This class represent an Employee
/// </summary>
public class Employee
{
    public int Id { get; set; }
    public string Name { get; set; }
    public decimal Basic { get; set; }
    public decimal Incentive { get; set; }
    public decimal TotalSalary { get; set; }
}

IFileWriter.cs: The interface represents the Implementor interface from the design pattern and exposes a single method. to write a given employee details to a file.

/// <summary>
/// The interface represents the Implementor interface from the design pattern and exposes a single method 
/// to write a given employee details to a file
/// </summary>
public interface IFileWriter
{
    void WriteFile(Employee employee);

XMLWriter.cs: This class represents a ConcreteImplementor and writes the given Employee details to an XML file.

/// <summary>
/// This class represent a ConcreteImplementor and write the given Employee details to 
/// an XML file
/// </summary>
public class XMLWriter : IFileWriter
{
    string fileName = "EmployeeSalaryDetails.xml";
    XmlSerializer xmlSerializer = new XmlSerializer(typeof(Employee));
    FileStream fileStream;

    public void WriteFile(Employee employee)
    {
        fileStream = new FileStream(fileName, FileMode.OpenOrCreate);
        xmlSerializer.Serialize(fileStream, employee);
        Console.WriteLine($"EmployeeId# : {employee.Id} salary details sucessfully written to {fileName} "); 
    }
}

JSONWriter.cs: This class represents another ConcreteImplementor class and writes the given Employee details to a JSON file.

/// <summary>
/// This class represents another ConcreteImplementor class and writes the given Employee details to 
/// a JSON file.
/// </summary>
public class JSONWriter : IFileWriter
{
    string fileName = "EmployeeSalaryDetails.json";
         
    public void WriteFile(Employee employee)
    {
        var serializedEmployeeData = JsonSerializer.Serialize(employee);
        File.WriteAllText(fileName, serializedEmployeeData);
        Console.WriteLine($"EmployeeId# : {employee.Id} salary details sucessfully written to {fileName} ");
    }
}

ContentWriter.cs: This abstract class represents the Abstraction from the pattern and has a different method WriteContent() and internally invokes the WriteFile() of the IFileWriter interface as it also maintains an internal reference to it.

  /// <summary>
/// This abstract class represents the Abstraction from the pattern and have different method 
/// than that of Implementor interface.
/// </summary>
public abstract class ContentWriter
{
    protected IFileWriter fileWriter;

    public ContentWriter(IFileWriter fileWriter)
    {
        this.fileWriter = fileWriter;
    }

    public virtual void WriteContent(Employee employee)
    {
        fileWriter.WriteFile(employee);
    }
}

SalaryCalculator.cs: This is the RefinedConcrete class, this class processes salary data and write the data on the file internally.

/// <summary>
/// This is the RefinedConcrete class, this class processes salary data and write the
/// data on the file.
/// </summary>
public class SalaryCalculator : ContentWriter
{
    /// <summary>
    /// The constructor accepts IFileWriter object, and the value is supplied during the runtime
    /// </summary>
    /// <param name="fileWriter"></param>
    public SalaryCalculator(IFileWriter fileWriter) : base(fileWriter)
    {
    } 

    /// <summary>
    /// The method processes the employee's salary and internally executes the WriteFile operation 
    /// to persists the employee data to a file.
    /// </summary>
    /// <param name="employee"></param>
    public void ProcessEmployeeSalary(Employee employee)
    {
        // Some Processing
        employee.TotalSalary = employee.Basic + employee.Incentive;
        Console.WriteLine($"EmployeeId# {employee.Id} calculated salary# {employee.TotalSalary}");
        fileWriter.WriteFile(employee);
    }
       
}

Client.cs: This is the client application that passes the required IFileWriter object to the SalaryCalculator during runtime.

class Client
{
    static void Main(string[] args)
    {
        SalaryCalculator salaryCalculator = new SalaryCalculator(new XMLWriter());

        Employee employee = new Employee { Id = 1001, Name = "John Doe", Basic = 3000, Incentive = 2000 };
        salaryCalculator.ProcessEmployeeSalary(employee);

        employee.Incentive = 2500;
        salaryCalculator = new SalaryCalculator(new JSONWriter());
        salaryCalculator.ProcessEmployeeSalary(employee);

    }
}

Application Output

The various output of the application are:

XML File: EmployeeSalaryDetails.xml

<?xml version="1.0"?>
<Employee xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
  <Id>1001</Id>
  <Name>John Doe</Name>
  <Basic>3000</Basic>
  <Incentive>2000</Incentive>
  <TotalSalary>5000</TotalSalary>
</Employee>

JSON File: EmployeeSalaryDetails.json

{
  "Id": 1001,
  "Name": "John Doe",
  "Basic": 3000,
  "Incentive": 2500,
  "TotalSalary": 5500
}

Console Output:

bridge design pattern sample application output
Console Output

Source Code

The source code of the application is available on GitHub: https://github.com/technicalbundle/bridgedesignpattern

Advantages of Bridge Design Pattern

Few of the advantages of the Bridge Design Pattern are as follows:

  • The client (consumer) application works with high-level abstraction without worrying about the implementation details. The implementation can be changed without breaking the client’s code.
  • Increases the code maintainability, reusability, and reduces code duplication.
  • Platform independent classes and applications can be developed.
  • Helps to promote the Open-Closed principle, new abstractions and implementations can be developed independently.

Disadvantages of Bridge Design Pattern

Few of the disadvantages of the Bridge Design Pattern are as follows:

  • The code being considered for abstraction and implementation must be separable.
  • The separated functionality must be able to work independently.
  • The introduction of the pattern increases code complexity.
  • A level of indirection is introduced as the request is passed from the Abstraction to the actual implementor.

Conclusion

In this blog post, we try to learn about the Bridge Design Pattern, few scenarios where it can be applied along with its advantages and disadvantages, and also tried to implement it using a simple use case.

I hope you find this post helpful. Thanks for visiting. Cheers!!!

[Further Readings: Decorator Design Pattern in C# |  Flyweight Design Pattern in C# |  Composite Design Pattern in C# |  Facade Design Pattern in C# |  Proxy Design Pattern in C# |  SQLite Studio to manage SQLite databases |  Adapter Design Pattern in C# |  How to use Blazor EditForm for Model Validation |  How to add a new profile in Windows Terminal |  How to easily Customize Global Settings in Windows Terminal |  How to add Git Bash to Windows Terminal Application |  How to customize Windows Terminal Application  ]  

0 0 votes
Article Rating
Subscribe
Notify of
guest
0 Comments
Inline Feedbacks
View all comments
0
Would love your thoughts, please comment.x
()
x