Open/Closed Principle
Open for extension, closed for modification.
Intent
Add new behavior without editing existing, working classes. Prefer new types over endless patches to one filter.
Problem
ProductFilter starts simple: filter_by_color, then filter_by_size, then filter_by_size_and_color. Every new criterion or combination means opening that class again. Combinations explode.
Solution
Introduce a Specification: an object that answers “does this item match?”BetterFilter only knows how to apply a spec. New rules are new classes.
Structure
| Piece | Role |
|---|---|
Product | Name, color, size |
ProductFilter | Old approach (violation) |
Specification | Match rule (is_satisfied) |
ColorSpecification / SizeSpecification | Concrete rules |
AndSpecification | Combine rules |
BetterFilter | Apply any spec |
Compare code
Violation — methods grow on ProductFilter
python
class ProductFilter:
def filter_by_color(self, products, color): ...
def filter_by_size(self, products, size): ...
def filter_by_size_and_color(self, products, size, color): ...
# next: weight, size+weight, color+weight, ...Compliant — this repo
python
class ColorSpecification(Specification):
def is_satisfied(self, item):
return item.color == self.color
class BetterFilter(Filter):
def filter(self, items, spec):
return [item for item in items if spec.is_satisfied(item)]
# compose without touching BetterFilter
large_blue = AndSpecification(
SizeSpecification(Size.LARGE),
ColorSpecification(Color.BLUE),
)Walkthrough
Quick check
Which change follows OCP for a new “filter by price” need?
Run locally
bash
uv run ocpSource: main.py · tutorial README
Takeaway
- Don’t grow one filter with every criterion
- Encode criteria as composable objects
- Extend by adding classes; keep the engine closed