• Backend Weekly
  • Posts
  • Part 7: Observer and Strategy Design Patterns (GoF)

Part 7: Observer and Strategy Design Patterns (GoF)

Today, we will explore the Observer and Strategy Design Patterns under the Behavioural Design Patterns.

In partnership with

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.

The Daily Newsletter for Intellectually Curious Readers

  • We scour 100+ sources daily

  • Read by CEOs, scientists, business owners and more

  • 3.5 million subscribers

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 Observer and Strategy Design Patterns under the Strategy Design Patterns.

Let’s get started quickly.

What is an Observer Pattern?

The Observer pattern defines a one-to-many dependency between objects so that when one object changes state, all its dependents are notified and updated automatically.

The Observer Pattern is useful because it establishes a one-to-many relationship between objects, allowing an object (the subject) to notify other dependent objects (the observers) about changes in its state.

This pattern is particularly beneficial when you want to maintain consistency between related objects or when changes in one object should automatically propagate to others.

As an example, in a chat application, a message broadcast system can notify multiple observers (e.g., UI components, logging services, and notification systems) whenever a new message is received. The message broadcaster does not need to know the details of each observer, promoting loose coupling.

// Subject Interface
class MessageBroadcaster {
    constructor() {
        this.observers = [];
    }

    addObserver(observer) {
        this.observers.push(observer);
    }

    removeObserver(observer) {
        this.observers = this.observers.filter(obs => obs !== observer);
    }

    broadcastMessage(message) {
        console.log(`Broadcasting message: "${message}"`);
        this.observers.forEach(observer => observer.receiveMessage(message));
    }
}

The above code snippet created a MessageBroadcaster class using the observer method. Within this class, we can add, remove, and broadcast messages to different observers.

// Observer Interface
class ChatObserver {
    receiveMessage(message) {
        throw new Error("Method 'receiveMessage()' must be implemented.");
    }
}

// Concrete Observers
class ChatDisplay extends ChatObserver {
    receiveMessage(message) {
        console.log(`ChatDisplay: New message - "${message}"`);
    }
}

class Logger extends ChatObserver {
    receiveMessage(message) {
        console.log(`Logger: Logging message - "${message}"`);
    }
}

The code snippet above creates the ChatObserver class that sends a message to all the objects that implement it. Also, we have created two different components that we want to notify another there’s a message namely ChatDisplay and Logger .

// Client code
const messageBroadcaster = new MessageBroadcaster();

const chatDisplay = new ChatDisplay();
const logger = new Logger();

messageBroadcaster.addObserver(chatDisplay);
messageBroadcaster.addObserver(logger);

messageBroadcaster.broadcastMessage("Hello, World!");  // Output: ChatDisplay: New message - "Hello, World!"
                                                       //         Logger: Logging message - "Hello, World!"

The last code snippet shows how to utilize the observer pattern in your codebase. Here we created the instance of MessageBroadcaster to add all the components that we want to be notified of in every message sent or received.

As you can see, the MessageBroadcaster does not depend on the concrete classes of the observers. It only interacts with them through the common ChatObserver interface. New observers can be added or removed without affecting the message broadcasting logic, making the system highly flexible and maintainable.

Why the Observer Pattern is Useful

The Observer Pattern is particularly beneficial in scenarios where multiple objects need to stay in sync or react to changes in another object. Its main advantages include:

  • Decouples subject from observers: Allows both to vary independently without tight coupling.

  • Automatic state synchronization: Ensures consistent and synchronized state across multiple components.

  • Loose coupling: Promotes flexibility by using a common interface for interactions between subjects and observers.

  • Supports event-driven programming: Facilitates event-based systems where components can subscribe to events and react to changes.

  • Broadcast communication: Allows for efficient communication between one subject and multiple observers.

By using the Observer Pattern, you can build systems that are more flexible, maintainable, and easier to extend, making it an essential pattern for managing dependencies between objects.

What is a Strategy Pattern?

The Strategy Pattern defines a family of algorithms, encapsulates each one, and makes them interchangeable. Strategy lets the algorithm vary independently from the clients that use it.

This allows clients to choose the appropriate algorithm or behavior at runtime without altering the context or client code.

The Strategy Pattern is particularly beneficial when you want to define multiple ways of performing an operation and want to be able to swap out these ways dynamically.

Now let’s take a real-world example of a payment system, and consider an e-commerce application that supports different payment methods such as credit card, PayPal, and Bitcoin.

Using the Strategy Pattern, each payment method can be implemented as a separate strategy, and you can switch between them at runtime based on the user's choice.

First, let’s create a PaymentStrategy class that will inherited by all payment processors and implement the payment method inside.

// Strategy Interface
class PaymentStrategy {
    pay(amount) {
        throw new Error("Method 'pay()' must be implemented.");
    }
}

Next, let’s create individual payment processors such as Paypal, CreditCard, Bitcoin, etc, and extend the PaymentStrategy class. Each of these payment processors overrides the pay method and implements different ways of handling payments.

// Concrete Strategy: Credit Card Payment
class CreditCardPayment extends PaymentStrategy {
    pay(amount) {
        console.log(`Paying $${amount} using Credit Card.`);
    }
}

// Concrete Strategy: PayPal Payment
class PayPalPayment extends PaymentStrategy {
    pay(amount) {
        console.log(`Paying $${amount} using PayPal.`);
    }
}

// Concrete Strategy: Bitcoin Payment
class BitcoinPayment extends PaymentStrategy {
    pay(amount) {
        console.log(`Paying $${amount} using Bitcoin.`);
    }
}

Next, let’s create a PaymentProcessor class to handle the switching between individual payment processors dynamically.

// Context: Payment Processor
class PaymentProcessor {
    setStrategy(strategy) {
        this.strategy = strategy;
    }

    executeStrategy(amount) {
        this.strategy.pay(amount);
    }
}

Lastly, let’s utilize the Strategy Pattern in our codebase to handle payments using different payment processors.

// Client code
const paymentProcessor = new PaymentProcessor();

paymentProcessor.setStrategy(new CreditCardPayment());
paymentProcessor.executeStrategy(100);  // Output: Paying $100 using Credit Card.

paymentProcessor.setStrategy(new PayPalPayment());
paymentProcessor.executeStrategy(200);  // Output: Paying $200 using PayPal.

paymentProcessor.setStrategy(new BitcoinPayment());
paymentProcessor.executeStrategy(300);  // Output: Paying $300 using Bitcoin.

Here, we are just setting the payment processor we want at runtime and calling the executeStrategy method to process each payment.

Each payment method is encapsulated as a separate strategy (CreditCardPayment, PayPalPayment, BitcoinPayment), making it easy to add or change payment methods without modifying the existing context (PaymentProcessor).

The client can choose or switch payment methods dynamically at runtime, making the system more flexible and adaptable to change.

Why the Strategy Pattern is Useful

The Strategy Pattern is particularly beneficial in scenarios where you have multiple ways of performing a task or where behaviors need to be selected dynamically. Its main advantages include:

  • Encapsulates algorithms: Each strategy encapsulates a specific algorithm, making it interchangeable and easy to manage.

  • Promotes Open/Closed Principle: You can extend the functionality of a class by adding new strategies without modifying existing code.

  • Supports dynamic selection of behaviors: Strategies can be selected or switched at runtime based on different conditions or user input.

  • Simplifies testing and maintenance: Each strategy can be tested independently, and changes to one strategy do not affect others.

  • Improves code reusability: Strategies are reusable components that can be used in different contexts or systems.

By using the Strategy Pattern, you can create systems that are more flexible, maintainable, and extensible, making it an essential pattern for managing complex behaviors and algorithms.

That will be all for today. I like to keep this newsletter short.

Today, I discussed the Strategy and Observer Patterns, you learned how to implement them, their similarities, and when to use which.

Next week, I will explore the Command Pattern and Chain of Responsibility Pattern under Behavioural Design Patterns.

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

or to participate.