Перейти к содержимому

3. SOLID: Open/Closed

Программные сущности (классы, модули, функции) должны быть открыты для расширения, но закрыты для изменения.

Если добавление новой функциональности требует изменения существующего кода — значит, архитектура не соответствует OCP.


Проблема: код который нужно постоянно менять

Заголовок раздела «Проблема: код который нужно постоянно менять»
// ❌ Каждый новый тип скидки требует изменения calculate()
class DiscountCalculator {
calculate(orderTotal: number, discountType: string): number {
if (discountType === 'seasonal') {
return orderTotal * 0.1;
} else if (discountType === 'loyalty') {
return orderTotal * 0.15;
} else if (discountType === 'new_user') {
return orderTotal * 0.2;
} else if (discountType === 'black_friday') {
// Добавили новый тип — пришлось менять существующий код
return orderTotal * 0.3;
}
return 0;
}
}

Добавляешь новый тип скидки — лезешь в существующий класс, меняешь его, рискуешь сломать уже работающие типы. Нарушение OCP.


// ✅ Интерфейс закрыт для изменений, открыт для реализаций
interface DiscountStrategy {
calculate(orderTotal: number): number;
get name(): string;
}
// Реализации — добавляем новые без изменения существующих
class SeasonalDiscount implements DiscountStrategy {
get name() { return 'seasonal'; }
calculate(orderTotal: number): number {
return orderTotal * 0.1;
}
}
class LoyaltyDiscount implements DiscountStrategy {
get name() { return 'loyalty'; }
calculate(orderTotal: number): number {
return orderTotal * 0.15;
}
}
class NewUserDiscount implements DiscountStrategy {
get name() { return 'new_user'; }
calculate(orderTotal: number): number {
return orderTotal * 0.2;
}
}
// Добавляем новый тип — создаём новый класс, НИЧЕГО не меняем
class BlackFridayDiscount implements DiscountStrategy {
get name() { return 'black_friday'; }
calculate(orderTotal: number): number {
return orderTotal * 0.3;
}
}
// Калькулятор не меняется при добавлении новых типов скидок
class DiscountCalculator {
private strategies = new Map<string, DiscountStrategy>();
register(strategy: DiscountStrategy): void {
this.strategies.set(strategy.name, strategy);
}
calculate(orderTotal: number, discountType: string): number {
const strategy = this.strategies.get(discountType);
return strategy ? strategy.calculate(orderTotal) : 0;
}
}
// Использование
const calculator = new DiscountCalculator();
calculator.register(new SeasonalDiscount());
calculator.register(new LoyaltyDiscount());
calculator.register(new BlackFridayDiscount());
const discount = calculator.calculate(1000, 'black_friday'); // 300

Ещё один способ — шаблонный метод (Template Method):

abstract class ReportGenerator {
// Метод закрыт — алгоритм фиксирован
generate(data: any[]): string {
const header = this.generateHeader();
const body = this.generateBody(data);
const footer = this.generateFooter();
return `${header}\n${body}\n${footer}`;
}
// Открыт для расширения — подклассы определяют детали
protected abstract generateHeader(): string;
protected abstract generateBody(data: any[]): string;
protected abstract generateFooter(): string;
}
class HtmlReportGenerator extends ReportGenerator {
protected generateHeader(): string { return '<html><body>'; }
protected generateBody(data: any[]): string {
return data.map(item => `<p>${item}</p>`).join('');
}
protected generateFooter(): string { return '</body></html>'; }
}
class MarkdownReportGenerator extends ReportGenerator {
protected generateHeader(): string { return '# Report\n'; }
protected generateBody(data: any[]): string {
return data.map(item => `- ${item}`).join('\n');
}
protected generateFooter(): string { return '\n---'; }
}

OCP работает не только с классами — в функциональном программировании это функции высшего порядка:

// Базовая функция закрыта для изменений
function processItems<T>(
items: T[],
transformer: (item: T) => T,
filter?: (item: T) => boolean,
): T[] {
const filtered = filter ? items.filter(filter) : items;
return filtered.map(transformer);
}
// Расширяем поведение через параметры, не меняя функцию
const doubled = processItems([1, 2, 3, 4], x => x * 2);
const doubledEven = processItems([1, 2, 3, 4], x => x * 2, x => x % 2 === 0);
const uppercased = processItems(['hello', 'world'], s => s.toUpperCase());

Express.js строится на OCP: каждый middleware расширяет цепочку обработки запросов без изменения ядра:

// Express не меняется, мы расширяем его через middleware
app.use(cors());
app.use(authMiddleware);
app.use(rateLimitMiddleware);
app.use(loggingMiddleware);
// Добавляем новую логику — новый middleware, не трогаем существующее
app.use(newFeatureMiddleware);

  • Функция/метод с длинной цепочкой if/else if или switch по типу
  • «А ещё добавь поддержку X» требует правок в трёх местах
  • Тесты ломаются при добавлении новой фичи

  1. Рефактори следующий код согласно OCP:
class FileProcessor {
process(content: string, type: 'compress' | 'encrypt' | 'base64'): string {
if (type === 'compress') return compress(content);
if (type === 'encrypt') return encrypt(content);
if (type === 'base64') return btoa(content);
throw new Error('Unknown type');
}
}
  1. Создай систему валидации форм с OCP: интерфейс Validator, несколько реализаций (email, phone, password), класс FormValidator который принимает массив валидаторов.

  2. Реализуй систему логирования с несколькими «сinks» (ConsoleLogger, FileLogger, RemoteLogger) используя OCP.

  3. Как OCP связан с паттерном Strategy? (Следующий урок будет о нём)

  4. Найди пример OCP в Node.js ecosystem — посмотри на систему плагинов в webpack или eslint.