Hello everyone, and welcome to .NET Core Central. In this blog post, I am going to walk through the Bridge Pattern.
The bridge design pattern is one of the patterns from the gang of four design patterns. The bridge pattern is a structural design pattern.
The main intent of the bridge design pattern is to decouple an abstraction from its implementation so that they both can vary independently.
Bridge Pattern Usecase
From the intent of the pattern, it is a little bit hard to understand exactly what this means. So I am going to first start with an example that does not implement the bridge pattern. And then I will go ahead and update the solution using the bridge pattern. That will make things clear.
For the purpose of demonstrating the bridge pattern, let us build a notification system. For the demonstration, the notification system will just print responses into the consol, instead of sending them externally using email or text.
NotificationProcessor
Let us consider we have an abstract class called NotificationProcessor
. The NotificationProcessor class has a single abstract method ProcessNotification
. The ProcessNotification method will have a single input parameter message
of type string
.
The ProcessNotification
class is the abstraction in the context of the bridge design pattern.
public abstract class NotificationProcessor
{
public abstract void ProcessNotification(string message);
}
TextNotificationProcessor
After defining the abstract class NotificationProcessor
, we will define another class called TextNotificationProcessor
. The TextNotificationProcesso
r class will derive from the NotificationProcessor
abstract class.
Since the TextNotificationProcessor
class is derived from the abstract class TextNotificationProcessor
, it will implement the abstract method ProcessNotification
. And inside the ProcessNotification
method, it will set the protected variable notificationMessage
with the incoming message
parameter.
public class TextNotificationProcessor : NotificationProcessor
{
protected string notificationMessage;
public override void ProcessNotification(string message)
{
notificationMessage = message;
}
}
TextNotificationSender
And then finally we have another class called TextNotificationSender
. The TextNotificationSender
class is responsible for sending text notifications.
The TextNotificationSender
class derives from the TextNotificationProcessor
, and then it overrides the ProcessNotification
method.
Inside the ProcessNotification
method, we will first we will call the ProcessNotification
of the base class. And then finally we will send the notification, but for this example, we will just write the notification message in the console output.
public class TextNotificationSender : TextNotificationProcessor
{
public override void ProcessNotification(string message)
{
base.ProcessNotification(message);
// Send message here
Console.WriteLine($"Text: {notificationMessage}");
}
}
EmailNotificationProcessor
Similar to the TextNotificationProcessor
, we have another class EmailNotificationProcessor
for sending emails, which behaves similar to the TextNotificationProcessor
.
The EmailNotificationProcessor
class will also derive from the NotificationProcessor
abstract class. And it will implement the abstract method ProcessNotification
.
Since it is email notification, it uses HTML content along with the input parameter message, inside the ProcessNotification
method. And it will set the protected variable notificationMessage
with the HTML content.
public class EmailNotificationProcessor : NotificationProcessor
{
protected string notificationMessage;
public override void ProcessNotification(string message)
{
notificationMessage = $"<html>{message}</html>";
}
}
EmailNotificationSender
Finally, we will have a class for sending the email notification. The EmailNotificationSender
class will have a similar implementation as the TextNotificationSender
class.
The EmailNotificationSender
class will derive from the EmailNotificationProcessor
, and then it overrides the ProcessNotification
method.
Inside the ProcessNotification
method, similar to the TextNotificationSender
, we will first we will call the ProcessNotification
method of the base class. And then finally we will write the notification message in the console output.
public class EmailNotificationSender : EmailNotificationProcessor
{
public override void ProcessNotification(string message)
{
base.ProcessNotification(message);
// Send message here
Console.WriteLine($"Email: {notificationMessage}");
}
}
Running the application
This is a normal hierarchical implementation for a notification system. Now, if we want to send a text notification, we can still use NotificationProcessor
as the contract, and then we can pass in the instance of TextNotificationSender
to send the notification.
To show it working, I am going to use the NotificationProcessor
as a dependency to a new minimal API with route /notification
.
using BridgePattern.Demo;
using Microsoft.AspNetCore.Mvc;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
builder.Services.AddSingleton<NotificationProcessor, TextNotificationSender>();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
app.MapPost("/notification", (NotificationProcessor notificationProcessor, [FromBody] string message) =>
{
notificationProcessor.ProcessNotification(message);
return Results.Ok();
})
.WithName("Notification");
app.Run();
In the above implementation, I am calling the ProcessNotification
method of the NotificationProcessor
abstract base class. And I am passing the message coming as a part of the POST request body.
And during the dependency injection registration, I am registering the TextNotificationSender
as the implementation type for the contract NotificationProcessor
. Hence inside the minimal API when we call the ProcessMessage
, it will call the implementation from the TextNotificationSender
class.
Now if we run the application and execute the API from the Swagger UI, we will see the message set in the POST request body will be printed in the console output.
Issues with this implementation
So now let us discuss what is the problem with this implementation, and the reason why do we need to change this implementation. The main problem with this implementation is this inheritance hierarchy.
What is the problem with inheritance hierarchy? Inheritance hierarchy is not problematic if it is done correctly. But for this implementation, the inheritance hierarchy creates a problem.
For example, if we now make changes to the contract, which is the NotificationProcessor
abstract class in our case. All the implementation of the TextNotificationSender
or the EmailNotificationSender
will also be impacted apart from the TextNotificationProcessor
and EmailNotificationProcessor
. Because they are tightly coupled, any contract change to the NotificationProcessor
abstract class will change the implementation of the derived classes also.
The solution with Bridge Pattern
The question is how do we decouple this tight coupling between abstraction and its implementation. That is where the bridge pattern comes into play.
As I mentioned earlier in this blog post, the main intent of the bridge pattern is to decouple the implementation from the abstraction.
So how can we do that for this example?
Create the INotificationProcessor interface
First of all, we will change the implementation of the NotificationProcesser
from a class to an interface with the name INotificationProcessor
.
public interface INotificationProcessor
{
void ProcessNotification(string message);
}
Next, we will update the implementation of the TextNotificationProcessor
to implement the interface INotificationProcessor. And the ProcessNotification
method will not be abstract anymore. Also, let us make the notificationMessage
variable private.
public class TextNotificationProcessor : INotificationProcessor
{
private string notificationMessage;
public void ProcessNotification(string message)
{
notificationMessage = message;
}
}
Create INotificationSender interface
We will create a new interface INotificationSender
. This interface will have the contract or sending notification. It will define a single method SendNotification
, which will have a parameter message
of type string.
public interface INotificationSender
{
void SendNotification(string message);
}
Update TextNotificationProcessor
After we define the INotificationSender
interface, we will update the implementation of the TextNotificationProcessor
to use this new interface.
public class TextNotificationProcessor : INotificationProcessor
{
private readonly INotificationSender notificationSender;
public TextNotificationProcessor(INotificationSender notificationSender)
{
this.notificationSender = notificationSender;
}
public void ProcessNotification(string message)
{
// Do processing of data here and send notification
notificationSender.SendNotification(message);
}
}
So here for the TextNotificationProcessor
, we will introduce a constructor, where we will inject the INotificationSender
interface. And in ProcessNotification
, instead of setting a local variable, we will do the necessary processing. And after that, we will call the SendNotification
on the INotificationSender
instance.
Update TextNotificationSender
Next, we will update the implementation of the TextNotificationSender
class. And instead of deriving from the TextNotificationProcessor
, this class will now implement the interface INotificationSender
.
public class TextNotificationSender : INotificationSender
{
public void SendNotification(string message)
{
Console.WriteLine($"Text: {message}");
}
}
Update Email related classes
Similarly, we will update the EmailNotificationProcessor
will be implementing the INotificationProcessor
interface.
public class EmailNotificationProcessor : INotificationProcessor
{
private readonly INotificationSender notificationSender;
public EmailNotificationProcessor(INotificationSender notificationSender)
{
this.notificationSender = notificationSender;
}
public void ProcessNotification(string message)
{
notificationSender.SendNotification($"<html>{message}</html>");
}
}
Finally, we will change the implementation of the EmailNotificationSender
to implement the interface INotificationSender
.
public class EmailNotificationSender : INotificationSender
{
public void SendNotification(string message)
{
Console.WriteLine($"Email: {message}");
}
}
Here we created a bridge between the Notification Processor and the Notification Sender. Because the notification processor’s implementation is using the notification sender to send the notification.
By implementing this pattern, what we have done is, if anything changes is a part of INotificationProcessor
will not impact the implementation of INotificationSender
. And this is how we decoupled the abstraction from its implementation.
One important point to note here is that if we implement the SOLID design principles properly, this will be the default implementation. If we are dealing with a notification message and we want to process the notification as well as send the notification, we will never create a class hierarchy.
And we will always end up creating a notification sender which is a single responsibility class responsible for notification sending. And notification processor is like a controller which processes the notification and uses the notification sender to send the notification. so as you can see this implementation or the bridge pattern will happen out of the box if we are just using SOLID design principles.
Run the bridge pattern implementation
Now to run the application, first, we will change the dependency injection registration and add the new class/interface created for the Text notification.
And we will also update the dependency of the API to use the INotificationProcessor
interface instead of the earlier NotificationProcessor
abstract class.
using BridgePattern.Demo;
using Microsoft.AspNetCore.Mvc;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
builder.Services.AddSingleton<INotificationProcessor, TextNotificationProcessor>();
builder.Services.AddSingleton<INotificationSender, TextNotificationSender>();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
app.MapPost("/notification", (INotificationProcessor notificationProcessor, [FromBody] string message) =>
{
notificationProcessor.ProcessNotification(message);
return Results.Ok();
})
.WithName("Notification");
app.Run();
Now if we run the application, we will see the exact same response as before.
Conclusion
This is a very simple implementation of the bridge pattern. And as I mentioned initially, back in the days before the SOLID design principle, it really made sense of using the bridge pattern in certain scenarios.
But after I started using the SOLID design principles, I personally feel that the bridge pattern kind of comes in for free with the SOLID design principles. If we use the SOLID principles properly, we will end up getting into the bridge pattern out of the box.
Because with SOLID, we will never have one hierarchical giant implementation for anything. We will always break things down into individual single responsibility classes and interfaces.
A Youtube video for this implementation is here.