Демистификация программирования SOLID (часть 1): принцип единой ответственности

Демистификация программирования 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.

Заключение

Таков принцип единой ответственности. Надеюсь, эта статья помогла вам. В следующем я расскажу о принципе открытого-закрытого


Также опубликовано здесь


Оригинал