Skip to main content

Command Palette

Search for a command to run...

SOLID Principles: Open Closed Principle

Published
5 min read
SOLID Principles: Open Closed Principle
M

Building Pyramids of Code: A Tech Leader from Cairo

I hail from the land of the Great Pyramids, where I've traded ancient wonders for the ever-evolving marvels of software. As Head of Software Engineering, my days are a captivating blend of the Nile's serenity and the frenetic energy of building cutting-edge tech solutions.

Cairo's vibrant tech scene is my playground. I've navigated its bustling hubs, honed my skills at startups and consultancies, and witnessed firsthand the power of technology to transform lives. This journey has instilled in me a deep understanding of the region's unique needs and opportunities.

My leadership philosophy? Think straight-talking Sphinx – I guide with unwavering honesty, challenging directly while caring fiercely. No sugarcoating, no backstabbing, just clear feedback and genuine support. We build pyramids of code, not walls of BS.

My team thrives in this arena of radical candor. We rip through roadblocks with open communication, celebrate wins loudly, and learn from failures even faster. It's messy, sometimes brutal, but always authentic. Because in the crucible of challenging directly, true brilliance emerges. I empower my teams to unleash their creativity, fostering a culture of innovation where ideas flow like the Nile. We tackle complex challenges, from scaling platforms to crafting intuitive interfaces, all while embracing the dynamic spirit of Cairo.

Introduction

Imagine software that embraces change effortlessly, evolving gracefully without breaking under the weight of modifications. That's the essence of the Open-Closed Principle (OCP) in SOLID design.

Here's the key idea:

  • Open for extension: Your code should welcome new features and adaptations without requiring modifications to its existing structure.

  • Closed for modification: Once a class or module is thoroughly tested and working flawlessly, it should remain sealed off from direct changes.

How does OCP achieve this magic?

  • Abstractions to the Rescue: Rely on interfaces and abstract classes to define contracts for behavior, allowing concrete implementations to vary without disrupting the core structure.

  • Delegation for Flexibility: Employ techniques like factories and dependency injection to select and manage concrete implementations at runtime, fostering adaptability.

Benefits of Embracing OCP:

  • Reduced Risk of Bugs: By minimizing code changes, you lower the chances of introducing new errors and maintain stability.

  • Improved Maintainability: Code that adheres to OCP becomes easier to understand, navigate, and modify over time.

  • Enhanced Flexibility: Your software can elegantly accommodate new features and requirements without disrupting its core foundation.

  • Promotes Testability: Dependency injection and loose coupling make code more testable, ensuring quality and reliability.

Ready to unlock the secrets of OCP? In the next article, we'll dive into practical examples and explore how to apply this principle effectively in your projects. Stay tuned!

Without OCP

  • You create a Notification class that directly sends emails, text messages, and push notifications.

  • When a new notification channel like WhatsApp is needed, you must modify the Notification class, potentially introducing bugs and making it harder to maintain.

public class Notification {

    public void sendEmail(String message, String emailAddress) {
        // Code to send email
    }

    public void sendTextMessage(String message, String phoneNumber) {
        // Code to send text message
    }

    public void sendPushNotification(String message, String deviceId) {
        // Code to send push notification
    }

    // New method added for WhatsApp notification
    public void sendWhatsAppNotification(String message, String whatsappId) {
        // Code to send WhatsApp notification
    }
}

Problems with this approach:

  • Tightly coupled: The Notification class is directly responsible for handling multiple notification channels, making it rigid and less adaptable.

  • Hard to maintain: Adding a new channel (like WhatsApp) requires modifying the Notification class, potentially introducing bugs and increasing complexity. What about a third channel? a fourth on? etc.

  • Testing challenges: Testing each notification channel independently becomes difficult as they're all intertwined within the same class.

  • Risk of errors: Changes to one channel's logic could inadvertently affect others, leading to unexpected issues.

  • Limited extensibility: Adding further channels in the future would require continuous modifications to this core class, hindering its maintainability over time.

Abstractions to the Rescue

This example highlights the importance of adhering to the Open-Closed Principle to create flexible, maintainable, and testable code that can gracefully accommodate change.

Rely on interfaces and abstract classes to define contracts for behavior, allowing concrete implementations to vary without disrupting the core structure.

public interface NotificationChannel {
    void sendNotification(String message);
}

public class EmailNotification implements NotificationChannel {
    @Override
    public void sendNotification(String message) {
        // Send email logic
    }
}

public class TextNotification implements NotificationChannel {
    @Override
    public void sendNotification(String message) {
        // Send text message logic
    }
}

// Later, add WhatsAppNotification as needed

public class WhatsappNotification implements NotificationChannel {
    @Override
    public void sendNotification(String message) {
        // Send whatsapp message logic
    }
}


public class Notification {
    private NotificationChannel channel;

    public Notification(NotificationChannel channel) {
        this.channel = channel;
    }

    public void send(String message) {
        channel.sendNotification(message);
    }
}

Benefits:

  • Flexibility: Add new notification channels without touching the Notification class.

  • Testability: Easily test different notification channels independently.

  • Maintainability: Changes to one channel don't affect others.

  • Decoupling: The core Notification class is no longer tied to specific implementations.

Delegation for Flexibility

Employ techniques like factories and dependency injection to select and manage concrete implementations at runtime, fostering adaptability.

1. Introduce a factory:

We'll introduce later what is factory design pattern, and how we can employ it.

public class NotificationFactory {
    public static Notification createNotification(String type) {
        if (type.equals("email")) {
            return new Notification(new EmailNotification());
        } else if (type.equals("text")) {
            return new Notification(new TextNotification());
        } else if (type.equals("whatsapp")) {
            return new Notification(new WhatsAppNotification()); // Easily add new channels
        } else {
            throw new IllegalArgumentException("Invalid notification type");
        }
    }
}

2. Use dependency injection:

public class UserService{
private final NotificationFactory notifcationFactory;
public UserService(NotificationFactory notifcationFactory)
{
        this.notifcationFactory = notifcationFactory;
}
public static void sendWelcomeEmail(User newUser) {
    Notification notification = NotificationFactory.createNotification("email");
    notification.send("Hello from email!: " + newUser.getUserName());
}
}

Key takeaways:

  • Delegation shifts responsibility to other classes, making code more adaptable.

    • A class acting as a factory.

    • Separate classes for each channel

    • A notification class injected in it the notification channel interface.

  • Factories encapsulate object creation logic, centralizing configuration and simplifying client code.

  • Dependency injection promotes loose coupling and testability by injecting dependencies rather than creating them within classes.

  • Loose coupling: UserService doesn't depend on specific notification implementations, promoting flexibility and testability.


With the Open-Closed Principle under our belt, we’re now equipped to build software that evolves gracefully, adding new features without fracturing existing functionality. Buckle up, because next we'll explore the Liskov Substitution Principle, the key to ensuring seamless replacements within our code's architecture. Prepare to witness the power of subclasses that truly stand in for their parents.

More from this blog

DevDemystified

14 posts

Shaped tech and product strategic direction for startups in different stages, executing strategic plans. Led, managed and mentored engineering teams, setting clear goals.