Принципы проектирования нужны не ради красивых слов, а чтобы код было дёшево менять. На собесе их почти всегда спрашивают через пример: «покажи, где в React нарушается SRP» или «как применить DIP во фронтенде». Разберём каждый так, чтобы ты мог привести свой пример.
SOLID
S: Single Responsibility (единственная ответственность). У модуля одна причина для изменения. Формулировка Роберта Мартина: модуль отвечает перед одним «актором», то есть одной группой людей, которые просят изменений.
// Плохо: компонент и грузит данные, и считает скидки, и рисует
function Cart() {
const [items, setItems] = useState([]);
useEffect(() => { fetch('/api/cart').then(r => r.json()).then(setItems); }, []);
const total = items.reduce((s, i) => s + i.price * i.qty * (i.promo ? 0.9 : 1), 0);
return <div>{/* разметка */}{total}</div>;
}
// Лучше: загрузка в хуке, бизнес-правило в чистой функции, компонент только рисует
const total = calcCartTotal(items); // меняется, когда меняются правила скидок
const { data: items } = useCart(); // меняется, когда меняется API
O: Open/Closed (открыт для расширения, закрыт для изменения). Новое поведение добавляем новым кодом, а не правкой старого. Во фронтенде это композиция, слоты, children, render props, словари вместо switch.
// Каждый новый тип уведомления = новая запись, функция не меняется
const renderers: Record<NoticeType, (n: Notice) => ReactNode> = {
info: (n) => <Info {...n} />,
error: (n) => <Error {...n} />,
};
L: Liskov Substitution (подстановка Барбары Лисков). Наследник или реализация интерфейса должна работать везде, где ожидают базовый тип, без сюрпризов. Пример нарушения: свой <Input> принимает onChange, но вызывает его со строкой, а не с событием, как ожидает код, написанный под нативный input.
I: Interface Segregation (разделение интерфейсов). Не заставляй клиента зависеть от того, чем он не пользуется. Во фронтенде: компонент получает только нужные пропсы, а не весь объект user из стора. Меньше пропсов, меньше лишних ререндеров и проще тесты.
D: Dependency Inversion (инверсия зависимостей). Модули верхнего уровня зависят от абстракций, а не от деталей. Компонент не импортирует axios напрямую, а получает API-клиент или хук. Это упрощает тесты (подменяешь реализацию) и замену транспорта.
interface AnalyticsPort { track(event: string, props?: object): void }
// В приложении передаём реализацию для Яндекс Метрики, в тестах заглушку
Важно: DIP (принцип) и DI (внедрение зависимостей, техника) не одно и то же. DI один из способов соблюдать DIP. В React роль контейнера часто играет Context.
Другие принципы
- DRY (Don't Repeat Yourself): не дублируй знание. Два похожих куска кода, которые меняются по разным причинам, лучше оставить раздельными. Преждевременная абстракция хуже дубля.
- KISS: самое простое решение, которое работает.
- YAGNI: не делай то, что «может понадобиться». Гибкость стоит денег на поддержку.
- Связность и зацепление (cohesion и coupling): внутри модуля всё про одно (высокая связность), между модулями мало зависимостей (низкое зацепление).
- Композиция вместо наследования: в React наследование компонентов почти не используют, поведение собирают из хуков и вложенных компонентов.
- Закон Деметры: «говори только с ближайшими друзьями».
order.customer.address.cityв компоненте означает, что компонент знает слишком много о структуре данных.
Частые ошибки
- Применять SOLID механически: класс на каждую строчку, интерфейс с одной реализацией «на всякий случай». Это нарушает KISS и YAGNI.
- Путать SRP с «функция делает одну вещь». SRP про причины изменений, а не про размер.
- Выносить в общий хелпер всё, что похоже, и потом обвешивать его флагами
isAdmin,isMobile,variant. Это и есть неправильный DRY.
Что спросят на собесе
- Расшифруй SOLID и приведи пример каждого принципа из фронтенда.
- Чем DIP отличается от DI и как это выглядит в React?
- Когда дублирование лучше абстракции?
- Что такое связность и зацепление, как их оценить в коде?
- Как бы ты разбил компонент на 600 строк?