Open-Closed Principle
Open-Closed Principle (OCP) — второй принцип SOLID: программные сущности должны быть открыты для расширения, но закрыты для изменения — добавление нового поведения не должно требовать изменения существующего кода.
Зачем нужно
Нарушение OCP означает, что каждое новое требование вынуждает изменять рабочий код, ломая существующую функциональность и требуя повторного тестирования. Соблюдение OCP через абстракции (полиморфизм, strategy, плагины) позволяет расширять систему без риска регрессий.
Где используется
- Плагинные системы (добавление новых обработчиков без изменения ядра)
- Strategy Pattern (замена алгоритма без изменения контекста)
- Middleware и декораторы
- Компонентные библиотеки (расширение через props, children, slots)
Основной контент
Нарушение OCP
// Плохо: каждый новый тип требует изменения функции
function calculateDiscount(type, price) {
if (type === 'regular') {
return price * 0.05;
} else if (type === 'premium') {
return price * 0.10;
} else if (type === 'vip') {
return price * 0.20;
}
// Для нового типа нужно менять эту функцию!
return 0;
}
Соблюдение OCP через полиморфизм
// Хорошо: каждая стратегия — отдельный класс
class RegularDiscount {
calculate(price) { return price * 0.05; }
}
class PremiumDiscount {
calculate(price) { return price * 0.10; }
}
class VIPDiscount {
calculate(price) { return price * 0.20; }
}
// Контекст закрыт для изменений
class PriceCalculator {
constructor(discountStrategy) {
this.strategy = discountStrategy;
}
getPrice(price) {
return price - this.strategy.calculate(price);
}
}
// Новый тип — новый класс, не изменяем существующий код
class StudentDiscount {
calculate(price) { return price * 0.15; }
}
const calc = new PriceCalculator(new VIPDiscount);
console.log(calc.getPrice(1000)); // 800
const studentCalc = new PriceCalculator(new StudentDiscount);
console.log(studentCalc.getPrice(1000)); // 850
OCP через функции высшего порядка
// Реестр обработчиков вместо if/else цепочки
const handlers = {};
function register(type, handler) {
handlers[type] = handler;
}
function process(type, data) {
const handler = handlers[type];
if (!handler) throw new Error(`Неизвестный тип: ${type}`);
return handler(data);
}
// Расширение без изменения process
register('json', data => JSON.parse(data));
register('csv', data => data.split(',').map(s => s.trim()));
register('tsv', data => data.split('\t'));
console.log(process('json', '{"a":1}')); // { a: 1 }
console.log(process('csv', 'a, b, c')); // ['a', 'b', 'c']
OCP через декораторы
// Базовая функция закрыта для изменений
function fetchData(url) {
return fetch(url).then(r => r.json());
}
// Расширение через декораторы
function withLogging(fn) {
return function(...args) {
console.log('Запрос:', args[0]);
return fn(...args).then(result => {
console.log('Результат получен');
return result;
});
};
}
function withCache(fn, cache = new Map) {
return function(url) {
if (cache.has(url)) return Promise.resolve(cache.get(url));
return fn(url).then(data => {
cache.set(url, data);
return data;
});
};
}
const enhancedFetch = withCache(withLogging(fetchData));
Частые ошибки
- Преждевременная абстракция — OCP не означает «всегда создавать абстракцию». Абстрагируйте только когда видите реальную точку изменения (rule of three).
- Слишком большая иерархия классов — излишнее следование OCP через наследование создаёт глубокие иерархии. Предпочитайте composition и функции высшего порядка.
- Путаница OCP с immutability — OCP не про неизменяемость данных, а про неизменяемость исходного кода при расширении поведения.
🎓 Источники
- 🎓 [SOLID OCP — Open-Closed Principle for JavaScript] · 2024-12-03 · YouTube
- Тезисы: OCP — про обратную совместимость и контракты. Сущности должны быть открыты для расширения и закрыты для модификации. OCP тесно связан с LSP: подставлять подтипы можно, только когда OCP соблюдён — подтип не меняет, а расширяет контракт.
- «Сущности, классы, модули, функции, любые программные абстракции должны быть открыты для расширения и закрыты для модификации».