Демистификация программирования SOLID (часть 1): принцип единой ответственности
14 февраля 2023 г.По мере роста размеров и сложности программных систем их становится все труднее поддерживать и масштабировать. Часто это происходит из-за неудачных проектных решений, которые делают систему жесткой, хрупкой и трудно поддающейся изменению. Это может привести к явлению, известному как «гниение кода», когда кодовая база становится настолько сложной для работы, что разработчики боятся вносить изменения, что приводит к устаревшей системе.
Принципы 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
:< /p>
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
и валидатор
. . Фактическая работа по сохранению пользователя и проверке пользователя выполняется отдельными классами. Это упрощает поддержку и тестирование кода, поскольку каждый класс несет единственную ответственность и может тестироваться изолированно.
Сложная часть
Мы получаем много преимуществ от внедрения SRP. С учетом сказанного, для меня самой сложной частью этого принципа является компромисс между сложностью и ремонтопригодностью: поиск компромисса между повышенной сложностью (все больше и больше файлов) и улучшенной ремонтопригодностью кодовая база (обязанности распределены по разным файлам) представляет собой проблему при применении SRP.
Совет по реализации
Иногда бывает сложно реализовать этот принцип. Я много думал об этом и понял, что когда я задаю следующий вопрос, я обычно успешно реализую этот принцип:
<цитата>Указывает ли имя моего класса на то, что он делает? и указывают ли его методы, как он это делает?
С моей точки зрения, имя класса должно указывать на его конечную цель, а его методы должны указывать, как он достигает этой конечной цели. Приведенный выше вопрос помог мне во многих случаях, когда я хотел внедрить SRP.
Заключение
Таков принцип единой ответственности. Надеюсь, эта статья помогла вам. В следующем я расскажу о принципе открытого-закрытого
Также опубликовано здесь
Оригинал