По мере роста размеров и сложности программных систем их становится все труднее поддерживать и масштабировать. Часто это происходит из-за неудачных проектных решений, которые делают систему жесткой, хрупкой и трудно поддающейся изменению. Это может привести к явлению, известному как «гниение кода», когда кодовая база становится настолько сложной для работы, что разработчики боятся вносить изменения, что приводит к устаревшей системе.
Принципы SOLID представляют собой набор руководств, направленных на решение указанных проблем, и эти принципы таковы:
- Принцип единой ответственности (SRP): "У класса должна быть одна и только одна причина для изменения".
- Принцип открытого-закрытого (OCP): "Объекты должны быть открыты для расширения, но закрыты для модификации".
- Принцип замещения Лискова (LSP): «Подтипы должны быть взаимозаменяемыми для своих базовых типов».
- Принцип разделения интерфейсов (ISP): «Клиента нельзя заставлять реализовывать интерфейсы, которые он не использует».
- Принцип инверсии зависимостей (DIP): «Модули высокого уровня не должны зависеть от модулей низкого уровня, а оба должны зависеть от абстракций». .
В этой статье мы углубимся в первый принцип и рассмотрим остальные принципы в следующих статьях.
Принцип единой ответственности
Давайте подумаем об автомобиле: автомобиль состоит из многих частей, таких как двигатель, трансмиссия, колеса и тормоза. Каждая из этих частей имеет определенную функцию и отвечает за одну задачу. Двигатель отвечает за обеспечение мощности автомобиля, трансмиссия отвечает за переключение передач, колеса отвечают за движение автомобиля, а тормоза отвечают за замедление автомобиля. Каждая часть построена и предназначена для выполнения одной конкретной задачи, и они работают вместе, чтобы заставить автомобиль двигаться. Точно так же каждый класс или модуль в разработке программного обеспечения должен иметь одну и только одну ответственность, и они работают вместе, чтобы заставить программное обеспечение работать.
Теперь давайте подумаем, что произойдет, если мы объединим несколько частей автомобиля в один компонент. Например, вместо отдельных деталей для двигателя, трансмиссии и тормозов у нас будет одна деталь, отвечающая за все три функции. Это сделало бы компонент намного более сложным и трудным в обслуживании. Представьте, что вы пытаетесь починить или заменить только тормоза, когда к ним подключен двигатель и все остальные функции, это было бы сложно и требует много времени.
Точно так же и в разработке программного обеспечения, если мы не следуем принципу единой ответственности, мы получаем классы или модули, несущие множественные обязанности. Это делает код более трудным для понимания, труднее тестировать и труднее поддерживать. Когда изменение необходимо, становится трудно определить, какую часть кода нужно изменить и какое влияние это изменение окажет на остальную часть системы. Кроме того, это может увеличить риск ошибок и затруднить определение источника проблемы.
И в этом заключается идея этого принципа: "У класса должна быть одна и только одна причина для изменения".
Итак, как мы можем реализовать этот принцип?
Выполнение
Начнем с простого примера. Рассмотрим класс Invoice
, который отвечает за управление деталями счета, вычисление общей суммы и печать счета:
class Invoice { constructor(items) { this.items = items; } addItem(item) { this.items.push(item); } calculateTotal() { let total = 0; for (const item of this.items) { total += item.price * item.quantity; } return total; } printInvoice() { console.log("Invoice Details:"); for (const item of this.items) { console.log(`- ${item.name}: $${item.price} x ${item.quantity}`); } console.log(`Total: $${this.calculateTotal()}`); } }
Этот класс нарушает принцип единой ответственности, поскольку у него три обязанности: управление деталями счета, расчет общей суммы и печать счета.
Мы можем реорганизовать этот класс в соответствии с принципом единой ответственности, разделив эти три обязанности на три разных класса: Invoice
, InvoiceCalculator
и InvoicePrinter
:
class Invoice { constructor(items) { this.items = items; } addItem(item) { this.items.push(item); } } class InvoiceCalculator { constructor(items) { this.items = items; } calculateTotal() { let total = 0; for (const item of this.items) { total += item.price * item.quantity; } return total; } } class InvoicePrinter { constructor(items) { this.items = items; } printInvoice() { console.log("Invoice Details:"); for (const item of this.items) { console.log(`- ${item.name}: $${item.price} x ${item.quantity}`); } console.log(`Total: $${this.calculateTotal()}`); } }
В этом переработанном коде класс Invoice
теперь отвечает только за управление деталями счета, класс InvoiceCalculator
отвечает только за расчет общей суммы, а класс InvoicePrinter
отвечает только за печать счета. Это упрощает понимание, поддержку и тестирование кода, а также упрощает внесение изменений в одну обязанность, не затрагивая две другие обязанности.
Чтобы использовать эти классы, мы можем создавать экземпляры каждого класса и передавать между ними необходимую информацию, например:
const invoice = new Invoice([ { name: "Item 1", price: 10, quantity: 2 }, { name: "Item 2", price: 20, quantity: 1 }, ]); const invoiceCalculator = new InvoiceCalculator(invoice.items); const invoicePrinter = new InvoicePrinter(invoice.items); invoice.addItem({ name: "Item 3", price: 5, quantity: 3 }); const total = invoiceCalculator.calculateTotal(); invoicePrinter.printInvoice();
Давайте посмотрим на другой пример. У нас есть веб-приложение, которое отвечает за сохранение данных пользователя в БД:
class UserController { constructor() {} async createUser(request, response) { const user = request.body; const validationErrors = this.validateUser(user); if (validationErrors.length) { return response.status(400).send({ errors: validationErrors }); } const newUser = await this.saveUserToDB(user); return response.status(200).send({ user: newUser }); } validateUser(user) { const errors = []; if (!user.email) { errors.push({ field: 'email', message: 'Email is required' }); } if (!user.password) { errors.push({ field: 'password', message: 'Password is required' }); } return errors; } async saveUserToDB(user) { // code to save user to database } }
Этот пример нарушает принцип единой ответственности, поскольку класс контроллера должен иметь только одну цель: управление связью между клиентом и бизнес-логикой, но вместо этого он имеет три обязанности:
проверка пользовательских данных, сохранение пользователя в базе данных и обработка связи между клиентом и бизнес-логикой.
Чтобы этот код следовал принципу SRP, мы можем реорганизовать его следующим образом:
class UserController { constructor(userRepository, validator) { this.userRepository = userRepository; this.validator = validator; } createUser(user) { if (!this.validator.isValid(user)) { throw new Error('User is not valid'); } this.userRepository.save(user); } } class UserRepository { save(user) { // Save the user to the database } } class Validator { isValid(user) { // Validate the user object return true; } }
Теперь класс UserController
отвечает только за координацию процесса создания пользователя и делегирование задач своим соавторам, таким как userRepository
и validator
. Фактическая работа по сохранению пользователя и проверке пользователя выполняется отдельными классами. Это упрощает поддержку и тестирование кода, поскольку каждый класс несет единственную ответственность и может тестироваться изолированно.
Сложная часть
Мы получаем массу преимуществ от внедрения SRP. с учетом сказанного, для меня самой сложной частью этого принципа является компромисс между сложностью и ремонтопригодностью: найти компромисс между повышенной сложностью (все больше и больше файлов) и улучшенной ремонтопригодностью кодовая база (обязанности распределены по этим разным файлам) представляет собой проблему при применении SRP.
Совет по внедрению
Иногда бывает трудно реализовать этот принцип. Я много думал об этом и понял, что когда я задаю следующий вопрос, я обычно успешно реализую этот принцип:
Указывает ли имя моего класса на то, что он делает? и указывают ли его методы, как он это делает?
С моей точки зрения, имя класса должно указывать на его конечную цель, а его методы должны указывать, как он достигает этой конечной цели. Вопрос выше помог мне во многих случаях, когда я хотел внедрить SRP.
Заключение
Таков принцип единственной ответственности, надеюсь, эта статья помогла вам. В следующем я расскажу о принципе открытого-закрытого.