d9e5668cec
Fixes and improvements from exhaustive audit: Consistency fixes in SKILL.md: - 'Pipe & Filter' → 'Pipe and Filter' (one stray '&' found and corrected) - 'Singleton for factory instance' → clarified to 'register factory as singleton-scoped via DI container' (less misleading wording) - Documentation Format section updated with note that SKILL.md itself is the authoritative source for related-pattern combinations Coverage fix — Related Patterns sections: - Added '## Related Patterns' to ALL 94 pattern files (was 0/94) - Each section lists 3–6 related patterns with relationship descriptions - Covers: why they're related, when to prefer one vs the other, and which are often confused SOLID principles → Creational → Structural → Behavioral → Architectural → Concurrency → Functional → Resilience → Data Access → Messaging → Testing → Error Handling → Microservice — all 13 categories covered Code verification: - Python: 0 failures (all 85 testable blocks pass) - Go: 0 failures (all 76 testable blocks pass) - JavaScript: 0 failures (all 78 testable blocks pass) - All 239 code blocks verified correct after edits Final skill state: - 108 files, 36,524 lines across 13 reference categories - 94/94 pattern files have Related Patterns sections - 2,815-line SKILL.md with 67 decision trees, 23 scenarios, 0 broken references, 0 naming inconsistencies
11 KiB
11 KiB
Open/Closed Principle (OCP)
"Software entities should be open for extension but closed for modification." — Bertrand Meyer
Problem
Every time a new requirement appears (a new shape, a new payment method, a new report format), you must crack open an existing class and add another if/else or switch branch. This means:
- Existing, tested code is at risk of breakage with every change
- The class grows into an ever-expanding conditional monster
- Multiple developers touching the same function causes merge conflicts
- Unit tests must be re-run for all cases even when only one new case is added
Classic smell: A function with a chain of if type == "X" ... else if type == "Y" ... that grows every sprint.
Solution
Define an abstraction (interface, abstract class, protocol) that captures the varying behavior. Each new variant implements the abstraction. The code that uses the abstraction never changes — it simply calls the interface method. New behavior is added by writing a new class, not by editing existing ones.
When to Use
- You have a function with a growing chain of type-checks or switch statements
- New variants of behavior are frequently added
- You need to support plugins or extensions
- You want to add features without risking regression in stable code
When to Avoid
- The set of variants is truly fixed and unlikely to grow (e.g., boolean logic)
- Introducing an interface would add indirection for only one or two cases
- You are in early prototyping and the abstraction boundary is unclear
Pseudocode
Before — modification required for every new shape
FUNCTION calculate_total_area(shapes):
total = 0
FOR shape IN shapes:
IF shape.type == "circle":
total += PI * shape.radius ^ 2
ELSE IF shape.type == "rectangle":
total += shape.width * shape.height
// Must edit here to add triangle, pentagon, etc.
RETURN total
After — extension without modification
INTERFACE Shape:
METHOD area() -> number
CLASS Circle IMPLEMENTS Shape:
radius
METHOD area(): RETURN PI * radius ^ 2
CLASS Rectangle IMPLEMENTS Shape:
width, height
METHOD area(): RETURN width * height
// Adding Triangle requires NO changes to existing code:
CLASS Triangle IMPLEMENTS Shape:
base, height
METHOD area(): RETURN 0.5 * base * height
FUNCTION calculate_total_area(shapes: list of Shape):
total = 0
FOR shape IN shapes:
total += shape.area() // No type-checking needed
RETURN total
Python
"""
Open/Closed Principle — Python
BEFORE: AreaCalculator uses if/elif to handle each shape type.
AFTER: Each shape implements area(). New shapes need zero changes to the calculator.
"""
import math
from abc import ABC, abstractmethod
# ============================================================
# BEFORE: Violating OCP — must modify calculator for each shape
# ============================================================
class AreaCalculatorBefore:
@staticmethod
def calculate(shapes: list[dict]) -> float:
total = 0.0
for shape in shapes:
if shape["type"] == "circle":
total += math.pi * shape["radius"] ** 2
elif shape["type"] == "rectangle":
total += shape["width"] * shape["height"]
# Every new shape means editing this function
return total
# ============================================================
# AFTER: Following OCP — extend by adding classes, not editing
# ============================================================
class Shape(ABC):
@abstractmethod
def area(self) -> float:
...
class Circle(Shape):
def __init__(self, radius: float):
self.radius = radius
def area(self) -> float:
return math.pi * self.radius ** 2
def __repr__(self) -> str:
return f"Circle(radius={self.radius})"
class Rectangle(Shape):
def __init__(self, width: float, height: float):
self.width = width
self.height = height
def area(self) -> float:
return self.width * self.height
def __repr__(self) -> str:
return f"Rectangle({self.width}x{self.height})"
# NEW shape — no existing code modified
class Triangle(Shape):
def __init__(self, base: float, height: float):
self.base = base
self.height = height
def area(self) -> float:
return 0.5 * self.base * self.height
def __repr__(self) -> str:
return f"Triangle(base={self.base}, height={self.height})"
class AreaCalculator:
"""Works with ANY Shape — never needs modification."""
@staticmethod
def total_area(shapes: list[Shape]) -> float:
return sum(shape.area() for shape in shapes)
@staticmethod
def print_report(shapes: list[Shape]) -> None:
for shape in shapes:
print(f" {shape!r:>40} area = {shape.area():.2f}")
print(f" {'TOTAL':>40} area = {AreaCalculator.total_area(shapes):.2f}")
# ============================================================
# Demo
# ============================================================
def main():
print("--- BEFORE (OCP violated) ---")
old_shapes = [
{"type": "circle", "radius": 5},
{"type": "rectangle", "width": 4, "height": 6},
]
print(f" Total area: {AreaCalculatorBefore.calculate(old_shapes):.2f}")
print("\n--- AFTER (OCP applied) ---")
shapes: list[Shape] = [
Circle(5),
Rectangle(4, 6),
Triangle(3, 7), # Added without touching Circle, Rectangle, or AreaCalculator
]
AreaCalculator.print_report(shapes)
if __name__ == "__main__":
main()
Go
// Open/Closed Principle — Go
//
// BEFORE: totalArea() uses a switch on a type tag.
// AFTER: Shape interface — add new shapes without touching existing code.
package main
import (
"fmt"
"math"
)
// ============================================================
// BEFORE: Violating OCP
// ============================================================
type shapeData struct {
kind string
radius float64
width float64
height float64
base float64
}
func totalAreaBefore(shapes []shapeData) float64 {
total := 0.0
for _, s := range shapes {
switch s.kind {
case "circle":
total += math.Pi * s.radius * s.radius
case "rectangle":
total += s.width * s.height
// Must edit here for every new shape
}
}
return total
}
// ============================================================
// AFTER: Following OCP
// ============================================================
// Shape is the abstraction all shapes implement.
type Shape interface {
Area() float64
String() string
}
// Circle implements Shape.
type Circle struct{ Radius float64 }
func (c Circle) Area() float64 { return math.Pi * c.Radius * c.Radius }
func (c Circle) String() string { return fmt.Sprintf("Circle(radius=%.1f)", c.Radius) }
// Rectangle implements Shape.
type Rectangle struct{ Width, Height float64 }
func (r Rectangle) Area() float64 { return r.Width * r.Height }
func (r Rectangle) String() string { return fmt.Sprintf("Rectangle(%.1fx%.1f)", r.Width, r.Height) }
// Triangle — added without modifying Circle, Rectangle, or TotalArea.
type Triangle struct{ Base, Height float64 }
func (t Triangle) Area() float64 { return 0.5 * t.Base * t.Height }
func (t Triangle) String() string { return fmt.Sprintf("Triangle(base=%.1f, height=%.1f)", t.Base, t.Height) }
// TotalArea works with any Shape — never needs modification.
func TotalArea(shapes []Shape) float64 {
total := 0.0
for _, s := range shapes {
total += s.Area()
}
return total
}
func PrintReport(shapes []Shape) {
for _, s := range shapes {
fmt.Printf(" %-40s area = %.2f\n", s, s.Area())
}
fmt.Printf(" %-40s area = %.2f\n", "TOTAL", TotalArea(shapes))
}
// ============================================================
// Demo
// ============================================================
func main() {
fmt.Println("--- BEFORE (OCP violated) ---")
oldShapes := []shapeData{
{kind: "circle", radius: 5},
{kind: "rectangle", width: 4, height: 6},
}
fmt.Printf(" Total area: %.2f\n", totalAreaBefore(oldShapes))
fmt.Println("\n--- AFTER (OCP applied) ---")
shapes := []Shape{
Circle{Radius: 5},
Rectangle{Width: 4, Height: 6},
Triangle{Base: 3, Height: 7},
}
PrintReport(shapes)
}
JavaScript
// Open/Closed Principle — JavaScript
//
// BEFORE: discountCalculator uses if/else for each customer type.
// AFTER: Each customer type implements its own discount. New types need no edits.
// ============================================================
// BEFORE: Violating OCP
// ============================================================
function calculateDiscountBefore(customerType, amount) {
if (customerType === "regular") {
return amount * 0.05;
} else if (customerType === "premium") {
return amount * 0.10;
} else if (customerType === "vip") {
return amount * 0.20;
}
// Must edit this function for every new customer type
return 0;
}
// ============================================================
// AFTER: Following OCP
// ============================================================
class CustomerDiscount {
/** @param {number} amount */
discount(amount) {
return 0;
}
label() {
return "Unknown";
}
}
class RegularDiscount extends CustomerDiscount {
discount(amount) {
return amount * 0.05;
}
label() {
return "Regular (5%)";
}
}
class PremiumDiscount extends CustomerDiscount {
discount(amount) {
return amount * 0.10;
}
label() {
return "Premium (10%)";
}
}
class VIPDiscount extends CustomerDiscount {
discount(amount) {
return amount * 0.20;
}
label() {
return "VIP (20%)";
}
}
// NEW type — no existing code modified
class EmployeeDiscount extends CustomerDiscount {
discount(amount) {
return amount * 0.30;
}
label() {
return "Employee (30%)";
}
}
/** Works with ANY CustomerDiscount — never needs modification. */
function printDiscountReport(strategies, amount) {
for (const strategy of strategies) {
const d = strategy.discount(amount);
console.log(` ${strategy.label().padEnd(20)} on $${amount.toFixed(2)} = -$${d.toFixed(2)}`);
}
}
// ============================================================
// Demo
// ============================================================
function main() {
const amount = 200.0;
console.log("--- BEFORE (OCP violated) ---");
for (const type of ["regular", "premium", "vip"]) {
const d = calculateDiscountBefore(type, amount);
console.log(` ${type.padEnd(20)} on $${amount.toFixed(2)} = -$${d.toFixed(2)}`);
}
console.log("\n--- AFTER (OCP applied) ---");
const strategies = [
new RegularDiscount(),
new PremiumDiscount(),
new VIPDiscount(),
new EmployeeDiscount(), // Added without touching any existing class
];
printDiscountReport(strategies, amount);
}
main();
Related Patterns
- Strategy — strategy is the most direct OCP enabler: new behaviours are new strategy classes, no modification required.
- Decorator — decorator adds responsibilities to objects at runtime without modifying the decorated class.
- Template Method — template method lets subclasses extend a fixed algorithm skeleton without modifying the skeleton.
- Visitor — visitor adds operations to a stable type hierarchy without modifying the types themselves.