Чек-лист: не нарушаете ли вы 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?
- Найдите точки расширения — места, где часто появляются новые фичи.
- Выделите абстракции — интерфейсы, абстрактные классы.
- Используйте паттерны:
- Стратегия — для замены алгоритмов.
- Цепочка обязанностей — для последовательной обработки.
- Фабричный метод — для создания объектов.
- Декоратор — для добавления функциональности.
- Выносите логику в конфигурацию — вместо
if/else. - Предпочитайте композицию наследованию — гибче и безопаснее.
- Пишите тесты — они покажут, что вы ничего не сломали при расширении.
Запомните: OCP — это не про идеальный код с первого раза. Это про осознанность. Если вы понимаете, где ваша система будет расти, вы можете спроектировать её так, чтобы рост не требовал переписывания старого кода.
Пользуйтесь этим чек-листом при код-ревью и рефакторинге.