home / skills / yanko-belov / code-craft / open-closed

open-closed skill

/skills/open-closed

This skill helps you apply the Open/Closed Principle by guiding you to add new behavior via extensions rather than modifying existing code.

npx playbooks add skill yanko-belov/code-craft --skill open-closed

Review the files below or copy the command above to add this skill to your agents.

Files (1)
SKILL.md
5.9 KB
---
name: open-closed-principle
description: Use when adding new functionality to existing code. Use when tempted to add if/else or switch branches. Use when extending behavior of existing classes.
---

# Open/Closed Principle (OCP)

## Overview

**Software entities should be open for extension, but closed for modification.**

When new functionality is needed, extend the system with new code rather than modifying existing code. If adding a feature requires changing existing if/else chains, you're violating OCP.

## When to Use

- Adding a new payment method, notification channel, export format, etc.
- Tempted to add another `if/else` or `switch` case
- Existing code works but needs new variants
- Feature request: "add support for X"

## The Iron Rule

```
NEVER add another branch to an existing if/else or switch statement.
```

**No exceptions:**
- Not for "it's just one more case"
- Not for "we'll refactor later"
- Not for "the pattern is already established"
- Not for "it's faster this way"

**Noting the problem while doing it anyway is still a violation.**

## Detection: The "Add Branch" Smell

If your solution involves this pattern, STOP:

```typescript
// ❌ VIOLATION: Adding branches
if (type === 'existing') {
  // existing logic
} else if (type === 'new') {  // ← Adding this = OCP violation
  // new logic
}
```

Every `else if` you add is a modification to existing code that works.

## The Correct Pattern: Strategy/Plugin

Instead of modifying, extend:

```typescript
// ✅ CORRECT: Define interface, implement separately
interface PaymentMethod {
  process(amount: number): boolean;
}

class CreditCardPayment implements PaymentMethod {
  process(amount: number): boolean { /* ... */ }
}

class PayPalPayment implements PaymentMethod {
  process(amount: number): boolean { /* ... */ }
}

// Processor doesn't change when adding new methods
class PaymentProcessor {
  constructor(private methods: Map<string, PaymentMethod>) {}
  
  process(type: string, amount: number): boolean {
    const method = this.methods.get(type);
    if (!method) throw new Error(`Unknown: ${type}`);
    return method.process(amount);
  }
  
  register(type: string, method: PaymentMethod): void {
    this.methods.set(type, method);
  }
}
```

Adding Apple Pay? Create `ApplePayPayment`, call `register()`. **Zero modifications to PaymentProcessor.**

## Pressure Resistance Protocol

### 1. "Just Add Another Case"
**Pressure:** "The quickest approach: add more if/else branches"

**Response:** Adding branches takes the same time as creating a new class. The "quick" approach creates unmaintainable code.

**Action:** Create interface + implementation. Register the new variant.

### 2. "The Pattern Is Already There"
**Pressure:** "The code already uses if/else, just extend it"

**Response:** Existing violations don't justify more violations. This is the moment to refactor.

**Action:** 
1. Extract interface from existing branches
2. Convert branches to implementations
3. Add your new implementation

### 3. "We'll Refactor Later"
**Pressure:** "Add it now, we'll clean up later"

**Response:** You won't. The if/else will grow to 15 cases. Technical debt compounds.

**Action:** Refactor now. It takes 10 minutes. Adding to the mess takes the same time.

### 4. "I'll Note It But Do It Anyway"
**Pressure:** Internal rationalization that awareness = compliance

**Response:** **Noting the problem while violating the principle is still a violation.**

**Action:** Don't add the branch. Refactor instead. Comments about "should use strategy pattern" are not acceptable.

## Red Flags - STOP and Reconsider

If you notice ANY of these, you're about to violate OCP:

- Adding `else if` to existing conditional
- Adding `case` to existing switch
- Typing the same `if/else` structure that's already there
- Thinking "I'll mention this should be refactored"
- Method has more than 3 type-based branches

**All of these mean: Create an interface and implementation instead.**

## Refactoring Existing Violations

When you encounter existing if/else chains:

```typescript
// BEFORE: 5 branches in processPayment
if (type === 'card') { ... }
else if (type === 'paypal') { ... }
else if (type === 'apple') { ... }
else if (type === 'google') { ... }
else if (type === 'crypto') { ... }

// AFTER: Strategy pattern
interface PaymentMethod { process(amount: number): boolean; }
class CardPayment implements PaymentMethod { ... }
class PayPalPayment implements PaymentMethod { ... }
// etc.
```

**Refactor on touch:** When asked to add the 6th branch, refactor instead.

## Quick Reference

| Situation | Wrong | Right |
|-----------|-------|-------|
| Add payment method | Add `else if (type === 'new')` | Create `NewPayment implements PaymentMethod` |
| Add notification channel | Add `else if (channel === 'slack')` | Create `SlackNotifier implements Notifier` |
| Add export format | Add `case 'xlsx':` | Create `XlsxExporter implements Exporter` |
| Add discount type | Add `else if (discount === 'bogo')` | Create `BogoDiscount implements DiscountStrategy` |

## Common Rationalizations (All Invalid)

| Excuse | Reality |
|--------|---------|
| "It's just one more case" | That's what they said about the previous 5 cases. |
| "I noted it should be refactored" | Notes don't count. Refactor or don't, but don't pretend awareness is action. |
| "The code already uses if/else" | Existing violations don't justify more violations. |
| "Strategy pattern is overkill" | 3+ branches = strategy pattern is correct engineering. |
| "It's faster to add a branch" | It's not. You type the same code either way. |
| "We can refactor later" | You won't. The branch count will double. |

## The Bottom Line

**Open for extension. Closed for modification.**

When adding new functionality:
1. Create an interface (if none exists)
2. Implement the interface for the new variant
3. Register/inject the new implementation

Never add branches. Never "note it but do it anyway." Awareness without action is not compliance.

Overview

This skill enforces the Open/Closed Principle when adding new functionality to existing TypeScript code. It guides developers to extend behavior via interfaces and implementations instead of modifying working code. Use it to avoid growing if/else or switch chains and to keep systems maintainable.

How this skill works

The skill detects situations where new behavior is being added by branching (else if / case) and recommends the strategy/plugin pattern: define an interface, implement new variants, and register or inject them. It provides concrete refactoring steps and a pressure-resistance protocol for common objections. Examples show how a processor remains unchanged while new implementations are added.

When to use it

  • Adding a new payment method, notification channel, export format, or discount type
  • When you're tempted to add another if/else branch or switch case
  • Extending behavior of an existing class without changing its core logic
  • When requested to support a new variant and existing code already handles similar variants
  • When touching code that already has multiple type-based branches

Best practices

  • Never add another branch to an existing if/else or switch: extract an interface instead
  • Create a small interface (e.g., PaymentMethod) and one implementation per variant
  • Register or inject implementations so processors remain unchanged when adding variants
  • Refactor on touch: convert existing branches to implementations when adding a new case
  • Prefer composition and registration over central conditional logic for extensibility

Example use cases

  • Add Apple Pay: implement ApplePayPayment and register it with the payment processor
  • Add Slack notifications: create SlackNotifier implementing Notifier and inject it
  • Add XLSX export: build XlsxExporter implementing Exporter and plug it into the exporter registry
  • Convert a 5-branch discount function into DiscountStrategy implementations
  • Refactor an existing switch-based router into route handler classes registered dynamically

FAQ

What if the codebase already uses if/else everywhere?

Refactor now: extract an interface from the branches, implement each branch as a class, and replace the conditional with a registry or factory. Existing violations aren't an excuse to add more.

Is strategy/plugin overkill for a single extra case?

No. Adding one case now tends to become many. Implementing the pattern prevents exponential maintenance cost and usually takes the same time as writing another branch.