Понедельник. Code review. Junior разработчик гордо демонстрирует новую фичу. Класс на 800 строк. Методы по 150 строк. Десять обязанностей в одном классе. Изменение одного поведения требует правок в пяти местах. Тесты не работают после каждого чиха. Сеньор качает головой и пишет: "Почитай про SOLID".
Через неделю. Тот же junior. Новая задача. Теперь 50 классов по 20 строк. Интерфейс на каждый чих. Три уровня абстракций для простой операции. Никто не понимает где что находится. "Я применил SOLID", радостно сообщает разработчик. Сеньор вздыхает: "Это называется overengineering".
Знакомая ситуация?
SOLID принципы в 2026 году остаются фундаментом качественного объектно-ориентированного кода. Но между теоретическим пониманием и практическим применением лежит пропасть. Большинство junior разработчиков попадают в одну из двух крайностей: либо игнорируют SOLID полностью, либо применяют его фанатично без понимания контекста.
Эта статья про практическое применение SOLID принципов. Не теоретические определения, которые есть в каждом учебнике. Реальные примеры из продакшн проектов. Типичные ошибки junior разработчиков с объяснением почему это ошибки. Правильные решения. Когда SOLID применять нужно, а когда он только вредит. С кодом на современных языках. Без философии, только практика.
1. Что такое SOLID и почему это важно
История и контекст
SOLID - это акроним из пяти принципов объектно-ориентированного проектирования, введённых Robert C. Martin (Uncle Bob) в начале 2000-х. Аббревиатуру SOLID придумал Michael Feathers около 2004 года.
Пять принципов:
S - Single Responsibility Principle (Принцип единственной ответственности)
O - Open-Closed Principle (Принцип открытости/закрытости)
L - Liskov Substitution Principle (Принцип подстановки Барбары Лисков)
I - Interface Segregation Principle (Принцип разделения интерфейса)
D - Dependency Inversion Principle (Принцип инверсии зависимостей)
Зачем нужен SOLID
Когда проект маленький (1000 строк), можно писать как угодно. Всё работает, изменения вносятся быстро. Проблемы начинаются когда код растёт до 10 000, 100 000 строк.
Без SOLID:
Изменение в одном месте ломает код в неожиданных местах
Добавление новой функции требует переписывания больших кусков кода
Тестирование превращается в ночной кошмар
Новые разработчики не могут разобраться в коде
Технический долг растёт экспоненциально
С SOLID:
Код легче поддерживать (каждый класс делает одно, понятно что)
Проще добавлять новые фичи (расширяем, а не переписываем)
Тесты пишутся проще (изолированные компоненты)
Баги проще находить и фиксить (чёткие границы ответственности)
Код переживает рост проекта
Главное заблуждение junior разработчиков
SOLID - это не железные правила. Это принципы, guidelines. Их нужно применять осознанно, понимая trade-offs. Фанатичное следование SOLID может навредить так же как полное игнорирование.
Когда SOLID не нужен:
Скрипты на один раз (data migration script)
Прототипы и MVP где требования меняются каждый день
Performance-critical код где каждая абстракция стоит миллисекунды
Код который никогда не будет меняться (легаси поддержка)
Когда SOLID нужен:
Production код который будет жить и развиваться
Библиотеки и фреймворки (другие будут использовать)
Код с высокой частотой изменений
Большие проекты с несколькими разработчиками
2. Single Responsibility Principle (SRP): одна ответственность
Определение
Класс должен иметь только одну причину для изменения. Другими словами, класс должен иметь только одну ответственность.
Важное уточнение: ответственность - это не "одна функция" или "один метод". Это одна логическая область изменений. Класс User может иметь методы getName(), setName(), validate(), но всё это одна ответственность - управление данными пользователя.
Типичная ошибка junior: God Class
Плохо: Класс делает всё
class User {
constructor(name, email) {
this.name = name;
this.email = email;
}
// Работа с данными пользователя
getName() {
return this.name;
}
// Валидация (другая ответственность!)
validate() {
if (!this.email.includes('@')) {
throw new Error('Invalid email');
}
}
// Сохранение в БД (третья ответственность!)
save() {
const db = Database.connect();
db.query('INSERT INTO users...');
}
// Отправка email (четвёртая!)
sendWelcomeEmail() {
const smtp = new SmtpClient();
smtp.send(this.email, 'Welcome!');
}
// Генерация отчёта (пятая!)
generateReport() {
return `User Report: ${this.name}`;
}
}Почему это плохо:
Изменение логики email - нужно трогать класс User
Изменение формата отчёта - нужно трогать класс User
Смена базы данных - нужно трогать класс User
Класс невозможно протестировать изолированно
5 разных причин для изменения класса
Хорошо: Разделение ответственностей
// Только данные пользователя
class User {
constructor(name, email) {
this.name = name;
this.email = email;
}
getName() {
return this.name;
}
getEmail() {
return this.email;
}
}
// Только валидация
class UserValidator {
validate(user) {
if (!user.getEmail().includes('@')) {
throw new Error('Invalid email');
}
}
}
// Только сохранение в БД
class UserRepository {
save(user) {
const db = Database.connect();
db.query('INSERT INTO users VALUES (?, ?)',
[user.getName(), user.getEmail()]);
}
}
// Только отправка email
class EmailService {
sendWelcomeEmail(user) {
const smtp = new SmtpClient();
smtp.send(user.getEmail(), 'Welcome!');
}
}
// Только генерация отчётов
class UserReportGenerator {
generate(user) {
return `User Report: ${user.getName()}`;
}
}Теперь:
Изменение логики email - трогаем только EmailService
Смена БД - трогаем только UserRepository
Каждый класс можно тестировать отдельно
Каждый класс имеет одну причину для изменения
Реальный пример: система заказов e-commerce
Junior разработчик создаёт класс Order который:
Хранит данные заказа
Считает итоговую сумму
Применяет промокоды
Проверяет доступность товаров на складе
Резервирует товары
Обрабатывает оплату
Отправляет уведомления
Генерирует накладные
Результат: класс на 1200 строк. При изменении логики промокодов - риск сломать оплату. При добавлении нового способа оплаты - нужно редактировать гигантский класс.
Правильное разделение:
Order - данные заказа
PriceCalculator - расчёт цен и скидок
InventoryService - проверка и резервирование товаров
PaymentProcessor - обработка платежей
NotificationService - уведомления
InvoiceGenerator - генерация документов
Ошибка: слишком мелкое дробление
Некоторые junior, прочитав про SRP, доходят до абсурда:
class UserNameGetter {
get(user) {
return user.name;
}
}
class UserNameSetter {
set(user, name) {
user.name = name;
}
}
class UserEmailGetter {
get(user) {
return user.email;
}
}Это overengineering. Геттеры и сеттеры - часть одной ответственности (работа с данными). Не нужно разделять каждый метод в отдельный класс.
3. Open-Closed Principle (OCP): открыт для расширения, закрыт для модификации
Определение
Классы должны быть открыты для расширения, но закрыты для модификации. Вы должны иметь возможность добавлять новое поведение без изменения существующего кода.
Типичная ошибка junior:if/else или switch для каждого типа
Плохо: Модификация при добавлении нового типа
class PaymentProcessor {
processPayment(order, paymentType) {
if (paymentType === 'credit_card') {
// Обработка кредитной карты
console.log('Processing credit card');
// ...логика
} else if (paymentType === 'paypal') {
// Обработка PayPal
console.log('Processing PayPal');
// ...логика
} else if (paymentType === 'crypto') {
// Обработка криптовалюты
console.log('Processing crypto');
// ...логика
}
}
}Проблема:
Добавление нового способа оплаты (например, Apple Pay) требует модификации класса PaymentProcessor. Добавляем ещё один else if. Класс постоянно растёт. Риск сломать существующие способы оплаты. Нарушение OCP.
Хорошо: Расширение через полиморфизм
// Абстракция
interface PaymentMethod {
process(order): void;
}
// Реализации
class CreditCardPayment implements PaymentMethod {
process(order) {
console.log('Processing credit card');
// ...логика кредитной карты
}
}
class PayPalPayment implements PaymentMethod {
process(order) {
console.log('Processing PayPal');
// ...логика PayPal
}
}
class CryptoPayment implements PaymentMethod {
process(order) {
console.log('Processing crypto');
// ...логика криптовалюты
}
}
// Процессор работает с абстракцией
class PaymentProcessor {
processPayment(order, paymentMethod: PaymentMethod) {
paymentMethod.process(order);
}
}
// Использование
const processor = new PaymentProcessor();
const creditCard = new CreditCardPayment();
processor.processPayment(order, creditCard);Теперь:
Добавление Apple Pay - создаём новый класс ApplePayPayment, не трогая существующий код:
class ApplePayPayment implements PaymentMethod {
process(order) {
console.log('Processing Apple Pay');
// ...логика Apple Pay
}
}PaymentProcessor не меняется. Закрыт для модификации, открыт для расширения.
Реальный пример: система уведомлений
В проекте есть NotificationService который отправляет уведомления:
Плохо:
class NotificationService {
send(user, message, type) {
if (type === 'email') {
// Отправка email
this.sendEmail(user.email, message);
} else if (type === 'sms') {
// Отправка SMS
this.sendSMS(user.phone, message);
} else if (type === 'push') {
// Push уведомление
this.sendPush(user.deviceToken, message);
}
}
}Добавление Telegram уведомлений требует изменения NotificationService. Добавление Slack уведомлений - опять изменения. Класс раздувается.
Хорошо:
interface NotificationChannel {
send(user, message): void;
}
class EmailChannel implements NotificationChannel {
send(user, message) {
// Логика email
}
}
class SMSChannel implements NotificationChannel {
send(user, message) {
// Логика SMS
}
}
class PushChannel implements NotificationChannel {
send(user, message) {
// Логика Push
}
}
class NotificationService {
private channels: NotificationChannel[];
constructor(channels: NotificationChannel[]) {
this.channels = channels;
}
sendToAll(user, message) {
this.channels.forEach(channel => {
channel.send(user, message);
});
}
}
// Использование
const service = new NotificationService([
new EmailChannel(),
new SMSChannel(),
new PushChannel()
]);
service.sendToAll(user, 'Hello!');Добавление Telegram:
class TelegramChannel implements NotificationChannel {
send(user, message) {
// Логика Telegram
}
}
// Просто добавляем в конфигурацию
const service = new NotificationService([
new EmailChannel(),
new TelegramChannel() // Новый канал!
]);Ошибка: преждевременная абстракция
Junior читает про OCP и начинает создавать интерфейсы для всего на свете, даже если есть только одна реализация:
interface UserNameProvider {
getName(): string;
}
class User implements UserNameProvider {
getName() {
return this.name;
}
}Зачем интерфейс если есть только одна реализация? YAGNI (You Aren't Gonna Need It). Не создавайте абстракции заранее. Создавайте когда появляется вторая реализация.
4. Liskov Substitution Principle (LSP): подстановка подтипов
Определение
Объекты подклассов должны быть заменяемы объектами базовых классов без нарушения корректности программы. Если класс B наследует класс A, то B должен быть полноценной заменой A.
Проще: наследник не должен нарушать контракт родителя.
Классическая ошибка: Square/Rectangle problem
Плохо: Нарушение LSP
class Rectangle {
constructor(width, height) {
this.width = width;
this.height = height;
}
setWidth(width) {
this.width = width;
}
setHeight(height) {
this.height = height;
}
getArea() {
return this.width * this.height;
}
}
class Square extends Rectangle {
setWidth(width) {
this.width = width;
this.height = width; // Квадрат, стороны равны
}
setHeight(height) {
this.width = height;
this.height = height; // Квадрат, стороны равны
}
}
// Использование
function testRectangle(rectangle: Rectangle) {
rectangle.setWidth(5);
rectangle.setHeight(4);
console.log(rectangle.getArea()); // Ожидаем 20
}
const rect = new Rectangle(0, 0);
testRectangle(rect); // 20 - правильно
const square = new Square(0, 0);
testRectangle(square); // 16 вместо 20 - неправильно!Проблема:
Square нарушает поведение Rectangle. Функция testRectangle работает корректно с Rectangle, но ломается с Square. Это нарушение LSP. Подкласс не может быть заменой базового класса.
Почему так произошло:
Математически квадрат - это прямоугольник. Но в программировании Rectangle подразумевает что ширину и высоту можно менять независимо. Square нарушает это ожидание.
Хорошо: Общий интерфейс без наследования
interface Shape {
getArea(): number;
}
class Rectangle implements Shape {
constructor(private width: number, private height: number) {}
setWidth(width: number) {
this.width = width;
}
setHeight(height: number) {
this.height = height;
}
getArea() {
return this.width * this.height;
}
}
class Square implements Shape {
constructor(private side: number) {}
setSide(side: number) {
this.side = side;
}
getArea() {
return this.side * this.side;
}
}Реальный пример: система скидок
Плохо:
class Discount {
apply(price: number): number {
return price * 0.9; // 10% скидка
}
}
class NoDiscount extends Discount {
apply(price: number): number {
return price; // Нет скидки
}
}
class NegativeDiscount extends Discount {
apply(price: number): number {
return price * 1.1; // Надбавка 10%
}
}
// Использование
function processOrder(price: number, discount: Discount) {
const finalPrice = discount.apply(price);
// Ожидаем что цена уменьшится или останется такой же
if (finalPrice > price) {
throw new Error('Скидка не может увеличивать цену!');
}
return finalPrice;
}Проблема: NegativeDiscount нарушает ожидания от Discount. Базовый класс подразумевает уменьшение цены, а подкласс увеличивает.
Хорошо:
interface PriceModifier {
apply(price: number): number;
}
class Discount implements PriceModifier {
apply(price: number): number {
return price * 0.9;
}
}
class Surcharge implements PriceModifier {
apply(price: number): number {
return price * 1.1;
}
}
class NoModification implements PriceModifier {
apply(price: number): number {
return price;
}
}Теперь нет ложных ожиданий. PriceModifier может изменять цену в любую сторону.
Практические правила для LSP:
Подкласс не должен требовать более строгих предусловий чем базовый класс
Подкласс не должен обещать более слабых постусловий
Подкласс должен сохранять инварианты базового класса
Если метод базового класса не бросает исключения, подкласс тоже не должен
5. Interface Segregation Principle (ISP): разделение интерфейсов
Определение
Клиенты не должны зависеть от интерфейсов, которые они не используют. Лучше много маленьких специфичных интерфейсов, чем один большой универсальный.
Типичная ошибка: Fat Interface
Плохо: Гигантский интерфейс
interface Worker {
work(): void;
eat(): void;
sleep(): void;
code(): void;
test(): void;
deploy(): void;
writeDocumentation(): void;
conductMeetings(): void;
}
class Developer implements Worker {
work() { /* работа */ }
eat() { /* еда */ }
sleep() { /* сон */ }
code() { /* написание кода */ }
test() { /* тестирование */ }
deploy() { /* деплой */ }
writeDocumentation() { /* документация */ }
conductMeetings() { /* встречи */ }
}
class Robot implements Worker {
work() { /* работа */ }
eat() { throw new Error('Роботы не едят'); }
sleep() { throw new Error('Роботы не спят'); }
code() { /* код */ }
test() { /* тесты */ }
deploy() { /* деплой */ }
writeDocumentation() { /* документация */ }
conductMeetings() { throw new Error('Роботы не проводят встречи'); }
}Проблема:
Robot вынужден реализовывать методы eat(), sleep(), conductMeetings() которые ему не нужны. Приходится бросать исключения или писать пустые реализации. Нарушение ISP.
Хорошо: Разделённые интерфейсы
interface Workable {
work(): void;
}
interface Eatable {
eat(): void;
}
interface Sleepable {
sleep(): void;
}
interface Codeable {
code(): void;
test(): void;
deploy(): void;
}
interface Documentable {
writeDocumentation(): void;
}
interface Communicable {
conductMeetings(): void;
}
class Developer implements
Workable, Eatable, Sleepable,
Codeable, Documentable, Communicable {
work() { /* работа */ }
eat() { /* еда */ }
sleep() { /* сон */ }
code() { /* код */ }
test() { /* тесты */ }
deploy() { /* деплой */ }
writeDocumentation() { /* документация */ }
conductMeetings() { /* встречи */ }
}
class Robot implements Workable, Codeable, Documentable {
work() { /* работа */ }
code() { /* код */ }
test() { /* тесты */ }
deploy() { /* деплой */ }
writeDocumentation() { /* документация */ }
}Теперь каждый класс реализует только нужные ему интерфейсы.
Реальный пример: система CMS
В CMS есть разные типы контента: статьи, видео, подкасты.
Плохо:
interface Content {
getTitle(): string;
getBody(): string;
getVideoUrl(): string;
getAudioUrl(): string;
getDuration(): number;
getThumbnail(): string;
getTranscript(): string;
}
class Article implements Content {
getTitle() { return this.title; }
getBody() { return this.body; }
getVideoUrl() { throw new Error('Статьи не имеют видео'); }
getAudioUrl() { throw new Error('Статьи не имеют аудио'); }
getDuration() { throw new Error('Статьи не имеют длительности'); }
getThumbnail() { return this.image; }
getTranscript() { throw new Error('Статьи не имеют транскрипта'); }
}
class Video implements Content {
getTitle() { return this.title; }
getBody() { throw new Error('Видео не имеют текста'); }
getVideoUrl() { return this.url; }
getAudioUrl() { throw new Error('Видео не имеют аудио'); }
getDuration() { return this.duration; }
getThumbnail() { return this.thumbnail; }
getTranscript() { return this.transcript; }
}Хорошо:
interface Titled {
getTitle(): string;
}
interface Textual {
getBody(): string;
}
interface Visual {
getThumbnail(): string;
}
interface VideoContent {
getVideoUrl(): string;
getDuration(): number;
}
interface AudioContent {
getAudioUrl(): string;
getDuration(): number;
}
interface Transcriptable {
getTranscript(): string;
}
class Article implements Titled, Textual, Visual {
getTitle() { return this.title; }
getBody() { return this.body; }
getThumbnail() { return this.image; }
}
class Video implements Titled, Visual, VideoContent, Transcriptable {
getTitle() { return this.title; }
getThumbnail() { return this.thumbnail; }
getVideoUrl() { return this.url; }
getDuration() { return this.duration; }
getTranscript() { return this.transcript; }
}
class Podcast implements Titled, AudioContent, Transcriptable {
getTitle() { return this.title; }
getAudioUrl() { return this.url; }
getDuration() { return this.duration; }
getTranscript() { return this.transcript; }
}Ошибка: слишком много мелких интерфейсов
Некоторые junior доходят до абсурда:
interface HasName {
getName(): string;
}
interface HasEmail {
getEmail(): string;
}
interface CanSetName {
setName(name: string): void;
}
interface CanSetEmail {
setEmail(email: string): void;
}Это overengineering. Имя и email - логически связанные атрибуты пользователя, их можно объединить в один интерфейс UserProfile.
6. Dependency Inversion Principle (DIP): инверсия зависимостей
Определение
Модули высокого уровня не должны зависеть от модулей низкого уровня. Оба должны зависеть от абстракций. Абстракции не должны зависеть от деталей. Детали должны зависеть от абстракций.
Проще: зависьте от интерфейсов, а не от конкретных классов.
Типичная ошибка: прямая зависимость от конкретного класса
Плохо: Жёсткая связь
class MySQLDatabase {
connect() {
console.log('Connecting to MySQL');
}
query(sql: string) {
console.log('Executing query:', sql);
return [];
}
}
class UserService {
private database: MySQLDatabase;
constructor() {
this.database = new MySQLDatabase(); // Прямая зависимость!
}
getUser(id: number) {
this.database.connect();
return this.database.query(`SELECT * FROM users WHERE id = ${id}`);
}
}Проблема:
UserService жёстко привязан к MySQL
Невозможно переключиться на PostgreSQL без изменения UserService
Невозможно протестировать UserService без реальной базы данных
Высокоуровневый модуль (UserService) зависит от низкоуровневого (MySQLDatabase)
Хорошо: Зависимость от абстракции
// Абстракция
interface Database {
connect(): void;
query(sql: string): any[];
}
// Реализации
class MySQLDatabase implements Database {
connect() {
console.log('Connecting to MySQL');
}
query(sql: string) {
console.log('Executing MySQL query:', sql);
return [];
}
}
class PostgreSQLDatabase implements Database {
connect() {
console.log('Connecting to PostgreSQL');
}
query(sql: string) {
console.log('Executing PostgreSQL query:', sql);
return [];
}
}
// Для тестов
class MockDatabase implements Database {
connect() {
// Ничего не делаем
}
query(sql: string) {
return [{ id: 1, name: 'Test User' }];
}
}
// Высокоуровневый модуль зависит от абстракции
class UserService {
constructor(private database: Database) {
// Инъекция зависимости через конструктор
}
getUser(id: number) {
this.database.connect();
return this.database.query(`SELECT * FROM users WHERE id = ${id}`);
}
}
// Использование
const mysqlDb = new MySQLDatabase();
const userService = new UserService(mysqlDb);
// Легко переключить на PostgreSQL
const postgresDb = new PostgreSQLDatabase();
const userService2 = new UserService(postgresDb);
// Легко тестировать
const mockDb = new MockDatabase();
const userServiceForTests = new UserService(mockDb);Реальный пример: система логирования
Плохо:
class FileLogger {
log(message: string) {
// Запись в файл
fs.appendFileSync('app.log', message + '\n');
}
}
class OrderService {
private logger: FileLogger;
constructor() {
this.logger = new FileLogger();
}
createOrder(order) {
this.logger.log('Creating order');
// ...логика создания заказа
}
}Проблемы:
Невозможно переключиться на CloudWatch или другую систему логирования
В тестах будут реальные записи в файл
OrderService не может работать без файловой системы
Хорошо:
interface Logger {
log(message: string): void;
error(message: string): void;
}
class FileLogger implements Logger {
log(message: string) {
fs.appendFileSync('app.log', `[INFO] ${message}\n`);
}
error(message: string) {
fs.appendFileSync('app.log', `[ERROR] ${message}\n`);
}
}
class CloudWatchLogger implements Logger {
log(message: string) {
// Отправка в CloudWatch
}
error(message: string) {
// Отправка в CloudWatch с уровнем ERROR
}
}
class ConsoleLogger implements Logger {
log(message: string) {
console.log(`[INFO] ${message}`);
}
error(message: string) {
console.error(`[ERROR] ${message}`);
}
}
class OrderService {
constructor(private logger: Logger) {}
createOrder(order) {
this.logger.log('Creating order');
try {
// ...логика создания заказа
} catch (error) {
this.logger.error(`Order creation failed: ${error}`);
}
}
}
// Production
const fileLogger = new FileLogger();
const orderService = new OrderService(fileLogger);
// Development
const consoleLogger = new ConsoleLogger();
const devOrderService = new OrderService(consoleLogger);
// Tests
const mockLogger = {
log: jest.fn(),
error: jest.fn()
};
const testOrderService = new OrderService(mockLogger);Инъекция зависимостей (DI)
DIP часто реализуется через паттерн Dependency Injection. Три способа:
1. Constructor Injection (рекомендуется):
class Service {
constructor(private dependency: Dependency) {}
}2. Setter Injection:
class Service {
private dependency: Dependency;
setDependency(dependency: Dependency) {
this.dependency = dependency;
}
}3. Interface Injection:
interface DependencyInjectable {
injectDependency(dependency: Dependency): void;
}DI контейнеры:
В больших приложениях используют DI контейнеры:
TypeScript: InversifyJS, TSyringe
Java: Spring, Guice
C#: Autofac, Unity
Python: dependency-injector
7. Типичные ошибки junior разработчиков
Ошибка 1: Фанатичное применение SOLID везде
Junior прочитал про SOLID и начинает применять его даже где не нужно.
Пример: Простой DTO (Data Transfer Object) с геттерами и сеттерами. Junior создаёт:
Интерфейс для DTO
Абстрактный базовый класс
Фабрику для создания DTO
Билдер для конструирования DTO
Валидатор в отдельном классе
Для объекта с тремя полями!
Правило: SOLID для изменяемого кода. Если это простой DTO который никогда не изменится, оставьте его простым.
Ошибка 2: Создание интерфейса для каждого класса
interface UserService {
getUser(id: number): User;
}
class UserServiceImpl implements UserService {
getUser(id: number): User {
// ...
}
}Если есть только одна реализация UserService, интерфейс не нужен. YAGNI. Создайте интерфейс когда появится вторая реализация.
Ошибка 3: Слишком много маленьких классов
Junior разделил класс на 20 микроклассов по 3 строки. Никто не может найти где что находится. Навигация по коду превращается в кошмар.
Правило: Класс должен быть достаточно большим чтобы иметь осмысленную ответственность. 50-200 строк - норм ально. 10 строк - слишком мелко. 1000 строк - слишком крупно.
Ошибка 4: Абстракции без причины
abstract class AbstractUserRepositoryFactoryProvider {
abstract createFactory(): UserRepositoryFactory;
}
interface UserRepositoryFactory {
create(): UserRepository;
}Три уровня абстракций для создания репозитория. Зачем?
Правило: Каждая абстракция должна решать реальную проблему. Не создавайте абстракции "на будущее".
Ошибка 5: Нарушение всех принципов сразу
Самая частая проблема junior: God Class который делает всё.
class Application {
// Нарушает SRP - миллион обязанностей
// Нарушает OCP - if/else для каждого типа
// Нарушает LSP - наследники ведут себя неожиданно
// Нарушает ISP - интерфейс требует реализации 50 методов
// Нарушает DIP - зависит от конкретных классов напрямую
}Ошибка 6: Непонимание разницы между ответственностью и методом
Junior думает что SRP значит "один метод на класс":
class UserNameGetter {
get(user) { return user.name; }
}
class UserEmailGetter {
get(user) { return user.email; }
}Это не SRP, это паранойя. Геттеры - часть одной ответственности (доступ к данным).
Ошибка 7: Использование наследования вместо композиции
class Employee extends Person {
// ...
}
class Manager extends Employee {
// ...
}
class SeniorManager extends Manager {
// ...
}
class Director extends SeniorManager {
// ...
}Глубокие иерархии наследования - признак проблем. Prefer composition over inheritance.
8. SOLID в реальных фреймворках и библиотеках
Spring Framework (Java): DIP повсюду
Весь Spring построен на Dependency Injection:
@Service
public class UserService {
private final UserRepository userRepository;
private final EmailService emailService;
// Constructor injection - DIP
@Autowired
public UserService(
UserRepository userRepository,
EmailService emailService
) {
this.userRepository = userRepository;
this.emailService = emailService;
}
}React: SRP в компонентах
Каждый компонент - одна ответственность:
// Плохо - один компонент делает всё
function UserDashboard() {
// Загрузка данных
// Рендер профиля
// Рендер заказов
// Рендер уведомлений
}
// Хорошо - разделение
function UserDashboard() {
return (
<>
);
}Express.js: Middleware как OCP
Добавление новой функциональности через middleware без изменения существующего кода:
const app = express();
// Логирование
app.use(loggingMiddleware);
// Аутентификация
app.use(authMiddleware);
// Добавляем новый middleware - не трогаем существующие
app.use(rateLimitMiddleware);Django ORM: ISP в моделях
Миксины позволяют добавлять только нужную функциональность:
from django.db import models
# Маленькие переиспользуемые интерфейсы
class TimeStampedModel(models.Model):
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
abstract = True
class SoftDeleteModel(models.Model):
is_deleted = models.BooleanField(default=False)
class Meta:
abstract = True
# Используем только нужные
class Article(TimeStampedModel, SoftDeleteModel):
title = models.CharField(max_length=200)
content = models.TextField()9. Практические советы по применению SOLID
1. Начинайте с SRP
Это самый простой и важный принцип. Перед созданием класса спросите: "Какая у него одна ответственность?". Если не можете ответить одним предложением, класс делает слишком много.
2. Code review с фокусом на SOLID
В code review задавайте вопросы:
"Этот класс имеет одну ответственность?" (SRP)
"Нужно ли модифицировать этот класс при добавлении нового типа?" (OCP)
"Можно ли заменить базовый класс на подкласс?" (LSP)
"Все ли методы интерфейса используются?" (ISP)
"Зависит ли класс от абстракций или от конкретных реализаций?" (DIP)
3. Рефакторинг по шагам
Не пытайтесь исправить весь код сразу. Рефакторьте постепенно:
Выберите один проблемный класс
Покройте его тестами (если их нет)
Примените один SOLID принцип
Убедитесь что тесты проходят
Повторите для следующего класса
4. Паттерны проектирования как реализация SOLID
Многие паттерны реализуют SOLID принципы:
| Паттерн | SOLID принцип |
|---|---|
| Strategy | OCP, DIP |
| Factory | OCP, DIP |
| Decorator | OCP, SRP |
| Adapter | OCP, ISP |
| Observer | OCP, DIP |
5. Тесты как индикатор нарушений SOLID
Если класс сложно тестировать, скорее всего он нарушает SOLID:
Тест требует много моков: Возможно нарушение DIP или SRP
Тест очень длинный: Возможно нарушение SRP
Тест ломается при изменениях в других местах: Возможно нарушение OCP
Сложно написать тест: Возможно нарушение нескольких принципов
6. Используйте статические анализаторы
SonarQube: Детектирует God Classes, сложные методы
ESLint/TSLint: Правила для максимального размера файлов и функций
ReSharper: Подсказывает возможности рефакторинга
10. Когда НЕ применять SOLID
Сценарий 1: Прототипы и MVP
Когда требования меняются каждый день, преждевременная абстракция вредит. Напишите работающий код, потом отрефакторите.
Сценарий 2: Одноразовые скрипты
Скрипт миграции данных который запустится один раз не нуждается в SOLID. Главное - чтобы работал.
Сценарий 3: Performance-critical код
Каждая абстракция - это дополнительный вызов функции. В некоторых местах (игровые движки, обработка видео) это критично.
Сценарий 4: Очень простые системы
CRUD API на 5 эндпоинтов не нуждается в сложной архитектуре с DI контейнерами и абстракциями.
Правило:
Применяйте SOLID когда:
Код будет жить долго
Код будет часто меняться
Код будут поддерживать другие люди
Код критичен для бизнеса
Не применяйте когда:
Код одноразовый
Прототип
Deadline через 2 дня
Перформанс критичнее всего
11. Чек-лист для junior разработчика
Перед созданием нового класса:
☐ Могу ли я описать ответственность класса одним предложением? (SRP)
☐ Будет ли класс изменяться по одной причине? (SRP)
☐ Смогу ли я добавить новое поведение без изменения этого класса? (OCP)
☐ Все ли методы интерфейса будут использоваться? (ISP)
☐ Зависит ли класс от абстракций или от конкретных реализаций? (DIP)
При code review своего кода:
☐ Нет ли у меня классов больше 300 строк? (возможно SRP)
☐ Нет ли цепочек if/else для разных типов? (возможно OCP)
☐ Нет ли методов которые бросают "Not Implemented"? (возможно LSP или ISP)
☐ Нет ли прямых new в конструкторах? (возможно DIP)
☐ Легко ли будет тестировать этот код?
Красные флаги в коде:
Класс названный Manager, Handler, Util, Helper (часто God Class)
Методы больше 50 строк
Больше 3 уровней вложенности if/else
Импорт конкретных классов вместо интерфейсов
Невозможность замокать зависимости в тестах
Заключение
SOLID - это не догма. Это инструмент. Как молоток: хорош для гвоздей, плох для винтов.
Junior разработчики часто впадают в крайности: либо игнорируют SOLID (код превращается в спагетти), либо применяют фанатично (код превращается в лазанью из абстракций).
Правильный подход - баланс:
Понимайте принципы
Применяйте осознанно
Помните про trade-offs
Не создавайте абстракции заранее
Рефакторьте когда видите нарушения
Главный критерий качественного кода - не соответствие SOLID, а простота понимания и изменения. Если ваш код легко читается, легко тестируется и легко модифицируется, вы на правильном пути. SOLID помогает достичь этого, но не является самоцелью.
Пишите код так, будто его будет поддерживать психопат-убийца, который знает где вы живёте. И помните: лучший код - тот который работает и который другие могут понять.
Любой дурак может написать код, который поймёт компьютер. Хорошие программисты пишут код, который поймут люди (Martin Fowler)
А лучшие вакансии для разработчиков ищите на hirehi.ru