The Open-Closed Principle
In object-oriented programming, the open–closed principle (OCP) states “software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification”; that is, such an entity can allow its behaviour to be extended without modifying its source code.
Why should we follow this principle?
Applying OCP principle leads to flexible software that can easily be scaled and maintained. What is meant by this is that existing code should be modified only for bug fixing. New behavior is exposed only by adding a new functions or modules. Since OCP backs the SRP each module or function should be testable separately. This way it avoids hidden variables and dependencies (reduces complexity) and increases the product stability. One consequence regarding big software projects is that you can rely on already implemented and tested code, which in turn reduces the maintenance costs.
Bad Practice:
class Animal:
def __init__(self, name: str):
self.name = name
...
animals = [
Animal('lion'),
Animal('mouse'),
Animal('snake')
]
def animal_sound(animals: list[Animal]):
for animal in animals:
if animal.name == 'lion':
print('roar')
elif animal.name == 'mouse':
print('squeak')
elif animal.name == 'snake':
print('hiss')
animal_sound(animals)
Drawback: Adding a new animal (Fish, sound “blub”) requires changing different parts of the code.
Good Practice:
from abc import ABC,abstractmethod
#
# Example taken from https://gist.github.com/dmmeteo/f630fa04c7a79d3c132b9e9e5d037bfd
#
# "Open for Extension, closed for Modification"
#
class Animal(ABC):
def __init__(self, name: str):
self.name = name
@abstractmethod
def make_sound(self):
raise Exception("An abstract animal can't make a sound")
#
# Extensions, typically in separate source code
#
class Lion(Animal):
def make_sound(self):
return 'roar'
class Mouse(Animal):
def make_sound(self):
return 'squeak'
class Snake(Animal):
def make_sound(self):
return 'hiss'
def animal_sound(animals: list[Animal]):
for animal in animals:
print(animal.make_sound())
animal_sound([Lion("Simba"),Mouse("Jerry")])
Examples @ LE
Bad Practice: OPTISIM HEX_SIM
All correlations are implemented as branches of large if-else blocks in HEX_SIM code, highly entangled with discretization scheme.
See code.
Good Practice: SMILE Generic HEX
Heat Transfer (and also Pressure Drop) correlations are implemented as separate SMILE Models with common interfaces: $$ htc=\alpha_\mathrm{Muse} (\dot N_i, T,p,\dot H) $$ see code. $$ htc = \alpha_\mathrm{Genius} (\dot N_i, T,p,\dot H) $$ see code.
Hence, the Muse correlations can be extended or modified without touching the Genius code
Heat transfer correlations are implemented independently of discretization scheme; same correlations used for finite volume discretization and new “ODE” approach
(SRP, OCP, low coupling - high cohesion)
Differences SRP \(\leftrightarrow\) OCP
SRP: There are separate classes for Discretization, Pressure Drop, Heat Transfer Coefficient
OCP: There may be different variants for each of these classes with common interface
Benefits of applying the OCP
include:
Increased flexibility: Adhering to the OCP makes it easier to add new features without the risk of breaking existing functionality.
Improved maintainability: Since you don’t need to modify existing code to add new features, there’s a lower chance of introducing bugs or regression issues.
Enhanced modularity: Following the OCP encourages modular design, making your codebase more organized and easier to manage.
Code reusability: OCP encourages the creation of reusable components. New functionality can be implemented in separate classes or modules, which can be shared and reused in different parts of the application.
Adaptation to change: As requirements change, OCP allows for quick adaptation without having to refactor existing code.
Code smells that indicate the need for OCP refactoring include: Switch statements: Switch statements that check the type of an object and perform different actions based on that type can be a sign that the code violates the OCP.
Conditional statements: Conditional statements that check for specific values or states can also be a sign that the code violates the OCP.
Large classes or functions: Large classes or functions that perform multiple tasks can be a sign that the code violates the Single Responsibility Principle (SRP) and may need to be refactored to adhere to the OCP.
Inappropriate inheritance: Inappropriate inheritance can lead to code that violates the OCP. If a subclass modifies the behavior of its parent class, it can break the open-closed principle.
Feature envy: Feature envy occurs when a method in one class uses methods or properties of another class excessively. This can be a sign that the code violates the OCP.
Data clumps: Data clumps occur when a group of data items are frequently passed around together. This can be a sign that the code violates the OCP because it can lead to tight coupling between classes.
Shotgun surgery: Shotgun surgery occurs when a single change requires modifications to many different classes. This can be a sign that the code violates the OCP.
God objects: God objects are classes that have too much responsibility and perform too many tasks. This can be a sign that the code violates the SRP and may need to be refactored to adhere to the OCP. Testability is another important benefit of applying the Open-Closed Principle (OCP). By adhering to the OCP, software becomes more testable due to the following reasons:
Isolation of code: The OCP encourages modular design and separates different functionalities into separate classes or modules. This modularity makes it easier to isolate and test individual components without affecting the rest of the system. Tests can focus on specific behavior or functionality, improving the accuracy and reliability of the testing process.
Mocking and stubbing: When adhering to the OCP, new behavior is added by creating new functions or modules rather than modifying existing code. This allows for easier implementation of mock objects or stubs during testing. Mocking and stubbing are techniques used to simulate dependencies or external services in order to test specific functionality in isolation. By adhering to the OCP, these techniques can be applied more effectively, enabling thorough testing of individual components.
Test coverage and maintainability: The OCP encourages the creation of reusable components, which can be independently tested. This promotes better test coverage as each component can have its own set of tests, ensuring that all functionalities are thoroughly tested. Additionally, as new behavior is added by adding new functions or modules, it becomes easier to write new tests for the newly introduced functionality without affecting existing tests. This improves the maintainability of the test suite, as changes to one part of the codebase are less likely to break unrelated tests.
Test-driven development (TDD): The OCP aligns well with the principles of test-driven development. By designing software with the OCP in mind, developers can write tests before implementing new functionality. This approach helps ensure that the new behavior is well-defined and testable from the start. It also provides a safety net for refactoring or modifying existing code, as tests can quickly detect any unintended side effects or regressions.
While it is generally recommended to apply the OCP in each newly coded feature, there may be situations where it is worth ignoring if it adds too much complexity for a simple task. However, it is important to strike a balance between keeping the code flexible and maintaining its quality.
If one needs to change or improve an existing feature or function because it’s not advanced enough or contains bugs, the OCP suggests that existing code should be modified only for bug fixing. New behavior should be exposed only by adding new functions or modules. This way, the codebase becomes easier to maintain, understand, and troubleshoot.
Outlook
Several Design Patterns support OCP