Принцип открытости/закрытости гласит:

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

Это значит, что если вам нужно добавить в программу новую логику или новые возможности, вы должны делать это путем добавления нового кода, а не изменением уже существующего, протестированного и работающего.


❌ Нарушение принципа (плохой код)

Представьте, что у нас есть класс для расчета стоимости доставки. Изначально мы доставляем только на машинах (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?

  1. Безопасность: Старый код не редактируется, а значит, риск внести новые баги в уже работающие фичи равен нулю.
  2. Масштабируемость: Если завтра добавится доставка дронами, курьером или ракетой — мы просто напишем новые классы (DroneDelivery, CourierDelivery), реализующие интерфейс DeliveryMethod.
  3. Легкость тестирования: Новые классы тестируются отдельно, старые тесты переписывать не нужно.