How to Avoid Exponential Growth of Classes by Applying Bridge Pattern in C#
I wrongly designed my code which could lead to the multiplication classes. Then I was introduced to Bridge Pattern and fixed my code design.
Photo by Alex Knight on Unsplash
The Gang of Four Definition
The Bridge Pattern was introduced to the public in the book Design Patterns: Elements of Reusable Object-Oriented Software by Gamma Erich, Helm Richard, Johnson Ralph, Vlissides John, also known as the Gang of Four.
The definition — “Its purpose is to decouple an abstraction from its implementation so that two can vary independently.”
It can be difficult to understand because the explanation is quite confusing by itself. It is not completely obvious what abstraction and implementation stand for. Some can say that it is about interfaces and concrete implementations, but it is not. Let me show you why.
To fully understand the power of the Bridge Pattern, I recommend you follow my tutorial. It was made with an emphasis on real enterprise application aspects. After finishing this tutorial, you will be ready to recognize in time, if your code design is leading to an exponential growth of complexity and understand, how to use the Bridge Pattern to avoid it.
Tutorial
The code for this tutorial can be found on my GitHub.
Sample Application Introduction
The main sense of application is to provide the text report with time-tracked records of employees to their boss.
TimeEntry class
is representing the record with a tracked time of an employee. It is an abstract class
with an abstract method GetBossReportRow()
.
SpecificRangeTimeEntry
is derived from the TimeEntry class
. It is overriding the GetBossReportRow()
method to return the string
representation of time entry. It is representing the records; which time tracking began and end at a specific time.
public override string GetBossReportsRow() => $"The employee {EmployeeName} was {Description} from {StartTime} to {StopTime}";
DurationOnlyTimeEntry class
is also derived from the TimeEntry class
. It returns the boss report row in a different format. This class
is used to records, where we know only the duration, no specific dates.
public override string GetBossReportsRow() => $"The employee {EmployeeName} was {Description} for duration {TimeInterval}";
Finally, the report is built and printed into the console in the Main
function of Program class
.
New Requirement
The new requirements for an application arrived at our desk. Boss would like to include the information of who approved or banned the concrete time entry.
So the report row for specific time range records should look like this — The employee {EmployeeName} was {Description} from {StartTime} to {StopTime} and was approved/banned by authority {AuthorityName}.
And report row for duration only records should look like this — The employee {EmployeeName} was {Description} for duration {TimeInterval} and was approved/banned by authority {AuthorityName}.
Naive Approach
Firstly, let's implement the new requirement without the knowledge of the Bridge Pattern. Implementation of this approach can be found in the NaiveApproach folder.
I created four new classes. Two inherits from DurationOnlyTimeEntry
and two from SpecificRangeTimeEntry
. Each new class contains a property named AuthorityName
. The property will store the name of the authority that approved or banned the Time Entry. You can also see that I override the GetBossReportRow
method again, to format the message into the required form.
public class ApprovedDurationOnlyTimeEntry : DurationOnlyTimeEntry
{
public string AuthorityName { get; }
public ApprovedDurationOnlyTimeEntry(string description, TimeSpan timeInterval, string employeeName, string authorityName) : base(description, timeInterval, employeeName)
{
AuthorityName = authorityName;
}
public override string GetBossReportsRow() => $"{base.GetBossReportsRow()} and was approved by authority {AuthorityName}";
}
That’s it. We are ready to go. Let's create a few instances of TimeEntry
and print them into the console.
var approvedDurationOnlyTimeEntry = new Bridge.Pattern.NaiveApproach.ApprovedDurationOnlyTimeEntry("bugfixing", TimeSpan.FromHours(2), "Roger", "Karen");
var bannedDurationOnlyTimeEntry = new Bridge.Pattern.NaiveApproach.BannedDurationOnlyTimeEntry("smoke breaking", TimeSpan.FromHours(2), "Roger", "Karen");
var approvedSpecificRangeTimeEntry = new Bridge.Pattern.NaiveApproach.ApprovedSpecificTimeRangeTimeEntry("creating new feature", "Levi", DateTime.Now.AddHours(-1), DateTime.Now, "Lucas");
var bannedSpecificTimeRangeTimeEntry = new Bridge.Pattern.NaiveApproach.BannedSpecificTimeRangeTimeEntry("toilet breaking", "Levi", DateTime.Now.AddHours(-1), DateTime.Now, "Lucas");
Console.Write(CreateBossReport(approvedDurationOnlyTimeEntry, bannedDurationOnlyTimeEntry, approvedSpecificRangeTimeEntry, bannedSpecificTimeRangeTimeEntry));
Now a few words about what is wrong with this approach. This implementation leads to the exponential growth of the class hierarchy. In the beginning, we had two concrete classes, DurationOnlyTimeEntry
and SpecificRangeTimeEntry
. After the introduction of a new feature, the number of concrete classes grows to six.
Imagine what would happen when we would like to introduce another type of authority act. The answer is that the count of classes would grow by another two. Also, when we would define another type of Time Entry, the count of classes would grow too. Lastly, the additional features will also contribute to exponential growth.
Another problem of this approach is the duplication of property AuthorityName
in four concrete classes. This violates the Don’t Repeat Yourself principle, also recognized as DRY. There is also duplication of the content of the method GetBossReportRow
in classes starts with Approve..
public class ApprovedSpecificTimeRangeTimeEntry : SpecificRangeTimeEntry
{
public string AuthorityName { get; }
public ApprovedSpecificTimeRangeTimeEntry(string description, string employeeName, DateTime startTime, DateTime stopTime, string authorityName)
: base(description, employeeName, startTime, stopTime)
{
AuthorityName = authorityName;
}
public override string GetBossReportsRow() => $"{base.GetBossReportsRow()} and was approved by authority {AuthorityName}";
}
public class ApprovedDurationOnlyTimeEntry : DurationOnlyTimeEntry
{
public string AuthorityName { get; }
public ApprovedDurationOnlyTimeEntry(string description, TimeSpan timeInterval, string employeeName, string authorityName) : base(description, timeInterval, employeeName)
{
AuthorityName = authorityName;
}
public override string GetBossReportsRow() => $"{base.GetBossReportsRow()} and was approved by authority {AuthorityName}";
}
That was enough dirt we throw at the naive approach. Now we will implement the Bridge Pattern.
The Bridge Pattern Approach
Now forget for a moment what we implemented in the Naive Approach chapter and let’s get back to the state of the application, before any changes. It will be our starting point. Implementation of this approach can be found in the BridgePatternApproach folder.
We will implement the new requirements using the Bridge Pattern. Instead of extending the TimeEntry
hierarchy, we will introduce another one, the Authority
hierarchy.
I created the new abstract class
named Authority
. Our attention deserves the GetAuthorityVerb
method, which is overwritten by child classes ApprovingAuthority
and BanningAuthority
afterward. The method returns the string
representation of the authority act.
public class ApprovingAuthority : Authority
{
public ApprovingAuthority(string name) : base(name) { }
public override string GetAuthorityVerb() => "approved";
}
public class BanningAuthority : Authority
{
public BanningAuthority(string name) : base(name) { }
public override string GetAuthorityVerb() => "banned";
}
Then, I added an Authority
parameter to the TimeEntry
constructor, therefore I made the GetBossReportRow
method non-abstract, and finally, I created a new abstract GetBossReportRowCore
method. This is the body of the GetBossReportRow
method.
public string GetBossReportRow()
=> $"{GetBossReportRowCore()} was {Authority.GetAuthorityVerb()} by authority {Authority.Name}";
Now, the TimeEntry class
does is missing the “who” and “how long” report message part. This part is now implemented in an overwritten GetBossReportRowCore
method in derived classes.
public class DurationOnlyTimeEntry : TimeEntry
{
...
public override string GetBossReportRowCore()
=> $"The employee {EmployeeName} was {Description} for duration {TimeInterval}";
}
public class SpecificRangeTimeEntry : TimeEntry
{
...
public override string GetBossReportRowCore()
=> $"The employee {EmployeeName} was {Description} from {StartTime} to {StopTime}";
}
This, when I think of it, suits more since this part of the message differs in “how long” information and that is exactly why SpecificRangeTimeEntry
and DurationOnlyTimeEntry
classes exist. In other words, we are not violating the Single Responsibility Principle (SRP).
Let’s create a few instances of TimeEntry
and print them into the console again.
var approvedDurationOnlyTimeEntry =
new DurationOnlyTimeEntry("bugfixing", TimeSpan.FromHours(2), "Roger", new ApprovingAuthority("Karen"));
var bannedDurationOnlyTimeEntry =
new DurationOnlyTimeEntry("taking smoke break", TimeSpan.FromHours(2), "Roger", new BanningAuthority("Karen"));
var approvedSpecificRangeTimeEntry =
new SpecificRangeTimeEntry("creating new feature", "Levi", DateTime.Now.AddHours(-1), DateTime.Now, new ApprovingAuthority("Lucas"));
var bannedSpecificTimeRangeTimeEntry =
new SpecificRangeTimeEntry("taking toilet break", "Levi", DateTime.Now.AddHours(-1), DateTime.Now, new BanningAuthority("Lucas"));
Console.Write(CreateBossReport(approvedDurationOnlyTimeEntry, bannedDurationOnlyTimeEntry, approvedSpecificRangeTimeEntry, bannedSpecificTimeRangeTimeEntry));
We managed to get the same result with much less code and classes.
Summary
We have split our hierarchy in two. One contains the initial functionality and the other one contains the types of authorities. This split helped us to overcome issues with the naive approach.
All the business rules of how to format the message when authority acted are in one place which are the classes of Authority hierarchy. Thanks to that there is no violation of the DRY principle as in the naive approach.
The number of concrete classes in the example stayed the same, but that is just because the number of time entry types and authority acts is in our example small. If there is five-time entry types and 3 authority act types, then the number of classes will be multiplied:
Naive approach: 5 Time Entry Types 3 Authority Act Types = *15 classes
Because we are not using inheritance and we split hierarchies, we can do add operation instead of multiplication.
Bridge Pattern approach: 5 Time Entry Types + 3 Authority Act Types = 8 classes
The Bridge Pattern replaces complexity multiplication with complexity addition and threw that brings the growth of complexity under control. Complexity is a function of coupling which is the number of connections between aspects of our code. The larger that number, the higher the complexity. In our case, we have two such aspects.
- The number of employee’s time measurement types.
- The number of possible acts with time entries by authorities.
In the naive approach, each variation of one aspect is connected to each variation of the other. Which resulted in the multiplication of the number of connections.
During the implementation of the Bridge Pattern, we grouped all the variations of each aspect. They are connected only to their base class. And then we introduce another connection between the base classes, which can be named the Bridge.
We isolated the aspects from each other therefore we achieved high cohesion among elements of each aspect and loose coupling between them.
We replaced the connection multiplication with connection addition. The introduction of new aspects or variations of existing ones will not lead to the exponential growth of complexity anymore. Therefore, I find the origin definition confusing.
The origin definition: “Its purpose is to decouple an abstraction from its implementation so that two can vary independently.”
It is not about abstraction nor implementation. It is more about the reduction of coupling and splitting the class hierarchies. This definition sounds more precise to me:
The new definition: “Its purpose is to split a class hierarchy through composition to reduce coupling.”
Sources Pluralsight course by Vladimir Khorikov.