Table of Contents
- Course Overview
- Introduction to Bridge Design Pattern
- The example application: online cinema
- New requirement: rebates
- Summary: the naive approach and its problems
- Bridge Pattern Application
- Summary: Bridge Pattern vs Naive Approach
- The alternative implementation: composition rather than inheritance
- Summary: the alternative implementation
- New requirement: special offers
- Summary and conclusion
1. Course Overview
The Bridge Pattern is one of those design patterns that many developers find quite difficult to understand, and understandably so. The official definition of the Bridge Pattern is quite confusing.
In this course you will learn:
- When to apply the Bridge Pattern: You will see a complete example of the Bridge Pattern in a typical enterprise-level application.
- How to implement the Bridge Pattern: We will first implement a naive approach, then we will simplify it using the Bridge Pattern.
Major Topics Covered
- How to handle overly complicated class hierarchies
- An alternative implementation of the Bridge Pattern
- Why you should prefer composition over inheritance
At the end of this course, you will know everything you need to start applying the Bridge Pattern in your own projects.
Prerequisites
Before starting this course, you should be familiar with the C# programming language.
2. Introduction to Bridge Design Pattern
Section duration: 30m 19s
The official definition and its limits
The Gang of Four, in their book Design Patterns, defined the purpose of the Bridge Pattern as follows:
“Decouple an abstraction from its implementation, so that both can vary independently.”
This definition is confusing because it is not clear what “abstraction” and “implementation” mean in this context. You might think that it’s about interfaces and concrete classes that implement those interfaces, but in reality, it’s not that at all.
Additionally, many examples of this pattern involve desktop user interface elements, such as windows and buttons of different styles. Most developers don’t work on desktop UIs these days, and so it’s hard to relate to such examples.
What you will see in this course
In this course, you will see a complete example of the Bridge Pattern in a typical enterprise-level application: an online cinema. We’ll take one aspect of this cinema — purchasing movie licenses — and see how a naive approach to modeling this aspect leads to overly complicated code. We will then apply the Bridge Pattern to simplify it.
In passing, a definition of the pattern will be proposed which, according to Vladimir Khorikov, better reflects its intention. Finally, towards the end of the course, an alternative implementation of the Bridge Pattern using composition rather than inheritance will be presented.
Proposed alternative definition
Rather than the official Gang of Four definition, here is a clearer definition of the Bridge Pattern:
“Split a class hierarchy by composition to reduce coupling.”
This is exactly the essence of the Bridge Pattern. It allows you to split an overly complicated class hierarchy into two or more smaller, loosely coupled hierarchies that are easier to understand and maintain. This is a way to prevent the combinatorial explosion of the number of classes in an ever-increasing hierarchy.
3. The example application: online cinema
Application overview
The example application we will work on is an online cinema. It consists of a single entity: MovieLicense, which represents a movie purchased by a customer.
Initial structure of the MovieLicense class
In implementation terms, MovieLicense is a base abstract class that contains:
- The title of the purchased film
- Buying time
- Two abstract methods: one which returns the price of the license, and another which indicates when this license expires
There are also two concrete classes that inherit from MovieLicense:
TwoDaysLicense: represents a license that lasts only two daysLifeLongLicense: represents a license that never expires
MovieLicense class code
Here is the base abstract class MovieLicense with its two fields accepted in the constructor and its two abstract methods:
public abstract class MovieLicense
{
public string MovieTitle { get; }
public DateTime PurchaseDate { get; }
protected MovieLicense(string movieTitle, DateTime purchaseDate)
{
MovieTitle = movieTitle;
PurchaseDate = purchaseDate;
}
public abstract decimal GetPrice();
public abstract DateTime? GetExpirationDate();
}
Note: In a real project you would probably want to use a
Movieclass here rather than a simplemovieTitlestring, but for this demo project a simplestringwill suffice.
Initial concrete classes
The two concrete classes implement the abstract methods.
public class TwoDaysLicense : MovieLicense
{
public TwoDaysLicense(string movieTitle, DateTime purchaseDate)
: base(movieTitle, purchaseDate)
{
}
public override decimal GetPrice() => 4m;
public override DateTime? GetExpirationDate() => PurchaseDate.AddDays(2);
}
public class LifeLongLicense : MovieLicense
{
public LifeLongLicense(string movieTitle, DateTime purchaseDate)
: base(movieTitle, purchaseDate)
{
}
public override decimal GetPrice() => 8m;
public override DateTime? GetExpirationDate() => null;
}
TwoDaysLicensecosts $4 and expires in 2 daysLifeLongLicensecosts 8$ and never expires
Main program (app console)
The application is a simple console application. In the main program, we create a TwoDaysLicense and a LifeLongLicense, then we display their details: the title of the film, the price and the validity period of the license:
class Program
{
static void Main(string[] args)
{
var twoDaysLicense = new TwoDaysLicense("The Matrix", DateTime.Now);
var lifeLongLicense = new LifeLongLicense("The Matrix", DateTime.Now);
PrintLicenseDetails(twoDaysLicense);
PrintLicenseDetails(lifeLongLicense);
}
static void PrintLicenseDetails(MovieLicense license)
{
Console.WriteLine($"Movie: {license.MovieTitle}");
Console.WriteLine($"Price: ${license.GetPrice()}");
var expirationDate = license.GetExpirationDate();
if (expirationDate.HasValue)
Console.WriteLine($"Expires on: {expirationDate.Value.ToShortDateString()}");
else
Console.WriteLine("Expires: Never");
Console.WriteLine();
}
}
This is the current state of the project, and it is quite simple. We have a small class hierarchy that models the problem domain well. To better understand the Bridge Pattern, we need to see the challenges it helps us overcome.
4. New requirement: discounts
The request
Let’s say we received a new requirement: we need to introduce discounts into our system. There will be 2 types of discounts:
- A 10% military discount
- A 20% senior discount
This means that TwoDaysLicense and LifeLongLicense can be 10% or 20% cheaper respectively, depending on the type of discount.
Naive approach: extension of the existing hierarchy
How can this requirement be implemented? Well, one way to do that is to keep expanding our class hierarchy. We can add two additional subclasses to each of the concrete classes, as we can see in this diagram:
MovieLicense (abstract)
├── TwoDaysLicense
│ ├── MilitaryTwoDaysLicense ← nouveau
│ └── SeniorTwoDaysLicense ← nouveau
└── LifeLongLicense
├── MilitaryLifeLongLicense ← nouveau
└── SeniorLifeLongLicense ← nouveau
Code of new subclasses
TwoDaysLicense subclasses:
To implement a discount, we overload the GetPrice() method. We multiply the price by 0.9, which means that the price will be 10% cheaper. This class inherits the expiration time from the base class TwoDaysLicense.
public class MilitaryTwoDaysLicense : TwoDaysLicense
{
public MilitaryTwoDaysLicense(string movieTitle, DateTime purchaseDate)
: base(movieTitle, purchaseDate)
{
}
public override decimal GetPrice() => base.GetPrice() * 0.9m; // rabais de 10%
}
public class SeniorTwoDaysLicense : TwoDaysLicense
{
public SeniorTwoDaysLicense(string movieTitle, DateTime purchaseDate)
: base(movieTitle, purchaseDate)
{
}
public override decimal GetPrice() => base.GetPrice() * 0.8m; // rabais de 20%
}
LifeLongLicense subclasses:
public class MilitaryLifeLongLicense : LifeLongLicense
{
public MilitaryLifeLongLicense(string movieTitle, DateTime purchaseDate)
: base(movieTitle, purchaseDate)
{
}
public override decimal GetPrice() => base.GetPrice() * 0.9m; // rabais de 10%
}
public class SeniorLifeLongLicense : LifeLongLicense
{
public SeniorLifeLongLicense(string movieTitle, DateTime purchaseDate)
: base(movieTitle, purchaseDate)
{
}
public override decimal GetPrice() => base.GetPrice() * 0.8m; // rabais de 20%
}
Implementation check
var militaryLifeLong = new MilitaryLifeLongLicense("The Matrix", DateTime.Now);
var seniorTwoDays = new SeniorTwoDaysLicense("The Matrix", DateTime.Now);
PrintLicenseDetails(militaryLifeLong);
// → MilitaryLifeLongLicense, 10% moins chère qu'une LifeLongLicense normale
PrintLicenseDetails(seniorTwoDays);
// → SeniorTwoDaysLicense, 20% moins chère qu'une TwoDaysLicense normale
5. Summary: the naive approach and its problems
In the previous demo, we introduced the discount functionality using the naive approach, extending the existing class hierarchy. We have added four additional classes. Let’s look at the problems this creates.
Problem 1: exponential growth
The issue with this implementation is that it leads to exponential growth of the class hierarchy.
At first we only had 2 concrete classes. After adding the discount feature, the number increases to 6 classes. And that’s only with a few variations of discounts (military and senior). Imagine what would happen if we were to introduce 5 types of discounts instead of 2:
- With 5 discounts and 2 types of licenses → 12 classes
- If we add 1 additional type of license → change from 12 to 18 classes
The following table illustrates growth based on the number of licenses and discounts:
| License Types | Types of discounts | Classes (naive approach) | Classes (Bridge Pattern) |
|---|---|---|---|
| 2 | 0 | 2 | 2 |
| 2 | 2 | 6 | 4 |
| 2 | 5 | 12 | 7 |
| 5 | 5 | 25 | 10 |
| 10 | 10 | 100 | 20 |
The formula for the naive approach is: number_of_classes = number_licenses × number_discounts
The growth is therefore multiplicative (and therefore exponential with the addition of new dimensions).
Problem 2: Business logic duplication
Looking at the two variations of the military license, we see that the knowledge of how to calculate the military discount is present in both classes:
// Dans MilitaryTwoDaysLicense
public override decimal GetPrice() => base.GetPrice() * 0.9m;
// Dans MilitaryLifeLongLicense
public override decimal GetPrice() => base.GetPrice() * 0.9m;
This violates the DRY (Don’t Repeat Yourself) principle. The same duplication is found for senior licenses.
Issue 3: Adding more features makes the problem worse
Additionally, new features will also contribute to exponential growth. For example, let’s say we want to introduce special offers and extend the duration of the TwoDaysLicense by two additional days. For this, we should introduce even more classes:
// Exemple de la classe SpecialOffer dans l'approche naïve
public class SpecialOfferMilitaryTwoDaysLicense : MilitaryTwoDaysLicense
{
public SpecialOfferMilitaryTwoDaysLicense(string movieTitle, DateTime purchaseDate)
: base(movieTitle, purchaseDate)
{
}
public override DateTime? GetExpirationDate()
{
var baseDate = base.GetExpirationDate();
return baseDate.HasValue ? baseDate.Value.AddDays(2) : null;
}
}
And these new classes should inherit from all existing subclasses of TwoDaysLicense. The number of classes explodes even more.
Summary of problems with the naive approach
- Combinatorial explosion: The number of classes grows multiplicatively, even exponentially, with the addition of new variations or dimensions.
- Violation of the DRY principle: The business logic (e.g.: calculating a discount) is duplicated in several classes.
- Maintenance difficulty: Modifying an existing behavior (eg: changing the percentage of a discount) requires modifying several classes.
- Hierarchy difficult to understand: The class hierarchy quickly becomes incomprehensible and difficult to navigate.
6. Application of the Bridge Pattern
The central idea
To apply the Bridge Pattern and simplify our overly complicated class hierarchy, we will introduce a separate class hierarchy called Discount, and we will move all discount-related functionality to this hierarchy.
This creates two distinct hierarchies connected by a “bridge” (bridge) — the reference that MovieLicense has to Discount:
Hiérarchie 1 (Licences) Hiérarchie 2 (Rabais)
-------------------------- -----------------------
MovieLicense (abstract) Discount (abstract)
├── TwoDaysLicense ─────────► ├── NoDiscount
└── LifeLongLicense (pont) ├── MilitaryDiscount
└── SeniorDiscount
Step 1: Creating the Discount hierarchy
We create a new public abstract class Discount with a single abstract method GetDiscount(). Note that its return type is int and not decimal — we’re using percentages here, and will convert them to a multiplier later.
public abstract class Discount
{
public abstract int GetDiscount();
}
Next, we create the concrete classes. NoDiscount will be our default option for no-discount licenses — it returns 0 as the discount percentage:
public class NoDiscount : Discount
{
public override int GetDiscount() => 0;
}
public class MilitaryDiscount : Discount
{
public override int GetDiscount() => 10;
}
public class SeniorDiscount : Discount
{
public override int GetDiscount() => 20;
}
This code can be shortened using expression-bodied methods, as above (=> value syntax).
Step 2: The Null Object Pattern with NoDiscount
Important note: We could pass
nullinstead of theNoDiscountclass for licenses without discounts. But it’s best to use the Null Object Pattern whenever possible, so you don’t have to deal with the special case of a null value. TheNoDiscountclass is an example of this Null Object Pattern. It does nothing and only serves as a neutral value to satisfy the manufacturer’s signature of the license.
Step 3: Modifying the MovieLicense class
We now accept the abstract class Discount in the MovieLicense constructor and save it in a private read-only field (readonly):
public abstract class MovieLicense
{
public string MovieTitle { get; }
public DateTime PurchaseDate { get; }
private readonly Discount _discount;
protected MovieLicense(string movieTitle, DateTime purchaseDate, Discount discount)
{
MovieTitle = movieTitle;
PurchaseDate = purchaseDate;
_discount = discount;
}
// Méthode publique non-abstraite qui calcule le prix avec rabais
public decimal GetPrice()
{
decimal basePrice = GetPriceCore();
int discountPercentage = _discount.GetDiscount();
decimal multiplier = 1 - discountPercentage / 100m;
return basePrice * multiplier;
}
// L'ancienne méthode abstraite GetPrice() est renommée GetPriceCore()
protected abstract decimal GetPriceCore();
public abstract DateTime? GetExpirationDate();
}
Important points:
- Old abstract method
GetPrice()is renamed toGetPriceCore() - A new non-abstract method
GetPrice()is created, which usesGetPriceCore()internally - New
GetPrice()first gets the discount value from the_discountfield, then applies it to the base price
Step 4: Simplifying Concrete Classes
The TwoDaysLicense and LifeLongLicense classes are now simplified — they no longer need to override GetPrice() to handle discounts. They simply implement GetPriceCore() and GetExpirationDate():
public class TwoDaysLicense : MovieLicense
{
public TwoDaysLicense(string movieTitle, DateTime purchaseDate, Discount discount)
: base(movieTitle, purchaseDate, discount)
{
}
protected override decimal GetPriceCore() => 4m;
public override DateTime? GetExpirationDate() => PurchaseDate.AddDays(2);
}
public class LifeLongLicense : MovieLicense
{
public LifeLongLicense(string movieTitle, DateTime purchaseDate, Discount discount)
: base(movieTitle, purchaseDate, discount)
{
}
protected override decimal GetPriceCore() => 8m;
public override DateTime? GetExpirationDate() => null;
}
Step 5: Remove redundant subclasses
We can now delete all the subclasses created with the naive approach:
MilitaryTwoDaysLicense→ removedSeniorTwoDaysLicense→ removedMilitaryLifeLongLicense→ removedSeniorLifeLongLicense→ removed
The final class hierarchy with the Bridge Pattern is:
MovieLicense (abstract)
├── TwoDaysLicense
└── LifeLongLicense
Discount (abstract)
├── NoDiscount
├── MilitaryDiscount
└── SeniorDiscount
Use with the Bridge Pattern
// Licence sans rabais (utilisation du Null Object NoDiscount)
var regularLicense = new TwoDaysLicense("The Matrix", DateTime.Now, new NoDiscount());
// Licence avec rabais militaire (10%)
var militaryLicense = new LifeLongLicense("The Matrix", DateTime.Now, new MilitaryDiscount());
// Licence avec rabais senior (20%)
var seniorLicense = new TwoDaysLicense("The Matrix", DateTime.Now, new SeniorDiscount());
7. Summary: Bridge Pattern vs Naive Approach
What has changed
We divided our class hierarchy into two: one containing the initial functionality with movie license types, and another containing the discount types.
This division allowed us to overcome the two problems of the naive implementation.
Problem 1 resolved: the DRY principle is respected
All business rules relating to the calculation of discounts are now located in the same place: the classes of the Discount hierarchy. There is no more duplication.
Problem 2 solved: growth is now linear
Note that in our particular case, the number of classes remains approximately the same: 5 concrete classes in the new version, against 6 in the naive implementation. But that’s only because the number of licenses and discount variations is not very large.
If we had 5 types of licenses and 5 types of discounts:
- Naive approach:
5 × 5 = 25 classes - Bridge Pattern:
5 + 5 = 10 classes
This is exactly the essence of this pattern. It allows you to replace the multiplication of complexity with addition, and thus to control the exponential growth of this complexity. In other words, it helps avoid complexity explosion.
The key concept: coupling and complexity
Complexity is a function of coupling, which is the number of connections between different aspects of your code. The larger this number, the higher the complexity.
In our case, we have two aspects:
- License types
- Types of discounts
In the naive approach: each variation of one aspect connects to each variation of the other aspect → multiplication of the number of connections.
In the Bridge Pattern approach: the two hierarchies are split. Each license type connects to a single Discount abstraction (the bridge), regardless of the specific discount → addition of the number of connections.
In summary: The Bridge Pattern allows to go from O(n × m) to O(n + m) in terms of the number of classes necessary to cover all combinations of two independent dimensions.
8. The alternative implementation: composition rather than inheritance
Another example to better understand
To better describe the alternative implementation, let’s look at another example. Here is an Order class with two states: PaymentStatus and DeliveryStatus.
PaymentStatuscan be:AwaitingPayment,PaidorPaymentFailedDeliveryStatuscan be:NotShipped,ShippedorDelivered
This class looks like our current application. These two statuses also represent different aspects of an order, just like film licensing and discount in our project.
Naive approach: a giant enumeration
You might want to combine payment and delivery statuses into one giant enumeration:
public enum OrderStatus
{
AwaitingPayment_NotShipped,
AwaitingPayment_Shipped,
AwaitingPayment_Delivered,
Paid_NotShipped,
Paid_Shipped,
Paid_Delivered,
PaymentFailed_NotShipped,
PaymentFailed_Shipped,
PaymentFailed_Delivered
}
As you can see, the elements of the enumeration now represent all possible combinations of the two statuses — exactly the same drawbacks as our naive implementation.
Bridge Pattern approach: two separate enumerations
Splitting the enumeration in two, just as we split the MovieLicense class hierarchy into two hierarchies, prevents uncontrollable growth:
public enum PaymentStatus
{
AwaitingPayment,
Paid,
PaymentFailed
}
public enum DeliveryStatus
{
NotShipped,
Shipped,
Delivered
}
public class Order
{
public PaymentStatus PaymentStatus { get; set; }
public DeliveryStatus DeliveryStatus { get; set; }
// ...
}
The difference here is that we deal with composition rather than inheritance. And in fact, we don’t have to stop at inheritance in our example project either — we can also refactor towards composition.
Refactoring towards composition in our cinema project
We can replace the entire class hierarchy with enumerations, then we can comment out the existing Discount classes.
Discount enumeration
public enum Discount
{
None, // émule la classe NoDiscount
Military,
Senior
}
The LicenseType enumeration
public enum LicenseType
{
TwoDays,
LifeLong
}
MovieLicense class refactored with composition
All business logic is now concentrated in the MovieLicense class itself. We add a new private method GetDiscount() which reviews all possible discount types and returns the appropriate number.
public class MovieLicense
{
public string MovieTitle { get; }
public DateTime PurchaseDate { get; }
public LicenseType LicenseType { get; }
public Discount Discount { get; }
public MovieLicense(string movieTitle, DateTime purchaseDate,
LicenseType licenseType,
Discount discount = Discount.None)
{
MovieTitle = movieTitle;
PurchaseDate = purchaseDate;
LicenseType = licenseType;
Discount = discount;
}
public decimal GetPrice()
{
decimal basePrice = GetBasePrice();
int discountPercentage = GetDiscount();
decimal multiplier = 1 - discountPercentage / 100m;
return basePrice * multiplier;
}
private decimal GetBasePrice()
{
return LicenseType switch
{
LicenseType.TwoDays => 4m,
LicenseType.LifeLong => 8m,
_ => throw new ArgumentOutOfRangeException()
};
}
private int GetDiscount()
{
return Discount switch
{
Discount.None => 0,
Discount.Military => 10,
Discount.Senior => 20,
_ => throw new ArgumentOutOfRangeException()
};
}
public DateTime? GetExpirationDate()
{
return LicenseType switch
{
LicenseType.TwoDays => PurchaseDate.AddDays(2),
LicenseType.LifeLong => null,
_ => throw new ArgumentOutOfRangeException()
};
}
}
Use with composition
// Licence sans rabais
var regularLicense = new MovieLicense("The Matrix", DateTime.Now, LicenseType.TwoDays);
// Licence avec rabais militaire
var militaryLicense = new MovieLicense("The Matrix", DateTime.Now,
LicenseType.LifeLong, Discount.Military);
// Licence avec rabais senior
var seniorLicense = new MovieLicense("The Matrix", DateTime.Now,
LicenseType.TwoDays, Discount.Senior);
9. Summary: the alternative implementation
In the previous demo, we replaced the two class hierarchies with two enumerations and moved all the business logic for these hierarchies into the MovieLicense base class. This is essentially an alternative implementation of the Bridge Pattern.
We could debate whether this is still a Bridge Pattern in the strict sense. But even if this is no longer the case, it is a natural extension of the Bridge Pattern.
The important rule: prefer composition to inheritance
Guideline: You should prefer composition over inheritance — Prefer composition over inheritance.
This is exactly what we did. We have moved from inheritance to composition. Here’s why you should favor composition:
-
More flexible: With composition, it is easier to change behavior on the fly by injecting different parts of that behavior into the movie license.
-
Inheritance is rigid: Most languages don’t let you derive from a single type — there is no multiple inheritance in C#. With composition, this constraint disappears.
-
Easier to understand: It is more natural to see the different components of domain classes at a glance, rather than trying to understand a complex inheritance hierarchy.
Of course, sometimes inheritance is more appropriate than composition, but in most cases, like the one shown in this sample project, composition works better.
10. New requirement: special offers
Illustrate ease of extension with composition
To illustrate how easily scalable this compounding solution is, let’s reintroduce the special offers requirement — the one we briefly looked at at the beginning of the module.
This feature adds two additional days to any non-zero expiration date.
How this would have been implemented with the naive approach
Here is the class that provided this functionality in the naive approach — it inherited all existing subclasses of TwoDaysLicense:
// Exemple : SpecialOffer pour une licence militaire deux jours (approche naïve)
public class SpecialOfferMilitaryTwoDaysLicense : MilitaryTwoDaysLicense
{
public SpecialOfferMilitaryTwoDaysLicense(string movieTitle, DateTime purchaseDate)
: base(movieTitle, purchaseDate)
{
}
public override DateTime? GetExpirationDate()
{
var baseDate = base.GetExpirationDate();
return baseDate.HasValue ? baseDate.Value.AddDays(2) : null;
}
}
With the naive approach, we should have created such a class for each existing combination of license type and rebate. The combinatorial explosion would continue.
Implementation with composition: only one additional enumeration
With composition, simply add a new SpecialOffer enumeration and inject it into MovieLicense:
public enum SpecialOffer
{
None,
TwoDaysExtension
}
We then modify the MovieLicense class to take this new field into account. We save SpecialOffer in a private field:
public class MovieLicense
{
public string MovieTitle { get; }
public DateTime PurchaseDate { get; }
public LicenseType LicenseType { get; }
public Discount Discount { get; }
public SpecialOffer SpecialOffer { get; }
public MovieLicense(string movieTitle, DateTime purchaseDate,
LicenseType licenseType,
Discount discount = Discount.None,
SpecialOffer specialOffer = SpecialOffer.None)
{
MovieTitle = movieTitle;
PurchaseDate = purchaseDate;
LicenseType = licenseType;
Discount = discount;
SpecialOffer = specialOffer;
}
// ...GetPrice() reste identique...
public DateTime? GetExpirationDate()
{
DateTime? baseDate = GetBaseExpirationDate();
TimeSpan extension = GetSpecialOfferExtension();
return baseDate.HasValue ? baseDate.Value.Add(extension) : null;
}
private DateTime? GetBaseExpirationDate()
{
return LicenseType switch
{
LicenseType.TwoDays => PurchaseDate.AddDays(2),
LicenseType.LifeLong => null,
_ => throw new ArgumentOutOfRangeException()
};
}
private TimeSpan GetSpecialOfferExtension()
{
return SpecialOffer switch
{
SpecialOffer.None => TimeSpan.Zero,
SpecialOffer.TwoDaysExtension => TimeSpan.FromDays(2),
_ => throw new ArgumentOutOfRangeException()
};
}
// ...GetBasePrice() et GetDiscount() restent identiques...
}
What changed:
- The
GetExpirationDate()method extracts existing code to a private methodGetBaseExpirationDate() - A new private method
GetSpecialOfferExtension()returns the extension according to the value ofSpecialOffer - It either returns no extension (
TimeSpan.Zero), or an extension of two days (TimeSpan.FromDays(2)), or throws an exception if the value of the enumeration is unknown - We save the
baseDatein a variable, we get the extension, and we return the base date plus this extension as the final expiration date
Usage
// Licence normale deux jours
var normalLicense = new MovieLicense("The Matrix", DateTime.Now, LicenseType.TwoDays);
// → expire dans 2 jours
// Licence avec offre spéciale (extension de 2 jours)
var specialLicense = new MovieLicense("The Matrix", DateTime.Now,
LicenseType.TwoDays,
Discount.None,
SpecialOffer.TwoDaysExtension);
// → expire dans 4 jours
After adding and printing this new license, the application displays 4 days of validity instead of 2.
Comparison of the complexity of adding a new feature
| Approach | What to do to add SpecialOffer |
|---|---|
| Naive | Create N new subclasses (one for each existing combination) |
| Bridge Pattern (legacy) | Create 1 new SpecialOffer hierarchy and link the classes |
| Bridge Pattern (composition) | Add 1 new enumeration + modify 1 method. That’s it. |
11. Summary and conclusion
What you learned in this course
The problem
When you have a class whose behavior varies along several independent dimensions (like license types and discount types), the naive approach is to create subclasses for each combination. This leads to a combinatorial explosion in the number of classes, duplication of business logic, and code that is difficult to maintain.
The solution: the Bridge Pattern
The Bridge Pattern solves this problem by dividing the class hierarchy into two (or more) separate hierarchies, and connecting them via a reference — the “bridge”. This turns complexity multiplication into simple addition.
The alternative: composition
To go even further in reducing complexity and increasing flexibility, we can replace class hierarchies with enumerations and move all business logic into the main class.
Revised Bridge Pattern Definition
The definition of the Gang of Four:
“Decouple an abstraction from its implementation, so that both can vary independently.”
The definition proposed in this course, which better reflects the real intention of the pattern:
“Split a class hierarchy by composition to reduce coupling.”
The three dimensions of MovieLicense
At the end of the course, our MovieLicense is characterized by three decoupled dimensions represented by three enumerations:
// Trois aspects indépendants de MovieLicense
public enum LicenseType { TwoDays, LifeLong }
public enum Discount { None, Military, Senior }
public enum SpecialOffer { None, TwoDaysExtension }
These three enumerations represent three different aspects of the MovieLicense domain class. They can be seen as three dimensions that are loosely coupled to each other. One could go even further and use the Enumeration Pattern (or Smart Enum) instead of simple enumeration values, but that is outside the scope of this course.
Summary table of approaches
| Criterion | Naive approach | Bridge Pattern (legacy) | Bridge Pattern (composition) |
|---|---|---|---|
| Number of classes | Exponential O(n×m) | Linear O(n+m) | Minimal |
| DRY principle | ✗ Violated | ✓ Respected | ✓ Respected |
| Flexibility | Low | Average | High |
| Readability | Difficult | Good | Excellent |
| Ease of expansion | Difficult | Easy | Very easy |
| Multiple inheritance constraint | ✗ Present | ✗ Present | ✓ Absent |
When to use the Bridge Pattern?
Use the Bridge Pattern when:
- You have a class whose behavior can vary according to several independent dimensions
- You see your class hierarchy growing multiplicatively
- You see code duplication between subclasses
- You want to be able to edit variations independently of each other
- You want to facilitate the future extension of the application with new dimensions
Final Guiding Principle
Prefer composition over inheritance — Prefer composition over inheritance. Composition is more flexible, easier to modify when necessary, and more natural to understand. Inheritance is more rigid because most languages don’t let you derive from more than one type. There are certainly cases where inheritance is more appropriate, but in most cases, like the example in this course, composition works better.
Search Terms
c-sharp · design · patterns · bridge · testing · architecture · c# · .net · development · pattern · composition · approach · naive · movielicense · alternative · class · enumeration · application · definition · cinema · classes · complexity · concrete · discount