Definition: Entities should be open for extension but closed for modification.
The Open/Closed Principle (OCP) is a core tenet of object-oriented design, emphasizing that your code should be structured in a way that allows for new functionality to be added without requiring changes to existing, stable code. In essence, your code should be built to accommodate new requirements by adding new code rather than altering the existing codebase.
What Does “Open to Extension” Mean?
When we say that a class or module is “open to extension,” we mean that it can accommodate new behaviors or capabilities in the future. In a system that is closed to extension, the behavior is fixed and cannot be altered without directly modifying the source code. By ensuring that our code is open to extension, we can easily integrate new features without disrupting existing functionality.
What Does “Closed to Modification” Mean?
“Closed to modification” means that once a piece of code has been developed and tested, it should not require changes. The only way to change the behavior of the system should be through the extension, not by altering the existing code. Code that is closed for modification reduces the risk of introducing new bugs, breaking dependencies, or causing downtime due to redeployment.
Why Should Code Be Closed to Modification?
- Stability: By not altering existing code, you’re less likely to introduce new bugs.
- Efficiency: Avoiding code changes means you can skip redeployment, minimizing potential downtime.
- Safety: It reduces the risk of breaking other parts of the system that depend on the existing code.
- Simplicity: Code that adheres to OCP often has fewer conditionals, leading to cleaner, more maintainable code.
Note: Applying bug fixes directly to your code is acceptable, especially if there’s no better place to handle the issue. However, strive to minimize such direct changes.
An Example: Violating and Adhering to OCP
Consider the following example:
In this scenario, the Appliance
class contains methods for powering on specific appliances. If a new appliance type is introduced, you would need to modify the Appliance
class to add a new method. This approach violates the Open/Closed Principle because extending the system necessitates modifying existing code.
Refactoring to Adhere to OCP
To align with OCP, we can refactor the design as follows:
In this redesign, adding a new appliance, such as a WashingMachine
, does not require changes to the existing ApplianceOnOff
class or the IAppliance
interface. The system is now open to extension (adding new appliances) but closed to modification (no need to change existing code).
Balancing Abstraction and Concreteness
While it’s essential to design code that is open to extension, this often leads to increased abstraction. However, excessive abstraction can make your system overly complex. The key is to anticipate where flexibility is needed and apply abstraction only in those areas.
For example, consider this concrete implementation:
public class SomeFunctionality
{
public void DoSomething()
{
var doWhatever = new DoOnlyOneThing();
doWhatever.DoIt();
// rest of code
}
}
This code is very concrete and rigid. The use of the new
keyword directly ties the SomeFunctionality
class to a specific implementation (DoOnlyOneThing
), making it difficult to change the behavior without modifying the source code.
On the other hand, highly abstract code can look like this:
public class DoLiterallyAnything<TArg, TResult>
{
private Func<TArg, TResult> _function;
public DoLiterallyAnything(Func<TArg, TResult> function)
{
_function = function;
}
public TResult DoIt(TArg a)
{
return _function(a);
}
}
This class is highly flexible and can perform any action you define, but at the cost of clarity and simplicity. While abstract code is powerful, it should be used judiciously.
How to Predict Future Changes
Start with concrete implementations. As your code evolves and you notice a pattern of repeated changes, it might be time to introduce abstraction to make your system open to extension in that specific direction.
Common Approaches to Implementing OCP
- Parameters: Use method parameters to extend functionality without altering existing code.
- Inheritance: Leverage polymorphism by creating subclasses that extend the behavior of base classes.
- Composition/Dependency Injection: Use composition to inject dependencies, allowing behavior to change without modifying the class itself.
Example Implementations
Consider this initial example:
public class DoesOnlyOneThing
{
public void DoIt()
{
Console.WriteLine("Hello");
}
}
Parameter-based Extension
public class DoesOnlyOneThing
{
public void DoIt(string message)
{
Console.WriteLine(message);
}
}
Inheritance-based Extension
public class DoesOnlyOneThing
{
public virtual void DoIt()
{
Console.WriteLine("Hello world!");
}
}
public class DoesSomethingElse : DoesOnlyOneThing
{
public override void DoIt()
{
Console.WriteLine("Bye bye world!");
}
}
Here, the DoesOnlyOneThing
class is extended through inheritance, allowing DoesSomethingElse
to override the DoIt
method with new behavior.
Composition-based Extension
public class DoOnlyOneThing
{
private readonly IPrintService _printService;
public DoOnlyOneThing(IPrintService printService)
{
_printService = printService;
}
public void DoIt()
{
Console.WriteLine(_printService.GetMessage());
}
}
Instead of hardcoding the message in the DoIt
method, the class now relies on an external IPrintService
to provide the message. This allows different implementations of IPrintService
to be injected, thus changing the behavior without modifying the DoOnlyOneThing
class itself.
Working with Legacy Code
When dealing with legacy code, it’s often better to create new classes that adhere to modern principles rather than modifying the old code. This approach allows you to:
- Design for the problem at hand: Avoid the constraints of existing code and patterns.
- Avoid dependencies: New classes have no dependencies, offering more freedom in design.
- Minimize risk: Introducing new behavior without touching legacy code reduces the chance of unforeseen issues.
- Follow SRP: Ensure your new class adheres to the Single Responsibility Principle.
- Make it testable: Build new classes in a way that they are easy to unit test.