Принцип открытости/закрытости гласит:
Программные сущности (классы, модули, функции) должны быть открыты для расширения, но закрыты для модификации.
Это значит, что если вам нужно добавить в программу новую логику или новые возможности, вы должны делать это путем добавления нового кода, а не изменением уже существующего, протестированного и работающего.
❌ Нарушение принципа (плохой код)
Представьте, что у нас есть класс для расчета стоимости доставки. Изначально мы доставляем только на машинах (Car). Но бизнес растет, и нам нужно добавить доставку на самолете (Air).
Чтобы это сделать, нам приходится заходить внутрь рабочего класса ShippingCalculator и менять конструкцию if/else или switch.
class ShippingCalculator {
// Нарушение OCP: каждый раз при добавлении нового типа доставки
// нам придется модифицировать этот метод и этот класс.
public calculateCost(type: string, weight: number): number {
if (type === 'Car') {
return weight * 5; // 5 рублей за кг
} else if (type === 'Train') {
return weight * 3; // 3 рубля за кг
} else if (type === 'Air') {
return weight * 20; // 20 рублей за кг (пришлось дописать код!)
}
throw new Error('Неизвестный тип доставки');
}
}
// Использование
const calculator = new ShippingCalculator();
console.log(calculator.calculateCost('Car', 10)); // 50Почему это плохо? Если мы случайно ошибемся при редактировании этого файла, мы можем сломать расчет для старых типов доставки (Car или Train), которые уже давно успешно работают.
✅ Соблюдение принципа (хороший код)
Чтобы соблюсти OCP, мы создаем интерфейс (абстракцию) для всех видов доставки. Теперь каждый новый тип доставки будет отдельным классом. Класс-калькулятор станет «закрыт» от изменений, но «открыт» для расширения за счет полиморфизма.
1. Создаем интерфейс для доставки
interface DeliveryMethod {
getCost(weight: number): number;
}2. Реализуем конкретные типы доставки как отдельные классы
class CarDelivery implements DeliveryMethod {
public getCost(weight: number): number {
return weight * 5;
}
}
class TrainDelivery implements DeliveryMethod {
public getCost(weight: number): number {
return weight * 3;
}
}
// Новая фича: Доставка самолетом. Создаем НОВЫЙ класс, не трогая старый код!
class AirDelivery implements DeliveryMethod {
public getCost(weight: number): number {
return weight * 20;
}
}3. Класс калькулятора (Закрыт для модификации)
Теперь ShippingCalculator принимает любой объект, реализующий интерфейс DeliveryMethod. Ему не важно, какой именно транспорт используется — код этого класса больше никогда не изменится, сколько бы новых видов транспорта мы ни добавили.
class ShippingCalculator {
// Метод принимает абстракцию, а не конкретный тип
public calculateCost(delivery: DeliveryMethod, weight: number): number {
return delivery.getCost(weight);
}
}
// Использование:
const calculator = new ShippingCalculator();
const car = new CarDelivery();
const air = new AirDelivery();
console.log(calculator.calculateCost(car, 10)); // 50 (Доставка машиной)
console.log(calculator.calculateCost(air, 10)); // 200 (Доставка самолетом)В чем выгода OCP?
- Безопасность: Старый код не редактируется, а значит, риск внести новые баги в уже работающие фичи равен нулю.
- Масштабируемость: Если завтра добавится доставка дронами, курьером или ракетой — мы просто напишем новые классы (
DroneDelivery,CourierDelivery), реализующие интерфейсDeliveryMethod. - Легкость тестирования: Новые классы тестируются отдельно, старые тесты переписывать не нужно.