Bridge pattern in C#

 Hello, C# enthusiasts! 😍

Today, I want to share with you a very useful design pattern that can help you separate the business logic from the data access layer in your applications. This pattern is called the Bridge pattern, and it is one of the structural design patterns that deal with how classes and objects are composed to form larger structures. 🏗️

The Bridge pattern allows you to decouple an abstraction (such as an interface or an abstract class) from its implementation (such as a concrete class that implements the interface or inherits from the abstract class) so that the two can vary independently. This means that you can change the data access layer without affecting the business logic layer, and vice versa. 🙌

But why would you want to do that? Well, there are many scenarios where you might have more than one version of an abstraction, or more than one way of implementing an abstraction. For example, suppose you are developing a financial application that needs to perform different operations on different types of accounts (such as checking, saving, or credit). You could define an abstraction for the account, such as an interface called IAccount, and then have different implementations for each type of account, such as CheckingAccount, SavingAccount, and CreditAccount. However, this would lead to a large number of classes, and a tight coupling between the abstraction and the implementation. 😱

The Bridge pattern solves this problem by introducing another level of abstraction, called the Implementor, which defines the common operations for all implementations. Then, each concrete implementation inherits from the Implementor and provides its own specific behavior. The original abstraction (IAccount) then holds a reference to an Implementor object, and delegates the calls to it. This way, you can have a hierarchy of abstractions (such as DepositOperation, WithdrawOperation, TransferOperation) that are independent of the hierarchy of implementations (such as CheckingAccount, SavingAccount, CreditAccount). You can also easily add new abstractions or new implementations without breaking the existing code. 😎

Let’s see how this works in C# with a simple example. First, we define the Implementor interface:


// The 'Implementor' interface
public interface IAccountImplementor
{
// A method to get the balance of an account
decimal GetBalance();

// A method to update the balance of an account
void UpdateBalance(decimal amount);
}

Then, we define the concrete implementations for each type of account:

// The 'ConcreteImplementorA' class
public class CheckingAccount : IAccountImplementor
{
private decimal _balance;

public CheckingAccount(decimal initialBalance)
{
_balance = initialBalance;
}

public decimal GetBalance()
{
return _balance;
}

public void UpdateBalance(decimal amount)
{
_balance += amount;
}
}


// The 'ConcreteImplementorC' class
public class CreditAccount : IAccountImplementor
{
private decimal _balance;
private readonly decimal _creditLimit;

public CreditAccount(decimal initialBalance, decimal creditLimit)
{
_balance = initialBalance;
_creditLimit = creditLimit;
}

public decimal GetBalance()
{
return _balance - _creditLimit;
}

public void UpdateBalance(decimal amount)
{
_balance += amount;
}
}


// The 'ConcreteImplementorB' class
public class SavingAccount : IAccountImplementor
{
private decimal _balance;
private readonly decimal _interestRate;

public SavingAccount(decimal initialBalance, decimal interestRate)
{
_balance = initialBalance;
_interestRate = interestRate;
}

public decimal GetBalance()
{
return _balance * (1 + _interestRate);
}

public void UpdateBalance(decimal amount)
{
_balance += amount;
}
}

Next, we define the Abstraction interface:

// The 'Abstraction' interface
public interface IAccountOperation
{
// A property to get or set the implementor
IAccountImplementor Implementor { get; set; }

// A method to perform an operation on an account
void Perform();
}


And then we define the refined abstractions for each operation:


// The 'RefinedAbstractionA' class
public class DepositOperation : IAccountOperation
{
public IAccountImplementor Implementor { get; set; }

private readonly decimal _amount;

public DepositOperation(decimal amount)
{
_amount = amount;
}

public void Perform()
{
Console.WriteLine("Depositing " + _amount + " to the account");
Implementor.UpdateBalance(_amount);
Console.WriteLine("The new balance is " + Implementor.GetBalance());
}
}

// The 'RefinedAbstractionB' class
public class WithdrawOperation : IAccountOperation
{
public IAccountImplementor Implementor { get; set; }

private readonly decimal _amount;

public WithdrawOperation(decimal amount)
{
_amount = amount;
}

public void Perform()
{
Console.WriteLine("Withdrawing " + _amount + " from the account");
Implementor.UpdateBalance(-_amount);
Console.WriteLine("The new balance is " + Implementor.GetBalance());
}
}

// The 'RefinedAbstractionC' class
public class TransferOperation : IAccountOperation
{
public IAccountImplementor Implementor { get; set; }

private readonly IAccountImplementor _target;
private readonly decimal _amount;

public TransferOperation(IAccountImplementor target, decimal amount)
{
this._target = target;
this._amount = amount;
}

public void Perform()
{
Console.WriteLine("Transferring " + _amount + " from the source account to the target account");
Implementor.UpdateBalance(-_amount);
_target.UpdateBalance(_amount);
Console.WriteLine("The new balance of the source account is " + Implementor.GetBalance());
Console.WriteLine("The new balance of the target account is " + _target.GetBalance());
}
}

Finally, we can use the Bridge pattern in our client code like this:

// Create an array of operations
IAccountOperation[] operations = new IAccountOperation[3];
operations[0] = new DepositOperation(100);
operations[1] = new WithdrawOperation(50);
operations[2] = new TransferOperation(new SavingAccount(200, 0.05m), 25);

// Set the implementor for each operation based on the type of account
string accountType = "Checking"; // You can change this to "Saving" or "Credit"
switch (accountType)
{
case "Checking":
foreach (var operation in operations)
{
operation.Implementor = new CheckingAccount(100);
}
break;
case "Saving":
foreach (var operation in operations)
{
operation.Implementor = new SavingAccount(100, 0.05m);
}
break;
case "Credit":
foreach (var operation in operations)
{
operation.Implementor = new CreditAccount(100, 500);
}
break;
default:
Console.WriteLine("Invalid account type");
break;
}

// Perform the operations
foreach (var operation in operations)
{
operation.Perform();
Console.WriteLine();
}


The output of this program will be:

Depositing 100 to the account
The new balance is 200

Withdrawing 50 from the account
The new balance is 50

Transferring 25 from the source account to the target account
The new balance of the source account is 75
The new balance of the target account is 236.25


As you can see, the Bridge pattern allows us to change the data access layer of the accounts without affecting the business logic layer of the operations, and vice versa. We can also easily add new types of accounts or new types of operations without modifying the existing code. 🚀

I hope you enjoyed this article and learned something new about the Bridge pattern. If you want to learn more about this pattern, you can check out these resources:


Happy coding! 😊


You can find the code examples here: source code


Comments

Popular posts from this blog

Which GOF patterns are good for C#?

Angular on a regular SharePoint page (part 2)

Chain of Responsibility pattern in C#