Formal definition from the Gang of Four:
“it’s purpose is to decouple an abstraction from its implementation, so that the two can vary independently”
Code smells for this pattern
- Not following DRY principle i.e. code duplication
- A big class hierarchy with lots of inheritance
Example
Say you’re building a ticket system with two types of tickets
You now have new requirements you need to add discounts for students and seniors. A naive way to approach this is to simply add subclasses to each type of ticket, as follows:
Where in the student and senior subclasses we simply override the GetPrice()
methods to apply the discount.
The problem
The new requirement has meant we have added four more classes to the existing hierarchy. This presents a problem because if we wanted to add further discounts say we would have to add even more subclasses. We began with 2, and now it’s 6. If we had another requirement, using this approach we’d have to add another say 3 classes to each subclass. We’d then have 18 classes in total. As you can see these numbers are getting exponentially bigger. This exponential growth of complexity is exactly what we want to avoid.
Another problem is the duplication of business logic.
In the StudentDayPass
and StudentMonthPass
classes the overridden GetPrice()
method would be exactly the same; applying the exact same discount.
You’d have the same duplication for the senior discount classes.
This is a clear code smell since we are not obeying the DRY principle.
The Bridge pattern
The bridge pattern tells use we should split the hierarchy into two.
To do this we are going to move all discount related functionality to a different hierarchy.
We now add discount as a argument for the abstract class Ticket
.
We can now refactor the Ticket
class a little.
We rename GetPrice()
to GetPriceCore()
which only gets the original price of the ticket. We then write a new GetPrice()
method will calculate the discounted price using GetPriceCore()
and the _discount
property.
Since GetPriceCore()
is only used by derived classes of Ticket
it means we can set it to protected
instead of private
.
Also note that GetPrice()
and GetExpirationDate()
are abstract methods.
This now gives us the following stucture:
Note that the arrow between Ticket
and Discount
is different; it is the arrow for composition.
This is what acts as the “bridge” between the two hierarchies.
If we look at the structure we have now. First thing to note is that we no longer violate the DRY principle. All the business logic to calculate the discounts are found in one place - the derived classes of discount - instead of it being repeated multiple times like before.
Also, we can see that this structure, and by extension the code that follows from this, would be much simpler. We have 5 concrete classes as opposed to the 6 we had before. But more than this, we can now add more types of discount without exponentially increasing the total number of classes. Say for example we had 5 licenses and 5 discounts using our previous approach we would have gotten
5 licenses * 5 discounts = 25 concrete classes.
But using the bridge pattern:
5 licenses + 5 discounts = 10 concrete classes.
We can clearly see that the bridge pattern drastically reduces the number of concrete classes required.
This is the crux of this pattern, it replaces complexity multiplication with complexity addition. Thus it brings under control the expoential growth of complexity.
Recap
Complexity is a function of coupling, where by coupling we mean the number of connection between elements in your code.
The larger the number of connections there are, the greater the complexity of your code.
We can then define the bridge pattern in another way:
It’s purpose is to split a class hierarchy through composition to reduce coupling.
Alternative implementation of the Bridge Pattern
Although we have refactored in the example above we don’t need to stop there. There is still some code duplication that we can refactor out, and we can do this by getting rid of the inheritance and refactoring towards composition.
First, note how in each concrete discount class essentially does the same thing.
What we can do here is instead is make discount an enumeration over all the different types of discount. Then move the logic of the discount into a GetDiscount()
method in Ticket
.
In this method you could simply have a switch statement that based on the value of _discount
would decide what discount to give.
We dont have to stop here though.
You can see that DayPass
and MonthPass
are also very similar.
This means we can convert them to a enum
too.
We would also have to do what we did before, we need to inject this enum
into the Ticket
constructor as _ticketType
.
Further we can now remove GetPrice()
and replace it with GetBasePrice()
to contain the business logic for the ticket prices.
It would simply contain a switch statement containing all the prices based on the value of _ticketType
.
We can also refactor GetExpirationDate
to check the value of _licenseType
and then have a switch statement return the correct date (making use of PurchaseDate
). e.g. for a DayPass
we simply have to do PurchaseDate + 1
.
For a month, PurchaseDate + 30
.
Doing this then allows us to get rid of the concrete classes for DayPass
and MonthPass
Summary
Using the bridge pattern we have done the following:
- Replaced inheritance with composition
- Moved all business logic to the base class