Skip to Content
ПродвинутыйПринцип открытой закрытостиЧек-лист: не нарушаете ли вы OCP?

Чек-лист: не нарушаете ли вы OCP?

Этот раздел нужен для самостоятельной проверки кода. Пройдите по пунктам и честно ответьте себе на каждый вопрос. Если хотя бы на один ответ «да» — задумайтесь о рефакторинге.


1. Цепочки условий

Вопрос: Есть ли в вашем коде цепочки if/else или switch, которые проверяют тип объекта или строковое значение?

// Плохо if (type === "email") { sendEmail(); } else if (type === "sms") { sendSms(); } else if (type === "push") { sendPush(); }

Если да:

  • Каждый раз при добавлении нового типа вы правите этот код → нарушение OCP.
  • Замените на полиморфизм (стратегии, цепочку обязанностей, фабрику).

Хорошо:

// Хорошо type Sender = { execute(): void; } class EmailSender implements Sender { execute() { ... } } class SmsSender implements Sender { execute() { ... } } // Новый тип — новый класс

2. Расширение интерфейсов

Вопрос: Приходится ли вам изменять интерфейс при добавлении новой функциональности?

// Плохо interface EventPublisher { publishOrderCreated(event: OrderCreated): void; publishProductUpdated(event: ProductUpdated): void; // Новое событие → новый метод → изменение интерфейса }

Если да:

  • Интерфейс не закрыт для изменений.
  • Используйте обобщённый подход: publish<T>(event: T).

Хорошо:

// Хорошо interface EventPublisher { execute<T>(event: T): void; }

3. Наследование

Вопрос: Наследник изменяет поведение родителя или выбрасывает исключения там, где родитель их не выбрасывал?

// Плохо (нарушение LSP, а значит и OCP) class Bird { public fly() { ... } } class Penguin extends Bird { public fly() { throw new Error("I can't fly!"); } }

Если да:

  • Наследник не может быть подставлен вместо родителя.
  • Используйте композицию или разделяйте интерфейсы.

Хорошо:

// Хорошо type Bird = { eat(): void; } type FlyingBird = Bird & { fly(): void; } class Sparrow implements FlyingBird { ... } class Penguin implements Bird { ... }

4. Расширение через наследование

Вопрос: Можете ли вы добавить новую функциональность, создав наследника, не изменяя родительский класс?

// Хорошо class BaseService { // общая логика } class UserService extends BaseService { // расширение }

Если нет:

  • Базовый класс требует изменений при добавлении новых фич → нарушение OCP.
  • Вынесите общую логику в абстрактный класс или интерфейс.

5. Статические методы и утилиты

Вопрос: Используете ли вы статические методы, которые сложно расширить?

// Плохо class HashService { static resolveToken(token: string) { ... } static resolveHeader(auth: string) { ... } }

Если да:

  • Нельзя подменить реализацию в тестах.
  • Нельзя добавить новую логику без изменения класса.
  • Используйте внедрение зависимостей вместо статики.

Исключение: Статика допустима для чистых функций (преобразование данных, математика) и фабрик (как BitBuilder.fromConfig). В этих случаях нет состояния, и тестировать такие функции легко. Но для сервисов с зависимостями (БД, внешние API) статика — антипаттерн.

6. Конфигурация vs логика

Вопрос: Добавляете ли вы новую фичу через изменение кода, а не через конфигурацию?

// Плохо const getDiscount = (type: string) => { if (type === "regular") return 0.9; if (type === "vip") return 0.8; if (type === "employee") return 0.7; ... }

Если да:

  • Каждый новый тип требует изменения функции.
  • Вынесите логику в конфигурацию или стратегии.

Хорошо:

// Хорошо const discounts = { regular: 0.9, vip: 0.8, employee: 0.7, } as const; const getDiscount = (type: keyof typeof discounts) => { return discounts[type]; };

7. Копипаста

Вопрос: Есть ли в проекте дублирующиеся классы, которые делают одно и то же, но с разными типами данных?

// Плохо class OrderCreatedListener { ... } class ProductUpdatedListener { ... } // Почти идентичный код

Если да:

  • Каждый новый тип требует копирования кода.
  • Вынесите общую логику в базовый класс.

Хорошо:

// ✅ Хорошо abstract class KafkaListener<T> { // общая логика } class OrderCreatedListener extends KafkaListener<OrderCreatedEvent> { ... }

8. Обратная совместимость

Вопрос: Ломается ли старый код при добавлении новой функциональности?

Если да:

  • Вы нарушаете OCP.
  • Используйте расширение через новые классы/интерфейсы, а не изменение старых.

Хорошо:

// Хорошо // Старый код остаётся без изменений class OldFeature { ... } // Новый код добавляется отдельно class NewFeature { ... }

9. Тестируемость

Вопрос: Можете ли вы протестировать новый функционал, не изменяя существующие тесты?

Если нет:

  • Новый функционал слишком сильно связан со старым.
  • Выделяйте новый функционал в отдельные классы/модули.

10. Модульность

Вопрос: Можете ли вы заменить один модуль другим без изменения остальной системы?

Если нет:

  • Модули слишком сильно связаны.
  • Используйте интерфейсы/абстракции для слабой связности.

Итоговый подсчёт

Количество «да»Уровень нарушения OCP
0–2Отлично! Ваш код соблюдает OCP.
3–5Есть нарушения. Начните с самых критичных мест.
6–8Серьёзные проблемы. Система требует рефакторинга.
9–10Код негибкий. Рекомендуется пересмотреть архитектуру.

Что делать, если вы нарушаете OCP?

  1. Найдите точки расширения — места, где часто появляются новые фичи.
  2. Выделите абстракции — интерфейсы, абстрактные классы.
  3. Используйте паттерны:
    • Стратегия — для замены алгоритмов.
    • Цепочка обязанностей — для последовательной обработки.
    • Фабричный метод — для создания объектов.
    • Декоратор — для добавления функциональности.
  4. Выносите логику в конфигурацию — вместо if/else.
  5. Предпочитайте композицию наследованию — гибче и безопаснее.
  6. Пишите тесты — они покажут, что вы ничего не сломали при расширении.

Запомните: OCP — это не про идеальный код с первого раза. Это про осознанность. Если вы понимаете, где ваша система будет расти, вы можете спроектировать её так, чтобы рост не требовал переписывания старого кода.


Пользуйтесь этим чек-листом при код-ревью и рефакторинге.

Last updated on