Git для повседневной работы: команды и сценарии, которые сэкономят часы
Практическое руководство по Git для разработчиков: топ-10 команд, алгоритм разрешения конфликтов и сценарии, которые ускоряют повседневную работу и экономят часы.
Разбираем принципы SOLID на практике: как они помогают писать чистый код, какие типичные нарушения встречаются в реальных проектах и как это связано с подготовкой к собеседованиям.
04.08.2026
При подготовке к собеседованиям расшифровку SOLID часто заучивают наизусть: Single Responsibility, Open‑Closed, Liskov Substitution, Interface Segregation, Dependency Inversion. На словах всё звучит красиво, но на практике многие разработчики долго не могут понять, как эти принципы помогают писать код, который не требует переписывания через месяц.
В этой статье будут рассмотрены каждый принцип в коде, типичные места, где чаще всего спотыкаются, и связь с вопросами на собеседованиях.
SRP - самый простой для понимания, но самый сложный для соблюдения принцип. Он гласит: у класса должна быть только одна причина для изменения. На практике это означает, что класс должен делать что-то одно, но делать это хорошо.
Типичное нарушение - "божественный класс", который отвечает и за бизнес-логику, и за валидацию, и за сохранение в базу, и за отправку уведомлений. Например, вот такой монстр на JavaScript:
class OrderService { processOrder(order) { // валидация if (order.total < 0) { throw new Error('Negative total'); } // сохранение в БД this.orderRepository.save(order); // отправка email this.emailSender.sendConfirmation(order.customerEmail); // логирование this.logger.log(`Order processed: ${order.id}`); } }
Здесь четыре причины для изменения: изменение правил валидации, изменение логики сохранения, изменение формата письма и изменение способа логирования. Любое из этих изменений потребует редактировать этот класс, а значит, есть риск что-то сломать в несвязанной части.
Как исправить? Разнести ответственности по отдельным классам или сервисам. Например, создать OrderValidator, OrderRepository, OrderNotifier и OrderLogger. Тогда каждый класс будет меняться только по своей причине.
На собеседовании часто спрашивают: "Приведите пример нарушения SRP и как вы его исправили". Хороший ответ - показать именно такой рефакторинг, с объяснением, что это упростило тестирование: теперь можно тестировать валидацию отдельно, не поднимая базу данных.
OCP - это про то, что мы должны иметь возможность добавить новую функциональность, не меняя существующий код.
Классическое нарушение - цепочки if-else или switch, которые растут с каждым новым типом. Вот пример на JavaScript:
PYTHONfunction calculateDiscount(orderType) {if (orderType === 'regular') {return 0.05;} else if (orderType === 'premium') {return 0.1;} else if (orderType === 'vip') {return 0.2;} else {return 0.0;}}
Каждый раз, когда появляется новый тип заказа, мы лезем в эту функцию и добавляем ещё одну ветку. Через пару лет это превращается в простыню из пятнадцати elif, которую страшно трогать.
Решение - выделить интерфейс или абстрактный класс DiscountPolicy и реализовать его для каждого типа:
PYTHON// Базовый класс (аналог абстрактного)class DiscountPolicy {calculate(order) {throw new Error('Method calculate() must be implemented');}}class RegularDiscount extends DiscountPolicy {calculate(order) {return 0.05;}}class PremiumDiscount extends DiscountPolicy {calculate(order) {return 0.1;}}
Теперь добавление нового типа - это создание нового класса, а не изменение существующего. Код становится открытым для расширения и закрытым для изменения.
На собеседовании любят спрашивать: "Как бы вы спроектировали систему, чтобы добавить новый тип отчёта без изменения существующего кода?". Ответ - через стратегию или фабрику, что является прямым применением OCP.
LSP - это про то, что наследник не должен ломать контракт базового класса. Если у вас есть функция, которая принимает базовый класс, то она должна работать и с любым его наследником, не ожидая сюрпризов.
Самый известный пример нарушения - классический "квадрат против прямоугольника". Если Square наследуется от Rectangle, но переопределяет методы setWidth и setHeight так, чтобы они меняли обе стороны, то код, который ожидает прямоугольник, сломается.
Вот пример на JavaScript:
CSHARPclass Rectangle {constructor(width, height) {this._width = width;this._height = height;}get width() {return this._width;}set width(value) {this._width = value;}get height() {return this._height;}set height(value) {this._height = value;}}class Square extends Rectangle {constructor(size) {super(size, size);}get width() {return this._width;}set width(value) {this._width = value;this._height = value;}get height() {return this._height;}set height(value) {this._width = value;this._height = value;}}function resize(rect) {rect.width = 5;rect.height = 10;// Для прямоугольника площадь станет 50 (5 * 10),// для квадрата — 100 (10 * 10), так как при установке height=10// ширина тоже становится 10, а предыдущее значение 5 перезаписывается.}
Здесь Square нарушает инвариант прямоугольника: ширина и высота независимы. В реальном проекте такое случается, когда мы пытаемся сэкономить на коде и наследуемся от "похожего" класса, но поведение отличается.
Как избежать? Использовать композицию вместо наследования, либо выделять общий интерфейс с методами, которые действительно одинаковы для всех подтипов. Например, вместо иерархии "прямоугольник-квадрат" можно сделать интерфейс IShape с методом GetArea().
На собеседовании часто дают задачу: "Есть класс Bird с методом Fly, а есть Penguin. Как правильно спроектировать?". Правильный ответ - не заставлять пингвина летать, а выделить интерфейс IFlyable для тех, кто умеет летать.
ISP - это про то, что интерфейсы должны быть узкими и специфичными. Если у вас есть "толстый" интерфейс, который содержит методы для разных сценариев, то классы, которые используют только часть методов, вынуждены реализовывать лишнее.
Типичный пример - интерфейс Worker с методами work(), eat(), sleep(). Робот на заводе умеет работать, но не ест и не спит. Если он реализует этот интерфейс, то приходится выбрасывать исключения или оставлять пустые методы.
На JavaScript это выглядит так:
JAVAclass Worker {work() {throw new Error('Method work() must be implemented');}eat() {throw new Error('Method eat() must be implemented');}sleep() {throw new Error('Method sleep() must be implemented');}}class Robot extends Worker {work() {// реализация работыconsole.log('Robot is working');}eat() {throw new Error('Robot does not eat');}sleep() {throw new Error('Robot does not sleep');}}
Решение - разбить интерфейс на несколько узких: Workable, Eatable, Sleepable. Тогда робот реализует только Workable, а человек - все три.
В реальных проектах нарушение ISP встречается, когда мы создаём один большой интерфейс для репозитория, а потом разные сервисы используют разные методы. Например, интерфейс UserRepository с методами findById, findByEmail, save, delete, updatePassword, getAllPermissions. Сервис аутентификации использует только findByEmail, а сервис профиля - findById и updatePassword. В итоге каждый сервис зависит от всего интерфейса, и изменение любого метода тянет перекомпиляцию всех.
На собеседовании могут спросить: "Почему интерфейс с 20 методами - это плохо?". Ответ: потому что это увеличивает связанность и усложняет тестирование - моки приходится создавать с кучей заглушек.
DIP - это, пожалуй, самый важный принцип для архитектуры. Он гласит, что модули верхнего уровня не должны зависеть от модулей нижнего уровня; оба должны зависеть от абстракций. И абстракции не должны зависеть от деталей; детали должны зависеть от абстракций.
На практике это означает, что вместо того чтобы создавать конкретный объект внутри класса, мы получаем его через конструктор или параметр. Это позволяет легко заменять реализации и тестировать код с моками.
Пример нарушения на JavaScript:
PYTHONclass EmailService {send(message) {// отправка через SMTPconsole.log(`Sending email: ${message}`);}}class NotificationService {constructor() {this.emailService = new EmailService(); // жёсткая зависимость}notify(message) {this.emailService.send(message);}}
Если завтра мы захотим отправлять уведомления через SMS, придётся лезть в NotificationService и менять код. А если мы хотим протестировать NotificationService, не отправляя реальные письма, - не получится без подмены.
Решение - ввести абстракцию MessageSender и передавать её в конструктор:
PYTHON// Базовый класс (абстракция)class MessageSender {send(message) {throw new Error('Method send() must be implemented');}}// Конкретная реализация — отправка по emailclass EmailSender extends MessageSender {send(message) {// SMTP отправкаconsole.log(`Sending email: ${message}`);}}// Сервис уведомлений, зависит от абстракции MessageSenderclass NotificationService {constructor(sender) {this.sender = sender; // внедрение зависимости}notify(message) {this.sender.send(message);}}
Теперь мы можем подставить любой MessageSender - хоть SMS, хоть push-уведомление. И в тестах легко создать мок.
На собеседованиях DIP проверяют вопросами про внедрение зависимостей (DI) и контейнеры. Если вы умеете объяснить, почему new внутри класса - это зло, и как DI-контейнер помогает управлять зависимостями, это сильный плюс.
Теперь давайте посмотрим, как эти принципы нарушаются в реальной разработке, и что с этим делать.
Часто можно встретить сервисы, которые отвечают за всё: от валидации до отправки событий в Kafka. Это прямое нарушение SRP, сервисы следует разбивать по бизнес-функциям, а не по техническим слоям. Если OrderService делает 10 разных вещей, стоит подумать, какие из них можно вынести в отдельные классы с чёткой ответственностью.
Когда появляется новый тип сущности, вы добавляете ещё один case в пяти местах. Это нарушение OCP. Решение - использовать полиморфизм или таблицы соответствий. Например, вместо switch по типу платежа можно создать мапу paymentType -> PaymentProcessor.
Наследование ради переиспользования пары методов — это антипаттерн, который почти всегда ведёт к нарушению принципа подстановки Лисков (LSP). Проблема становится очевидной, когда подкласс изменяет контракт базового класса. Рассмотрим пример: AdminUser наследуется от User и переопределяет getPermissions(), возвращая расширенный набор прав. Однако родительский класс User в других частях системы используется с расчётом на стандартные ограничения — в итоге подстановка AdminUser вместо User ломает бизнес-логику (например, проверку isAdmin()). Единственное надёжное решение — отказаться от наследования в пользу композиции: внедрите экземпляр User как внутреннее поле в AdminUser и делегируйте ему общие методы, а специфику реализуйте отдельно.
В проектах с ORM часто создают один интерфейс на всю таблицу, а потом каждый сервис тянет его целиком. Это нарушение ISP. Решение - разделять интерфейсы по сценариям использования: UserQueryService для чтения, UserCommandService для записи.
Когда класс сам создаёт свои зависимости через new, это нарушение DIP. Особенно это заметно в тестах: чтобы покрыть такой класс, приходится поднимать всю инфраструктуру. Как избежать? Внедряйте зависимости через конструктор или свойства, используйте DI-контейнер.
При применении SOLID на практике можно заметить несколько конкретных улучшений.
Во-первых, код становится проще для чтения. Когда каждый класс делает одну вещь, не нужно держать в голове весь контекст — достаточно посмотреть на класс, чтобы понять его назначение. Это особенно важно, когда над проектом работает несколько человек.
Во-вторых, тестирование упрощается. Узкие интерфейсы и внедрение зависимостей (DI) делают подмену реальных сервисов моками тривиальной операцией. В результате основная логика покрывается быстрыми и изолированными юнит-тестами, а интеграционные тесты пишутся только для критических точек взаимодействия, а не для каждого компонента. Это сокращает время выполнения тестового набора и повышает надёжность проверок.
В-третьих, количество багов снижается, потому что изменения становятся локальными. Раньше для добавления нового типа уведомления приходилось править пять классов, и в трёх из них возникали ошибки. Теперь достаточно добавить новый класс, реализующий интерфейс, — и всё работает.
Конечно, SOLID не панацея. Иногда следование всем принципам приводит к излишней абстракции, особенно в маленьких проектах. Но в средних и крупных проектах это окупается.
На собеседованиях SOLID - одна из самых частых тем. Вопросы могут быть разными: от "Расскажите про принципы SOLID" до "Приведите пример нарушения и как вы его исправили". Чтобы хорошо ответить, нужно не просто заучить определения, а уметь показать на реальном коде.
Рекомендуется подготовить два-три примера из практического опыта, где применялся SOLID для рефакторинга. Например, как разбить монолитный сервис на несколько классов или заменить конструкцию switch на полиморфизм. Это позволит продемонстрировать, что теория не просто заучена, а применяется на практике.
Ещё один частый вопрос - "Как бы вы спроектировали систему X?". Здесь важно показать, что вы думаете о расширяемости и тестируемости, а не просто пишете код, который работает сегодня.
SOLID даёт конкретные ориентиры для повышения поддерживаемости, тестируемости и гибкости кода. Внедряйте их поэтапно: начните с SRP — разделите самый большой класс на несколько маленьких. Затем примените OCP — замените if-else полиморфизмом. Вы быстро увидите, как код становится прозрачнее, а время на доработки сокращается. Не пытайтесь переписать всё сразу — двигайтесь малыми шагами.
И не забывайте: на собеседовании важно не только знать принципы, но и уметь показать, как вы их применяете. Практикуйтесь на своих проектах, и тогда ответы будут звучать уверенно.
Практическое руководство по Git для разработчиков: топ-10 команд, алгоритм разрешения конфликтов и сценарии, которые ускоряют повседневную работу и экономят часы.
Разбираемся, когда паттерны проектирования действительно нужны, а когда превращают код в переусложнённую конструкцию. На примерах Factory, Observer и Strategy - с кодом и реальными сценариями из разработки.
Разбор видов тестирования - юнит, интеграционных и E2E - с практическими рекомендациями, инструментами и примерами, которые помогут разработчику уверенно отвечать на собеседованиях и писать полезные тесты.
Запустите тренировочную сессию с ИИ и получите детальную обратную связь, чтобы увереннее проходить реальные интервью