- Backend Weekly
- Posts
- Part 6: Facade, Bridge, and Decorator Design Patterns (GoF)
Part 6: Facade, Bridge, and Decorator Design Patterns (GoF)
Today, we will explore the Facade, Bridge, and Decorator Design Patterns under the Structural Design Patterns.
Hello “👋
Welcome to another week, another opportunity to become a Great Backend Engineer.
Today’s issue is brought to you by Masteringbackend → A great resource for backend engineers. We offer next-level backend engineering training and exclusive resources.
Before we get started, I have a few announcements:
We recently launched the first modules of three of our courses with a 63% discount for you. Use the coupon code PRESALE at checkout.
Here are what to expect in each course.
10+ in-depth modules
50+ in-depth chapters
160+ high-quality lessons
60+ hours of video training content
Note that the discounted prices increase monthly as we add new modules. Also, you can pick up a single course from our collection on the MB Platform.
Check this out:
For Those Who Seek Unbiased News.
Be informed with 1440! Join 3.5 million readers who enjoy our daily, factual news updates. We compile insights from over 100 sources, offering a comprehensive look at politics, global events, business, and culture in just 5 minutes. Free from bias and political spin, get your news straight.
Now, back to the business of today.
In this series, I will explore Design Patterns, their types, the GoF design patterns, drawbacks, and benefits for backend engineers.
This comes from my new Vue.js book on “Vue Design Patterns”. However, I’m only transferring the knowledge to backend engineers in this series.
Today, we will explore the Facade, Bridge, and Decorator Design Patterns under the Structural Design Patterns.
Let’s get started quickly.
What is a Facade Pattern?
The Facade pattern provides a simplified interface to a complex subsystem, making it easier for clients to interact with that subsystem. It hides the complexities of the underlying system by exposing a unified and easy-to-use interface, reducing the learning curve for developers and the risk of errors.
The Facade Pattern is particularly beneficial when dealing with large, complex systems that have numerous interdependent classes or when integrating legacy systems.
Real-world use case:
Imagine you have a video conversion library that requires multiple steps like file parsing, codec selection, compression, and so on.
Instead of requiring clients to call these low-level methods individually, you can create a facade that provides a simple convert()
method to handle all the complexity internally.
// Complex subsystems
class VideoFile {
constructor(filename) {
this.filename = filename;
}
}
class CodecFactory {
static extract(file) {
console.log(`Extracting codec from ${file.filename}`);
return "Codec";
}
}
class VideoCompressor {
static compress(codec) {
console.log(`Compressing video with ${codec}`);
}
}
class AudioMixer {
static mix() {
console.log("Mixing audio tracks.");
}
}
// Facade
class VideoConverterFacade {
convert(filename) {
const file = new VideoFile(filename);
const codec = CodecFactory.extract(file);
VideoCompressor.compress(codec);
AudioMixer.mix();
console.log(`Video conversion complete for ${filename}`);
}
}
// Client code
const converter = new VideoConverterFacade();
converter.convert("movie.mp4");
Looking at the code snippet above, you can see that we have independent classes handling specific complex processing such as CodecFactory
, VideoCompressor
, AudioMixer
, and VideoFile
.
However, we don’t want developers to have to instantiate and call each class before converting a video each time.
Therefore, we provide the VideoConverterFacade
Facade with a convert
method that calls all the classes and implements all the complexities so you don’t have to do it again but simply call the convert
method to convert a video.
Helps Integrate Legacy Systems
The Facade Pattern can act as an intermediary interface for legacy systems, providing a modern, simplified interface for new clients to interact with the legacy system. This allows for gradual refactoring or replacement of the legacy system without breaking existing functionality.
Let’s take a look at an example: If you have a legacy billing system with a complex API, a facade can offer a simplified API for new clients.
// Legacy billing system
class LegacyBillingSystem {
calculateTotal(items) {
console.log("Calculating total cost.");
return items.reduce((total, item) => total + item.price, 0);
}
generateInvoice(total) {
console.log(`Generating invoice for ${total} USD.`);
}
sendInvoice(invoice) {
console.log(`Sending invoice: ${invoice}`);
}
}
Above is a legacy billing system that we want to support in our system. Therefore, we need to create a facade pattern to help easily integrate or remove the legacy system.
// Facade
class BillingFacade {
constructor() {
this.billingSystem = new LegacyBillingSystem();
}
processBilling(items) {
const total = this.billingSystem.calculateTotal(items);
const invoice = this.billingSystem.generateInvoice(total);
this.billingSystem.sendInvoice(invoice);
}
}
The code snippet above shows a legacy billing system that we still want to support. However, we created a Facade class to help us integrate and support the legacy billing system without having to communicate with it every time.
// Client code
const items = [{ name: 'Item 1', price: 50 }, { name: 'Item 2', price: 30 }];
const billingFacade = new BillingFacade();
billingFacade.processBilling(items);
Here’s how the developer will use the Facade pattern to handle payment and billing everywhere in their code.
The Facade Pattern provides a modern, easy-to-use interface (processBilling()
) for interacting with the legacy billing system. It allows for smooth integration of legacy systems with new clients without changing the legacy code.
Why the Facade Pattern is Useful
The Facade Pattern is beneficial in the following scenarios:
Simplifies complex systems: It provides a unified and simplified interface to interact with complex subsystems.
Reduces coupling: It decouples client code from the intricate details of subsystems, making the system easier to modify and maintain.
Improves readability and maintenance: A single, well-defined interface improves code readability and simplifies maintenance.
Facilitates integration with legacy systems: It allows new clients to interact with legacy systems using a simplified interface.
Provides a high-level interface: The Facade Pattern presents a high-level interface that abstracts away the low-level details of subsystem interactions.
Overall, the Facade Pattern helps manage complexity, improve code quality, and provide a cleaner API for clients to interact with complex subsystems.
Next, let’s look at the Bridge Design Pattern:
What is a Bridge Design Pattern?
The Bridge Pattern is used to decouple an abstraction from its implementation, allowing them to vary independently.
This pattern is especially beneficial when you want to separate different aspects of a class so that both can evolve independently, promoting flexibility, scalability, and reusability.
It’s commonly used when there are multiple dimensions of variations, such as when a class could have several implementations or operate on multiple platforms. The Bridge Pattern helps you avoid a combinatorial explosion of classes by using composition over inheritance.
Real-world example: Drawing Shapes with Different Drawing APIs
Imagine a system that draws shapes like circles and rectangles using different drawing APIs (e.g., a 2D API and a 3D API).
Without the Bridge Pattern, you would need to create separate classes for every combination, such as Circle2D
, Rectangle2D
, Circle3D
, and Rectangle3D
.
With the Bridge Pattern, you can separate the shape (abstraction) from the drawing API (implementation), allowing both to vary independently.
// Implementation interface
class DrawingAPI {
drawCircle(x, y, radius, type) {
console.log("Method must be implemented.");
}
}
// Concrete implementation 1: 2D Drawing API
class DrawingAPI2D extends DrawingAPI {
drawCircle(x, y, radius){
this.drawCircle(x, y, radius, '2d')
console.log(`Drawing 2D Circle at (${x}, ${y},${radius}`);
}
}
// Concrete implementation 2: 3D Drawing API
class DrawingAPI3D extends DrawingAPI {
drawCircle(x, y, radius){
this.drawCircle(x, y, radius, '3d')
console.log(`Drawing 3D Circle at (${x}, ${y},${radius}`);
}
}
The code snippet above separates the implementation by creating the DrawingAPI
with a drawCircle
method to call the external drawing API allowing the two implementations ( DrawingAPI2D
and DrawingAPI3D
)to inherit the drawCircle
method to perform either 2D or 3D drawings.
// Abstraction
class Shape {
constructor(drawingAPI) {
this.drawingAPI = drawingAPI;
}
draw(x,y,radius) {
this.drawingAPI.drawCircle(x, y, radius);
}
}
// Refined Abstraction: Circle Shape
class Circle extends Shape {
constructor(x, y, radius, drawingAPI) {
super(drawingAPI);
this.x = x;
this.y = y;
this.radius = radius;
}
this.draw(this.x, this.y, this.radius)
}
The code snippet above shows the abstraction of different shapes that we can create using the two implementations ( DrawingAPI2D
and DrawingAPI3D
) that we have created above.
With this in place, we can create more shapes using the implementation such as Circle
, Triangle
, etc simply by inheriting the Shape
abstraction.
// Client code
const circle2D = new Circle(5, 10, 15, new DrawingAPI2D());
circle2D.draw(); // Output: Drawing 2D Circle at (5, 10, 15)
const circle3D = new Circle(20, 30, 25, new DrawingAPI3D());
circle3D.draw(); // Output: Drawing 3D Circle at (20, 30, 25)
The code snippet here shows how you will use the pattern above, you can create two varieties of each shape that you implement. For example, here we created the 2D and 3D versions of the Circle
class.
The shape abstraction (e.g., Circle
) is decoupled from the drawing implementation (DrawingAPI2D
or DrawingAPI3D
). You can change the drawing API without affecting the shape class, and vice-versa, enabling independent variation and extension.
Why the Bridge Pattern is Useful
The Bridge Pattern is particularly beneficial in scenarios where you have multiple dimensions of variation or when you want to separate abstraction from implementation to promote flexibility and reusability. Its main advantages include:
Decouples abstraction from implementation: Promotes loose coupling, allowing independent evolution of both parts.
Avoids combinatorial explosion: Prevents the need for numerous classes by separating concerns and using composition over inheritance.
Supports multiple variations: Easily supports multiple implementations, platforms, or algorithms without duplicating code.
Improves flexibility and scalability: The system can be extended in various directions without breaking existing code.
Simplifies testing and maintenance: Facilitates testing and maintenance by reducing dependencies and providing a clear separation of concerns.
By using the Bridge Pattern, you can build systems that are more flexible, scalable, and easier to maintain, making it an essential pattern in software design.
What is a Decorator Design Pattern?
The Decorator Pattern allows you to dynamically add new behavior or responsibilities to an object at runtime without altering its structure.
This pattern provides a flexible alternative to subclassing for extending functionality. Instead of creating numerous subclasses for each possible combination of behaviors, the Decorator Pattern allows you to "wrap" objects with additional functionality as needed, making your code more modular, reusable, and easy to maintain.
Real-world use case:
Suppose you want to send notifications to users via different channels, such as email, SMS, and push notifications. Using the Decorator Pattern, you can create separate decorators for each notification type.
// Component Interface
class Notifier {
send(message) {
console.log("Method 'send()' must be implemented.");
}
}
First, we created a Notifier
class to send any notification to the user. You will need to implement the send
method.
// Abstract Decorator
class NotifierDecorator extends Notifier {
constructor(notifier) {
super();
this.notifier = notifier;
}
send(message) {
this.notifier.send(message);
}
}
Next, I created a NotifierDecorator class which extends the Notifier object with the send
method to send notifications to the user. Now, the important thing here is that the notification that is sent is dependent on the type of Notifier
object that is passed to the constructor.
// Concrete Component
class BasicNotifier extends Notifier {
send(message) {
console.log(`Sending basic notification: ${message}`);
}
}
// Concrete Decorators
class EmailNotifierDecorator extends NotifierDecorator {
send(message) {
super.send(message);
console.log(`Sending email notification: ${message}`);
}
}
class SMSNotifierDecorator extends NotifierDecorator {
send(message) {
super.send(message);
console.log(`Sending SMS notification: ${message}`);
}
}
Next, I created all the different types of notifications that I wanted to send to users. Here, I have created BasicNotifier
, EmailNotifierDecorator
, and SMSNotifierDecorator
. Each of these notification types extends NotifierDecorator
and overrides the send
method.
Except the BasicNotifier
which is a base notifier or default notifier which extends the Notifier
class directly.
// Client code
let notifier = new BasicNotifier();
notifier = new EmailNotifierDecorator(notifier);
notifier = new SMSNotifierDecorator(notifier);
notifier.send("Your order has been shipped.");
Lastly, here is how we use the decorator pattern we have created above to send different notifications to users. We can add as many different types of notifications as possible without changing the decorator.
Each decorator class (EmailNotifierDecorator
, SMSNotifierDecorator
) handles a specific responsibility, such as sending email or SMS, adhering to the Single Responsibility Principle. You can easily add or remove notification channels without modifying the existing notifier classes.
Why the Decorator Pattern is Useful
The Decorator Pattern is particularly beneficial in scenarios where you want to extend the functionality of an object dynamically without modifying its structure. Its main advantages include:
Dynamic behavior modification: Allows you to add or remove behavior dynamically at runtime.
Promotes single responsibility: Each decorator class handles a specific behavior or functionality.
Avoids class explosion: Reduces the number of subclasses needed for different combinations of behaviors.
Supports Open/Closed Principle: You can extend the behavior of objects without modifying their code.
Enables flexible and reusable designs: Decorators can be combined, applied, and reused independently, making the system modular and adaptable.
By using the Decorator Pattern, you can create flexible, maintainable, and easily extensible systems that allow you to extend functionality without modifying the core structure of objects.
That will be all for today. I like to keep this newsletter short.
Today, I discussed the Facade, Bridge, and Decorator Patterns, you learned how to implement them, their similarities, and when to use which.
Next week, I will start exploring the Behavioural Design Patterns starting with Observer Pattern and Strategy Pattern.
Don’t miss it. Share with a friend
Did you learn any new things from this newsletter this week? Please reply to this email and let me know. Feedback like this encourages me to keep going.
See you on Next Week.
Remember to start learning backend engineering from our courses:
Top 5 Remote Backend Jobs this week
Here are the top 5 Backend Jobs you can apply to now.
👨‍💻 Stripe
✍️ Backend / API Engineer, Local Payment Methods
đź“ŤRemote, Dublin, Ireland
đź’° Click on Apply for salary details
Click here to Apply for this role.
👨‍💻 LaunchDarkly
✍️ Backend Engineer - AI
đź“ŤRemote, US
đź’° Click on Apply for salary details
Click here to Apply for this role.
👨‍💻 Pinterest
✍️ Backend Software Engineer
đź“ŤRemote, Worldwide
đź’° Click on Apply for salary details
Click here to Apply for this role.
👨‍💻Sportradar
✍️ Backend Engineer
đź“ŤRemote
đź’° Click on Apply for salary details
Click here to Apply for this role.
Want more Remote Backend Jobs? Visit GetBackendJobs.com
Backend Engineering Resources
Whenever you're ready
There are 4 ways I can help you become a great backend engineer:
1. The MB Platform: Join 1000+ backend engineers learning backend engineering on the MB platform. Build real-world backend projects, track your learnings and set schedules, learn from expert-vetted courses and roadmaps, and solve backend engineering tasks, exercises, and challenges.
2. ​The MB Academy:​ The “MB Academy” is a 6-month intensive Advanced Backend Engineering BootCamp to produce great backend engineers.
3. MB Video-Based Courses: Join 1000+ backend engineers who learn from our meticulously crafted courses designed to empower you with the knowledge and skills you need to excel in backend development.
4. GetBackendJobs: Access 1000+ tailored backend engineering jobs, manage and track all your job applications, create a job streak, and never miss applying. Lastly, you can hire backend engineers anywhere in the world.
LAST WORD đź‘‹
How am I doing?
I love hearing from readers, and I'm always looking for feedback. How am I doing with The Backend Weekly? Is there anything you'd like to see more or less of? Which aspects of the newsletter do you enjoy the most?
Hit reply and say hello - I'd love to hear from you!
Stay awesome,
Solomon
I moved my newsletter from Substack to Beehiiv, and it's been an amazing journey. Start yours here.
Reply