Challenging the Gospel of SOLID Principles
SOLID principles, hailed as guiding lights for designing maintainable software, it is far from it and should be gospel.
Over complication and Abstraction Overload
One prevalent criticism centers around the potential for over complication and abstraction overload. Rigorous adherence to Single Responsibility, Interface Segregation, and Dependency Inversion principles may result in a proliferation of classes, interfaces, and abstractions. This can make the code base harder to navigate, particularly for developers new to the project.
Consider a simple application to calculate the area of a rectangle. A zealous application of SOLID might result in the following:
RectangleclassIAreaCalculatorinterfaceRectangleAreaCalculatorclass (implementingIAreaCalculator)- Dependency injection to pass an
IAreaCalculatorto the place where the calculation is needed
# Define the Rectangle class
class Rectangle:
def __init__(self, width, height):
self.width = width
self.height = height
# Introduce the IAreaCalculator interface
class IAreaCalculator:
def calculate_area(self, shape):
pass
# Implement the IAreaCalculator for rectangles
class RectangleAreaCalculator(IAreaCalculator):
def calculate_area(self, rectangle):
return rectangle.width * rectangle.height
# Utilize dependency injection to pass an IAreaCalculator to the area calculation function
def calculate_area_using_calculator(rectangle, area_calculator):
return area_calculator.calculate_area(rectangle)
# Example usage
rectangle = Rectangle(width=5, height=10)
area_calculator = RectangleAreaCalculator()
result = calculate_area_using_calculator(rectangle, area_calculator)
print(f"The area of the rectangle is: {result}")While technically adhering to SOLID, arguably this adds unnecessary complexity for a simple task
Single Responsibility Principle (SRP) — Overly Granular Classes
- Scenario: Imagine a
Customerclass. It starts with basic responsibilities like handling customer information (name, address, etc.). Following SRP rigidly, you might extract responsibilities like: CustomerAddressValidationCustomerEmailFormatCheckerCustomerOrderHistoryManager
- Criticism: While each class technically has a single responsibility, this degree of separation might lead to a convoluted codebase with excessive components to track, potentially hindering readability for the sake of theoretical purity.
Open/Closed Principle (OCP) — Predicting the Future
- Scenario: A
PaymentProcessorclass allows payment via credit card. Aiming for maximum OCP, you use inheritance or interfaces for every conceivable future payment method.
- Criticism: While well-intention-ed, this might lead to complex class hierarchies. Additionally, some anticipated payment methods might never materialize (crypto?), leaving you with unused code. It’s sometimes simpler to refactor the
PaymentProcessorwhen a truly new method is required rather than preemptively designing for every possibility.
Liskov Substitution Principle (LSP) — unintuitive coding
- Scenario: You have a
Birdclass with afly()method. You create subclassesDuck,Penguin, etc. Penguins can't fly.
Wrong ways:
- Overriding
fly()inPenguinto do nothing breaks expectations.penguin.fly()should be the same asbird.fly() - Having
fly()throw an exception inPenguinis also surprising behavior
The right ways:
- Have
FlyingBirdandNonFlyBirdclass and have the respectivepenguinandduckinherit from those - Have a
FlyingInterfaceThatduckcan implement from, to allow them to fly. Bird will not have fly method
2 problems:
- The right way requires modification of existing code. By OCP, we would have designed so that this shouldn’t happen. But by following OCP, we have redundancy overhead… hmm
- The wrong ways are more obvious to implement. As birds are known for flying, we would think the generic bird can fly.
- Exceptions to a general characteristic require a separation to another class/interface. While this is supposed to be a feature of LSP (lean, single purpose inerfaces). If it’s many single case exceptions; baby birds are featherless, some birds have bills instead of beaks; some birds migrate some don’t; we explode from a neat inheritance hierarchy to many single purpose interfaces/classes.
In Summary
While SOLID principles provide valuable guidance for designing robust and maintainable software, they can sometimes lead to the opposite. Acknowledging potential downsides and advocating for a balanced approach is crucial. Developers should aim for a nuanced understanding of SOLID, considering project-specific requirements and striking a balance between flexibility and simplicity.