Skip to content

OCP in Practice

When to Apply OCP

Apply OCP when: - You anticipate new variations of a concept (payment methods, exporters, validators) - You're building a plugin system or framework - Changes to existing code have high risk (production-critical systems) - Multiple teams extend the same codebase

Don't apply OCP when: - You have 1-2 stable variations unlikely to change - The abstraction cost outweighs the extension benefit - The domain is exploratory and requirements are unclear

Pattern: Plugin Systems as OCP

Python Plugin Example:

# Core system (closed for modification)
class PluginManager:
    def __init__(self):
        self.plugins = []

    def register(self, plugin: Plugin):
        self.plugins.append(plugin)

    def execute(self, data):
        for plugin in self.plugins:
            data = plugin.process(data)
        return data

# Extension point (open for extension)
class Plugin(ABC):
    @abstractmethod
    def process(self, data): pass

# New plugins extend without modifying core
class LoggingPlugin(Plugin):
    def process(self, data):
        log(data)
        return data

class ValidationPlugin(Plugin):
    def process(self, data):
        if not self.is_valid(data):
            raise ValidationError()
        return data

Pattern: Middleware as OCP

JavaScript/Express Example:

// Core pipeline (closed)
class Pipeline {
  constructor() {
    this.middlewares = [];
  }

  use(middleware) {
    this.middlewares.push(middleware);
    return this;
  }

  async execute(request) {
    let response = request;
    for (const mw of this.middlewares) {
      response = await mw(response);
    }
    return response;
  }
}

// Extensions (open)
const authMiddleware = async (req) => {
  if (!req.headers.auth) throw new AuthError();
  return req;
};

const loggingMiddleware = async (req) => {
  console.log(req);
  return req;
};

// Usage: no modification to Pipeline
const pipeline = new Pipeline()
  .use(loggingMiddleware)
  .use(authMiddleware);

Decision: OCP Patterns by Use Case

Use Case Pattern Example
Varying algorithms Strategy Payment methods, sorting algorithms
Adding features to objects Decorator Middleware, stream wrappers
Sequential processing Chain of Responsibility Validation chains, event handlers
Creating object variants Factory + Polymorphism Document exporters, database drivers
Extending behavior at runtime Observer Event systems, pub/sub

Data-Driven OCP

Instead of code changes, use configuration:

PHP Example:

// Violation: hardcoded rules
class PriceCalculator {
  public function calculate($price, $country) {
    if ($country === 'US') return $price * 1.07;
    if ($country === 'CA') return $price * 1.13;
    // Requires code change for new country
  }
}

// OCP via configuration
class PriceCalculator {
  public function __construct(private array $taxRates) {}

  public function calculate($price, $country) {
    $rate = $this->taxRates[$country] ?? 0;
    return $price * (1 + $rate);
  }
}

// New countries: update config, not code
$calculator = new PriceCalculator([
  'US' => 0.07,
  'CA' => 0.13,
  'UK' => 0.20,
]);

Common Mistakes

  • Creating interfaces with one implementation -- Premature. Wait for the second variation. Why: speculative abstraction (YAGNI)
  • Abstract base classes doing too much -- Base classes should define contracts, not behavior. Why: forces coupling to implementation details
  • Forgetting to close core logic -- If adding extensions requires changing the manager/processor, OCP isn't achieved. Why: defeats the purpose
  • Over-reliance on inheritance -- Prefer composition. Why: inheritance is tight coupling; composition is flexible
  • Not versioning extension points -- In libraries, breaking interface changes break clients. Why: interfaces are contracts; changes require versioning

Performance Considerations

OCP Trade-offs: - Polymorphic dispatch has negligible cost in modern runtimes - Plugin systems may have registration overhead - Excessive indirection hurts readability more than performance

Optimize when: Profiling shows extension mechanism is a bottleneck (rare). Document trade-off.