Welcome to this lesson on the Open/Closed Principle (OCP). In our previous discussion, we focused on the Single Responsibility Principle (SRP), which guides us to create small, highly-cohesive components that each have only one reason to change. This practice gives us modules that are stable, understandable, and easy to test. But once we have these stable modules, a critical question arises: how do we add new functionality to our system without modifying and potentially destabilizing this existing, working code?
The Open/Closed Principle provides the answer. As the "O" in SOLID, it's a cornerstone for building software that can evolve gracefully over time. In this lesson, we will apply OCP to design deliberate extension points in a codebase. We will explore the core strategies for achieving this, such as abstraction and plugin patterns, and see how they are implemented in both Python and Rust. By the end, you will understand how to build systems that welcome new features without requiring risky changes to their core logic.
Defining the Open/Closed Principle
The principle, as formulated by Bertrand Meyer and later popularized by Robert C. Martin, states:
"Software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification."
This sounds like a paradox at first. How can something be both open and closed? Let's break it down.
- Closed for modification: This means that once a component has been developed, tested, and deployed, its source code should be stable. We shouldn't need to go back and change it every time a new, related feature is requested. This minimizes the risk of introducing bugs into code that already works.
- Open for extension: This means that we should be able to add new behaviors or features to the component without changing its source code. We achieve this by designing clever "extension points."
Let's begin by reading a clear definition and discussion of the principle.
SOLID series: The Open-Closed Principle
This article from the LogRocket Blog provides an excellent introduction to OCP, including its history and modern interpretation.
Start by reading the introductory sections to grasp the core idea. Read the definition of "open for extension, closed for modification." Then, continue to the section on the evolution of OCP to understand how the modern interpretation relies on polymorphism and abstraction.
As the article clarifies, the key to the modern, "Polymorphic" OCP is abstraction. We design our modules to depend on abstract interfaces (or traits in Rust) rather than concrete implementations. This allows new functionality to be "plugged in" by providing new concrete implementations of that abstraction.
It's also crucial to address a common misunderstanding.
SOLID series: The Open-Closed Principle
Let's clarify what "closed for modification" really means.
In the same article, read the section Misconception 1. This is important: OCP is about protecting code from changes driven by new features, not about forbidding bug fixes or necessary refactoring.
Implementing OCP: Abstraction and Polymorphism
The most fundamental technique for implementing OCP is to introduce a layer of abstraction between a client and its collaborators. The client code is "closed" because it depends only on the stable abstraction. The system is "open" because anyone can add new collaborators that conform to that abstraction.
Python: Abstract Base Classes
In Python, Abstract Base Classes (ABCs) from the abc module are a classic way to define such an abstraction. Let's see a practical example.
SOLID series: The Open-Closed Principle
This section of the LogRocket article provides a clear, self-contained Python example using ABCs.
Please read the subsection Python: Using abstract base classes. Pay close attention to the notify_user function. Notice how it is completely decoupled from the details of how a notification is sent. It only knows about the Notification abstraction.
In this example, the notify_user function is closed for modification. If your team needs to add support for sending notifications via a new service like Slack or Pushbullet, you don't touch notify_user at all. You simply add a new class:
class SlackNotification(Notification):
def send(self, message: str) -> None:
print(f"Sending Slack message: {message}")
# No changes needed to notify_user
slack = SlackNotification()
notify_user(slack, "Hello via Slack!")
This is the essence of OCP. The core logic is stable, and new functionality is added through extension.
Rust: Traits
In Rust, the equivalent and idiomatic tool for this kind of abstraction is the trait. A trait defines a set of methods that a type must implement to be considered a member of a group.
Let's rewrite the notification example in Rust:
// The abstraction (the "extension point")
trait Notifier {
fn send(&self, message: &str);
}
// A high-level module that depends on the abstraction
// This function is "closed for modification"
fn notify_user(notifier: &dyn Notifier, message: &str) {
notifier.send(message);
}
// First implementation
struct EmailNotifier;
impl Notifier for EmailNotifier {
fn send(&self, message: &str) {
println!("Sending email: {}", message);
}
}
// Second implementation
struct SmsNotifier;
impl Notifier for SmsNotifier {
fn send(&self, message: &str) {
println!("Sending SMS: {}", message);
}
}
fn main() {
let email = EmailNotifier;
let sms = SmsNotifier;
notify_user(&email, "Hello via Email!");
notify_user(&sms, "Hello via SMS!");
}
Just like the Python example, the notify_user function is completely decoupled from the concrete notification types. It operates on any type that implements the Notifier trait (&dyn Notifier is a trait object, representing any type that satisfies the Notifier contract). To add a SlackNotifier, you would simply define a new struct and implement the Notifier trait for it, without ever modifying notify_user.
A More Powerful Strategy: The Plugin/Registry Pattern
While abstraction is the foundational mechanism, we can build more dynamic and powerful extension systems on top of it. The Registry Pattern is an excellent example that turns a simple if-elif-else chain into a fully pluggable system. This is especially useful when you need to select a behavior based on a key, like a string or an enum.
I Hate Long If-Elif Chains: This Design Pattern Solved It Once and For All
This video from ArjanCodes demonstrates a step-by-step refactoring from a rigid if-elif block to a flexible, decorator-based registry in Python.
First, watch how a dictionary is used as a simple registry to eliminate an if-elif chain from 02:25 to 03:52. Then, see how decorators are used to create a self-registering plugin system from 05:28 to 07:54.
The final pattern shown in the video, where functions register themselves using a decorator, is a powerful technique for designing extension points. The core logic (the export_data function) is closed for modification, but the system is open to extension by simply adding a new Python file with a decorated function.
This pattern is extremely relevant to your interest in industrial computer vision. Imagine an image processing pipeline where you want to support different filtering algorithms.
# --- In a central 'registry.py' file ---
FILTERS = {}
def register_filter(name):
def decorator(func):
FILTERS[name] = func
return func
return decorator
def apply_filter(name, image):
if name not in FILTERS:
raise ValueError(f"Unknown filter: {name}")
return FILTERS[name](image)
# --- In a separate 'basic_filters.py' file ---
from .registry import register_filter
import cv2
@register_filter("blur")
def blur_filter(image):
return cv2.GaussianBlur(image, (5, 5), 0)
@register_filter("sharpen")
def sharpen_filter(image):
kernel = np.array([[-1,-1,-1], [-1,9,-1], [-1,-1,-1]])
return cv2.filter2D(image, -1, kernel)
# --- Later, a developer can add 'edge_filters.py' without touching any existing code ---
from .registry import register_filter
import cv2
@register_filter("canny")
def canny_filter(image):
return cv2.Canny(image, 100, 200)
This is a clean, maintainable, and extensible design. The ImagePy project, an open-source image processing framework, is built entirely around this philosophy.
Image-Py/imagepy: Image process framework based on plugin like ...
Let's look at how a real-world computer vision tool implements OCP using a plugin architecture.
Start by reading the section Filter Plugin to see an example of a Canny edge detector implemented as a plugin. Then, carefully read the list of steps for creating a new filter. Notice how you extend the application by adding a new file that inherits from a base Filter class—a perfect example of OCP.
OCP at an Architectural Scale
The Open/Closed Principle doesn't just apply to classes and functions; it applies to entire architectural components. When you design a system to allow plugins, you are applying OCP at a macro level. The host application is "closed," and the plugins provide the "extension."
There are many ways to design these architectural extension points.

Let's analyze these options through the lens of OCP:
- Embed a dynamic script language (e.g., Python): The host application (perhaps written in C++ or Rust for performance) exposes an API to an embedded Python interpreter. Users can write new Python scripts to extend the system's functionality. The C++ core is closed, but it's open to extension via Python scripts.
- Run a separate container and make RPCs: The host application makes a network call (Remote Procedure Call) to an external service, perhaps running in a Docker container. To add new functionality, you deploy a new containerized service. The host application's code doesn't change; it just calls a different endpoint. This is common in microservice architectures.
- Wasm (WebAssembly): The host application loads and runs a compiled Wasm module. Wasm provides a secure, sandboxed environment. You can write your extension in Rust, C++, or any language that compiles to Wasm, and the host can run it without modification.
- Dynamically link a shared library (.so, .dll): This is a classic approach. The host application loads a shared library at runtime. To add functionality, you provide a new library file. This is how many desktop applications and game engines handle plugins.
Each of these approaches makes the host system open for extension while keeping its core closed for modification.
Advanced OCP in Rust: Extension Traits
Rust's strict ownership and coherence rules (specifically the "orphan rule") present a unique challenge and a correspondingly elegant solution related to OCP. The orphan rule prevents you from implementing a foreign trait on a foreign type. For example, you can't implement a trait from the serde crate for a type from the actix-web crate within your own code.
This is where the extension trait pattern comes in. It allows you to extend foreign types with new functionality in a clean, OCP-compliant way.
My favorite Rust design pattern
This video from "Let's Get Rusty" provides a superb explanation of the orphan rule and the extension trait pattern that solves it.
First, watch the explanation of the orphan rule to understand the problem. Then, see how extension traits are used to add new functionality to an external type. Pay close attention to the advanced example from 04:28 to 06:16, which shows how to add a unique() method to all iterators. Finally, watch the summary of the pattern's power.
The extension trait pattern is the epitome of the Open/Closed Principle in Rust. You are extending the functionality of an external type (Vec, Request, or any Iterator) without modifying its original source code. This is a powerful and idiomatic technique to have in your Rust architecture toolkit.
Conclusion
In this lesson, we have seen how the Open/Closed Principle guides us to build resilient and maintainable software. By separating stable core logic from volatile extensions, we create systems that can adapt to new requirements with minimal risk.
Key Takeaways:
- OCP Defined: Software entities should be open for extension but closed for modification.
- Core Mechanism: Abstraction is the key. Depend on abstract interfaces (like Python's ABCs/Protocols or Rust's traits), not on concrete implementations.
- Powerful Patterns: The Registry/Plugin pattern, often implemented with decorators in Python, provides a highly flexible way to design for extension.
- Rust Idioms: Traits are the primary tool for abstraction. The extension trait pattern is a specific, powerful application of OCP for adding behavior to external types while respecting Rust's orphan rule.
- Architectural View: OCP applies at all scales, from single functions to system-level components using plugins, microservices, or Wasm modules.
We have now covered how to create focused components (SRP) and how to extend them safely (OCP). However, creating correct abstractions is critical. If a new implementation of an interface behaves in a way the client does not expect, it can break the system, violating the principle of being "closed for modification." Our next lesson will address this by introducing the Liskov Substitution Principle (LSP), which provides the rules for creating correct and substitutable subtypes.