Bridge pattern without bullsh*t

iamprovidence
5 min readMay 17, 2024

--

Bridge is a structural design pattern that lets you split a large class into two separate hierarchies — abstraction and implementation — which can be developed independently of each other. Abstraction? Implementation? Huh 🤔?What a bullsh*t!

Today we will demystify the term abstraction and implementation. You will see a practical example of the bridge pattern. And most importantly, you will understand the whole point behind this pattern.

Let us start.

The confussion starts in 3… 2… 1…

The problem with this pattern starts as soon as you read the definition.

The bridge pattern is a design pattern used in software engineering that is meant to “decouple an abstraction from its implementation so that the two can vary independently”.

Abstraction and implementation. What an obscure choice of words. When people hear abstraction and implementation they usually think of an interface or an abstract class and a concrete class that implements it. In practice, in the context of the bridge pattern, those are completely different things.

Abstraction — is a hierarchy of classes that delegate the work to implementations.

Implementation — is a hierarchy of classes that do the actual work.

It is still hard to comprehend the idea behind this pattern. If you still struggle to distinguish abstraction from implementation, just completely forget about those. I will give you a definition without bullsh*t.

The bridge pattern is a design pattern used in software engineering that is meant to decouple single-class behavior into two separate hierarchies of classes.

Let’s see it in practice.

Refactoring to bridge pattern

One of the most common task on every project is exporting data into a file.

Imagine you are working on the ERP system and need to implement it. Our customer promised we would only export one entity in CSV format, so you have not designed anything complicated. But as always happens, customer ̶l̶i̶e̶ hold back a bit of their intention. Let’s see what problems we encounter with our naive implementation and how it can be improved with the Bridge pattern.

So, first our customer wanted to export only Users in CSV format. You quickly get to work and implement something like this:

interface IExporter
{
File Export();
}

class CsvUserExporter : IExporter
{
public File Export()
{
. . .
}
}

The feature was so successful, that customer asked to implement export for Companies. Not a big deal, you just implement another class:

class CsvCompanyExporter : IExporter
{
public File Export()
{
. . .
}
}

Now he wants to export Products, Invoices, Employees, etc. It may sound like a lot of work, but so far so good. You just implement a class for each entity and that’s it.

However, one day a customer demanded that every entity should be exported not only in CSV but also in JSON and XML formats. With the current approach, we need 10 more classes. It means three more time of work 😨

If only there was a better way 😞. Turns out, there is😱. Build a bridge 🌉.

This time we know that our customer is a little dirty liar who does not tell us about all his intentions. We know that we will export multiple entities into multiple formats. Those are two different behaviors that can be addressed by different hierarchies of classes:

// abstraction
// responsible for loading data for a specific entity
public interface IExporter
{
File Export()
{
var data = _db.GetData();
var formattedData = _formatter.Format(data);
return File.Export(formattedData);
}
}

// implementation
// responsible for formatting data in a specific format
internal interface IFormatter
{
FormattedData Format(RawData data);
}

IExporter is responsible for getting data from DB, formatting it with corresponding (CSV, JSON, XML) formatter, and writing it to the file. This hierarchy orchestrates other classes, like IFormatter, to do the job. The interface is public, since it should be used by the client code.

On the other hand, IFormatter is a hierarchy of classes that actually implement infrastructure code. The interface is internal since it can not exist on its own without ‘abstraction’ and is not intended to be called outside of it.

This time these two orthogonal functionality can be developed and maintained independently.

The customer asks to export Products, Invoices, and Employees. Not a problem, just add a new exporter:

class ProductExporter  : IExporter { . . . }
class InvoiceExporter : IExporter { . . . }
class EmployeeExporter : IExporter { . . . }

The customer wants to add new formats like JSON and XML. Sure, a new formatter it is:

class JsonFormatter : IFormatter { . . . }
class XmlFormatter : IFormatter { . . . }

The classes on the diagram will have the next look:

This is a great example of why composition is better than inheritance.

With inheritance, adding a new responsibility to the hierarchy of classes would cause us to reimplement the entire hierarchy. The total number of classes could be calculated by this formula:
(Number of Entities * Number of Formatters)

With composition, you just add a new class to an already existing hierarchy
(Number of Entities + Number of Formatters)

Conclusion

The confusion about abstraction and implementation should be over by now 😁. I hope now you have a better understanding of the Bridge pattern and use cases where it can be applied 😉.

As you can see it encourages single responsibility and is quite similar to the beloved strategy pattern.

Notice, that it is not necessary to have multiple classes to implement it. You still can get use of the Bridge, even if your hierarchy has only one class.

For the final, remember to clarify specifications beforehand not to end up in a situation where you need to refactor lots of classes 😅.

Share it 🔄 Clap it 👏 Support ☕️ And follow ✅ You know the order 😉

--

--

iamprovidence

👨🏼‍💻 Full Stack Dev writing about software architecture, patterns and other programming stuff https://www.buymeacoffee.com/iamprovidence