3. SOLID: Open/Closed
Design Patterns. Урок: SOLID — Open/Closed Principle
Заголовок раздела «Design Patterns. Урок: SOLID — Open/Closed Principle»Программные сущности (классы, модули, функции) должны быть открыты для расширения, но закрыты для изменения.
Если добавление новой функциональности требует изменения существующего кода — значит, архитектура не соответствует 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'); // 300OCP через наследование
Заголовок раздела «OCP через наследование»Ещё один способ — шаблонный метод (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 в функциональном стиле
Заголовок раздела «OCP в функциональном стиле»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());Middleware Pattern — OCP в действии
Заголовок раздела «Middleware Pattern — OCP в действии»Express.js строится на OCP: каждый middleware расширяет цепочку обработки запросов без изменения ядра:
// Express не меняется, мы расширяем его через middlewareapp.use(cors());app.use(authMiddleware);app.use(rateLimitMiddleware);app.use(loggingMiddleware);
// Добавляем новую логику — новый middleware, не трогаем существующееapp.use(newFeatureMiddleware);Признаки нарушения OCP
Заголовок раздела «Признаки нарушения OCP»- Функция/метод с длинной цепочкой
if/else ifилиswitchпо типу - «А ещё добавь поддержку X» требует правок в трёх местах
- Тесты ломаются при добавлении новой фичи
Практические задания
Заголовок раздела «Практические задания»- Рефактори следующий код согласно 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'); }}-
Создай систему валидации форм с OCP: интерфейс
Validator, несколько реализаций (email, phone, password), классFormValidatorкоторый принимает массив валидаторов. -
Реализуй систему логирования с несколькими «сinks» (ConsoleLogger, FileLogger, RemoteLogger) используя OCP.
-
Как OCP связан с паттерном Strategy? (Следующий урок будет о нём)
-
Найди пример OCP в Node.js ecosystem — посмотри на систему плагинов в webpack или eslint.