Принципы SOLID в современном C++

Добавлено 10 февраля 2022 в 20:51

Говоря о паттернах, одна аббревиатура кажется наиболее популярной – принципы SOLID. Да, на самом деле SOLID – это аббревиатура, которая означает:

  • Single responsibility principle (принцип единственной ответственности);
  • Open-closed principle (принцип открытости/закрытости);
  • Liskov substitution principle (принцип подстановки Лисков);
  • Interface segregation principle (принцип разделения интерфейса);
  • Dependency inversion principle (принцип инверсии зависимостей).

Слишком много странных терминов, и немного суховато. Возможно, понять их помогут примеры с использованием современного C++.

Внимание, спойлер: это не подробный технический текст. Это просто пример, который пытается использовать непрофессиональные термины, чтобы объяснить сухую тему. Если вы очень серьезно относитесь к этому, то сначала попробуйте почитать соответствующую книгу.

Фото Йонаса фон Верна на Unsplash
Фото Йонаса фон Верна на Unsplash

Хорошо, начнем. Наш пример – система продаж, и мы будем постепенно строить ее для каждого отдельного принципа. Система продаж состоит из класса Order (заказ). В этом классе мы можем обрабатывать объекты класса Item (товары). В общем, мы можем добавить товары в заказ, распечатать объект Order и, наконец, перейти к оплате.

Неоптимизированный код выглядит так:

struct Item {
  Item(std::string_view item, int quantity, float price)
      : item_{item}
      , quantity_{quantity}
      , price_{price} {}

  std::string_view item_;
  int quantity_;
  float price_;
};

Это класс Item. Заказ состоит из объектов Item. Класс Order выглядит следующим образом:

class Order {
 public:
  void AddItem(const Item& new_item);
  [[nodiscard]] float TotalPrice() const;
  void Pay(std::string_view payment_type, std::string_view security_code);
  void PrintOrder() const;

 private:
  std::vector<Item> items_;
  std::string status_{"open"};
};

И вариант использования может быть следующим:

int main() {
  Item item1{"Keyboard", 1, 50.0};
  Item item2{"SSD", 1, 150.0};
  Item item3{"USB cable", 2, 5.0};
  Order an_order{};

  an_order.AddItem(item1);
  an_order.AddItem(item2);
  an_order.AddItem(item3);

  an_order.PrintOrder();

  spdlog::info("The total price is {}", an_order.TotalPrice());
  try {
    an_order.Pay("debit", "09878");
  } catch (const Trouble& t) {
    spdlog::error(t.what());
  }

  try {
    an_order.Pay("credit", "96553");
  } catch (const Trouble& t) {
    spdlog::error(t.what());
  }

  return 0;
}

Ничего интересного, но слишком много кода с запашком. Согласны? Нет? Позвольте мне объяснить вам, почему.

Принцип единственной ответственности (single responsibility principle)

Этот принцип довольно прост. Это означает, что классы и/или методы должны делать одну вещь и делать это наиболее эффективным способом. На жаргоне программистов это переводится как то, что классы должны иметь высокую связность. Это также помогает в повторном использовании сущностей. В нашем неоптимизированном примере у класса Order есть проблема. Метод Pay здесь не к месту. Это должен быть другой объект, который получает в качестве входных данных объект Order и переходит к оплате с соблюдением определенных требований безопасности. Как мы можем исправить ситуацию? Во-первых, давайте создадим новый класс заказа и назовем его NewOrder. В этом классе больше нет метода Pay. Класс Item менять не нужно. Мы также добавили новую переменную-член для представления уникального кода заказа.

struct NewOrder {

  void AddItem(const Item& new_item);
  [[nodiscard]] float TotalPrice() const;
  void SetStatus(Status status);
  void PrintOrder() const;
  [[nodiscard]] const uuids::uuid &GetId() const;

 private:
  uuids::uuid id {uuids::uuid_system_generator{}()};
  std::vector<Item> items_;
  Status status_{Status::Open};
};

и

void NewOrder::AddItem(const Item& new_item){
  items_.push_back(new_item);
}

void NewOrder::PrintOrder() const {
  spdlog::info("The id of the order is {}, with items:", uuids::to_string(id));
  for (auto& item : items_) {
    spdlog::info(item.item_);
  }
  spdlog::info("and the status is {0}", StatusToString(status_));
}

float NewOrder::TotalPrice() const {
  float total {0.0};
  for (auto& item : items_) {
    total += static_cast<float>(item.quantity_)*item.price_;
  }
  return total;
}

void NewOrder::SetStatus(Status status) {
  status_ = status;
}

const uuids::uuid &NewOrder::GetId() const {
  return id;
}

Для оплаты мы добавим новый класс под названием PaymentProcessor. Обязанность этого класса состоит в том, чтобы выполнять одну вещь, оплачивать счета! Он получает объект Order и обрабатывает платеж, ничего больше.

struct PaymentProcessor {
  explicit PaymentProcessor(NewOrder &new_order)
      : new_order_{new_order} {}

  void DisplayInfo() const;
  void PayDebit(std::string_view security_code) const;
  void PayCredit(std::string_view security_code) const;
  void PayPaypal(std::string_view security_code) const;

 private:
  NewOrder &new_order_;
};

и

void PaymentProcessor::PayDebit(std::string_view security_code) const {
  spdlog::info("Processing debit payment type");
  spdlog::info("Verifying security code: {0}", security_code);
  new_order_.SetStatus(Status::Paid);
}

void PaymentProcessor::PayCredit(std::string_view security_code) const {
  spdlog::info("Processing credit payment type");
  spdlog::info("Verifying security code: {0}", security_code);
  new_order_.SetStatus(Status::Paid);
}

void PaymentProcessor::PayPaypal(std::string_view security_code) const {
  spdlog::info("Processing paypal payment type");
  spdlog::info("Verifying security code: {0}", security_code);
  new_order_.SetStatus(Status::Paid);
}

void PaymentProcessor::DisplayInfo() const {
  spdlog::info("Payment processor for order {0}", to_string(new_order_.GetId()));
}

Теперь пример использования выглядит так:

int main() {
  Item item1{"Keyboard", 1, 50.0};
  Item item2{"SSD", 1, 150.0};
  Item item3{"USB cable", 2, 5.0};

  NewOrder an_order{};

  an_order.AddItem(item1);
  an_order.AddItem(item2);
  an_order.AddItem(item3);

  an_order.PrintOrder();

  PaymentProcessor payment_processor1{an_order};
  payment_processor1.DisplayInfo();
  payment_processor1.PayCredit("65379");

  PaymentProcessor payment_processor2{an_order};
  payment_processor2.DisplayInfo();
  payment_processor2.PayDebit("98664");

  PaymentProcessor payment_processor3{an_order};
  payment_processor3.DisplayInfo();
  payment_processor3.PayPaypal("12245");

  return 0;
}

Хм, всё логично. Мы создали объект Order и можем произвести платеж, выбрав один из трех различных способов оплаты, используя соответствующий объект PaymentProcessor. Честно говоря, я думаю, что мы идем в правильном направлении.

Принцип открытости/закрытости (open-closed principle)

Это кажется странным. Смысл открытости в том, что структура кода (назовем её архитектурой) должна быть открыта для расширений (путем добавления новых функций), но закрыта для модификаций. Последнее означает, что нам не нужно изменять существующую кодовую базу для добавления новых функций, мы просто строим поверх нее или, что еще лучше, расширяем ее. В нашем случае, у нашей системы продаж с этим есть проблемы. Если мы хотим добавить новый способ оплаты (например, Paypal), нам нужно изменить класс PaymentProcessor. Что недопустимо. Решение исходит из создания классов и подклассов, связанных наследованием. Звучит знакомо?

При этом классы Item и NewOrder остаются прежними. Нам нужно создать абстрактный класс для обработчика платежей PaymentProcessor.

struct PaymentProcessorAbstractOC {
  virtual void Pay(std::string_view security_code) const = 0;
  virtual void DisplayInfo() const = 0;
  virtual ~PaymentProcessorAbstractOC() = default;
};

Теперь подклассы наследуются от родителя и выглядят следующим образом, по одному для каждого отдельного способа оплаты.

struct PaymentProcessorCreditOC final : public PaymentProcessorAbstractOC {
  explicit PaymentProcessorCreditOC(NewOrder &new_order)
      : new_order_{new_order} {}

  void Pay(std::string_view security_code) const override {
    spdlog::info("Processing credit payment type");
    spdlog::info("Verifying security code: {0}", security_code);
    new_order_.SetStatus(Status::Paid);
  }

  void DisplayInfo() const override {
    spdlog::info("Credit payment processor for order {0}", to_string(new_order_.GetId()));
  }

 private:
  NewOrder &new_order_;
};
struct PaymentProcessorDebitOC final : public PaymentProcessorAbstractOC {
  explicit PaymentProcessorDebitOC(NewOrder &new_order)
      : new_order_{new_order} {}

  void Pay(std::string_view security_code) const override {
    spdlog::info("Processing debit payment type");
    spdlog::info("Verifying security code: {0}", security_code);
    new_order_.SetStatus(Status::Paid);
  }

  void DisplayInfo() const override {
    spdlog::info("Debit payment processor for order {0}", to_string(new_order_.GetId()));
  }

 private:
  NewOrder &new_order_;
};

И давайте добавим новый способ оплаты, который использует Paypal.

struct PaymentProcessorPaypalOC final : public PaymentProcessorAbstractOC {
  explicit PaymentProcessorPaypalOC(NewOrder &new_order)
      : new_order_{new_order} {}

  void Pay(std::string_view security_code) const override {
    spdlog::info("Processing paypal payment type");
    spdlog::info("Verifying security code: {0}", security_code);
    new_order_.SetStatus(Status::Paid);
  }

  void DisplayInfo() const override {
    spdlog::info("Paypal payment processor for order {0}", to_string(new_order_.GetId()));
  }

 private:
  NewOrder &new_order_;
};

Всё просто. После этого пример использования будет выглядеть так:

int main() {
  Item item1{"Keyboard", 1, 50.0};
  Item item2{"SSD", 1, 150.0};
  Item item3{"USB cable", 2, 5.0};
  NewOrder an_order{};

  an_order.AddItem(item1);
  an_order.AddItem(item2);
  an_order.AddItem(item3);

  an_order.PrintOrder();

  PaymentProcessorDebitOC processor1{an_order};
  processor1.DisplayInfo();
  processor1.Pay("65379");

  PaymentProcessorCreditOC processor2{an_order};
  processor2.DisplayInfo();
  processor2.Pay("65379");

  PaymentProcessorPaypalOC processor3{an_order};
  processor3.DisplayInfo();
  processor3.Pay("65379");

  return 0;
}

Никаких серьезных отличий. В этом случае нам просто нужно создать различные виды объектов PaymentProcessor в зависимости от сценария.

Принцип подстановки Лисков (Liskov substitution principle)

Что теперь? Я ничего не могу понять из названия. Этот принцип немного суше. По сути, это означает, что подклассы должны иметь возможность заменять друг друга без изменения корректности программы или, лучше сказать, концепции. Чтобы объяснить это, нам нужно вернуться к нашей системе продаж. В классах PaymentProcessor, а точнее в части авторизации есть проблема. В Paypal нет кода безопасности, кроме проверки адреса электронной почты. Мы используем его, делая вид, что код безопасности – это электронная почта, но это неверно. Мы подделываем поведение. Это означает, что он злоупотребляет принципом подстановки Лисков. Нам нужно подумать о чем-то, что даст нам эту гибкость. Решение может быть простым: удалить код защиты из аргументов метода Pay и добавить его в качестве переменной-члена подклассов PaymentProcessor. В этом случае различные обработчики платежей будут выглядеть следующим образом:

struct PaymentProcessorCreditLiskov final : public PaymentProcessorAbstractLiskov {
  explicit PaymentProcessorCreditLiskov(NewOrder &new_order, std::string_view security_code)
      : new_order_{new_order}
      , security_code_{security_code} {}

  void Pay() const override {
    spdlog::info("Processing credit payment type");
    spdlog::info("Verifying security code: {0}", security_code_);
    new_order_.SetStatus(Status::Paid);
  }

  void DisplayInfo() const override {
    spdlog::info("Credit payment processor for order {0}", to_string(new_order_.GetId()));
  }

 private:
  NewOrder &new_order_;
  std::string_view security_code_;
};

И в конечном итоге обработчик платежей через Paypal станет таким:

struct PaymentProcessorPaypalLiskov final : public PaymentProcessorAbstractLiskov {
  explicit PaymentProcessorPaypalLiskov(NewOrder &new_order, std::string_view email_address)
      : new_order_{new_order}
      , email_address_{email_address} {}

  void Pay() const override {
    spdlog::info("Processing paypal payment type");
    spdlog::info("Verifying security code: {0}", email_address_);
    new_order_.SetStatus(Status::Paid);
  }

  void DisplayInfo() const override {
    spdlog::info("Paypal payment processor for order {0}", to_string(new_order_.GetId()));
  }

 private:
  NewOrder &new_order_;
  std::string_view email_address_;
  bool verified_{false};
};

Тогда пример использования становится понятным:

int main() {
  Item item1{"Keyboard", 1, 50.0};
  Item item2{"SSD", 1, 150.0};
  Item item3{"USB cable", 2, 5.0};
  NewOrder an_order{};

  an_order.AddItem(item1);
  an_order.AddItem(item2);
  an_order.AddItem(item3);

  an_order.PrintOrder();

  PaymentProcessorDebitLiskov processor1{an_order, "65379"};
  try {
    processor1.DisplayInfo();
    processor1.Pay();
  } catch (const std::exception &e) {
    spdlog::error(e.what());
  }

  PaymentProcessorCreditLiskov processor2{an_order, "65379"};
  try {
    processor2.DisplayInfo();
    processor2.Pay();
  } catch (const std::exception &e) {
    spdlog::error(e.what());
  }

  PaymentProcessorPaypalLiskov processor3{an_order, "angelos@in.gr"};
  try {
    processor3.DisplayInfo();
    processor3.Pay();
  } catch (const std::exception &e) {
    spdlog::error(e.what());
  }

  return 0;
}

Это доказывает, что нет нарушения использования подклассов. Всё используется так, как задумано и положено. Без обмана.

Принцип разделения интерфейса (interface segregation principle)

Это четвертый принцип, который опять же означает, что не стоит слишком обобщать. Лучше иметь несколько конкретных интерфейсов вместо одного универсального. Требуется пояснение? Ну, чтобы объяснить это, мы добавили в PaymentProcessor еще одну вещь. Это SMS-авторизация (двухфакторная аутентификация). Чтобы обеспечить соответствие каждого метода оплаты этому требованию, правильным местом для добавления этой возможности будет абстрактный класс.

struct PaymentProcessorAbstractLiskov {
  virtual void AuthSMS(std::string_view sms_code) = 0;
  virtual void Pay() const = 0;
  virtual void DisplayInfo() const = 0;
  virtual ~PaymentProcessorAbstractLiskov() = default;
};

А затем реализация в каждом обработчике так:

struct PaymentProcessorDebitLiskov final : public PaymentProcessorAbstractLiskov {
  explicit PaymentProcessorDebitLiskov(NewOrder &new_order, std::string_view security_code)
      : new_order_{new_order}
      , security_code_{security_code} {}

  void AuthSMS(std::string_view sms_code) override {
    spdlog::info("Verifying SMS code {0}", sms_code);
    verified_ = true;
  }

  void Pay() const override {
    if (!verified_) {
      throw Trouble{"Not authorised"};
    }
    spdlog::info("Processing debit payment type");
    spdlog::info("Verifying security code: {0}", security_code_);
    new_order_.SetStatus(Status::Paid);
  }

  void DisplayInfo() const override {
    spdlog::info("Debit payment processor for order {0}", to_string(new_order_.GetId()));
  }

 private:
  NewOrder &new_order_;
  std::string_view security_code_;
  bool verified_{false};
};

или в случае оплаты с кредитной карты, которая не поддерживается, это будет выглядеть так:

struct PaymentProcessorCreditLiskov final : public PaymentProcessorAbstractLiskov {
  explicit PaymentProcessorCreditLiskov(NewOrder &new_order, std::string_view security_code)
      : new_order_{new_order}
      , security_code_{security_code} {}

  void AuthSMS(std::string_view sms_code) override {
    throw Trouble("Credit card payments don't support SMS code authorization.");
  }

  void Pay() const override {
    spdlog::info("Processing credit payment type");
    spdlog::info("Verifying security code: {0}", security_code_);
    new_order_.SetStatus(Status::Paid);
  }

  void DisplayInfo() const override {
    spdlog::info("Credit payment processor for order {0}", to_string(new_order_.GetId()));
  }

 private:
  NewOrder &new_order_;
  std::string_view security_code_;
};

Хотя это проблема. Кажется, что наш абстрактный класс PaymentProcessor слишком общий. Он не может элегантно обрабатывать различные ситуации. И знаете, что? Это даже нарушает принцип подстановки Лисков, потому что в случае кредитной карты мы фактически вызываем исключение, чтобы избежать этого. Решение состоит в том, чтобы перейти к более точным интерфейсам, где возможна поддержка случаев с более высоким разрешением. Первый подход заключается в использовании наследования. Неудивительно. Нам нужно создать субинтерфейс абстрактного класса PaymentProcessor, который будет поддерживать SMS-авторизацию. Абстрактный класс PaymentProcessor, в свою очередь, возвращается в свое предыдущее состояние, в котором двухфакторная авторизация не была определена. Рассмотрим это решение. Ниже приведен абстрактный класс PaymentProcessor.

struct PaymentProcessorAbstractIS {
  virtual void Pay() const = 0;
  virtual void DisplayInfo() const = 0;
  virtual ~PaymentProcessorAbstractIS() = default;
};

Более специализированный интерфейс, поддерживающий авторизацию, выглядит так:

struct PaymentProcessorAbstractSMS : public PaymentProcessorAbstractIS {
  virtual void AuthSMS(std::string_view sms_code) = 0;
};

и отдельные классы обработчиков платежей будут использовать всё, что им подходит (самостоятельно):

struct PaymentProcessorCreditISInh final : public PaymentProcessorAbstractIS {
  explicit PaymentProcessorCreditISInh(const NewOrder &new_order, std::string_view security_code)
      : new_order_{std::make_shared<NewOrder>(new_order)}
      , security_code_{security_code} {}

  void Pay() const override {
    spdlog::info("Processing credit payment type");
    spdlog::info("Verifying security code: {0}", security_code_);
    new_order_->SetStatus(Status::Paid);
  }

  void DisplayInfo() const override {
    spdlog::info("Credit payment processor for order {0}", to_string(new_order_->GetId()));
  }

 private:
  std::shared_ptr<NewOrder> new_order_;
  std::string_view security_code_;
};

Обработчик оплатой кредиткой теперь чистый. Ничего сложного. Он наследуется от исходного интерфейса, не возясь с функциональностью авторизации. Другие обработчики будут подклассами более конкретного интерфейса, поддерживающего двухфакторную аутентификацию. Неплохо ведь?

struct PaymentProcessorDebitISInh final : public PaymentProcessorAbstractSMS {
  explicit PaymentProcessorDebitISInh(const NewOrder &new_order, std::string_view security_code)
      : new_order_{std::make_shared<NewOrder>(new_order)}
      , security_code_{security_code} {}

  void AuthSMS(std::string_view sms_code) override {
    spdlog::info("Verifying SMS code {0}", sms_code);
    verified_ = true;
  }

  void Pay() const override {
    if (!verified_) {
      throw Trouble{"Not authorised"};
    }
    spdlog::info("Processing debit payment type");
    spdlog::info("Verifying security code: {0}", security_code_);
    new_order_->SetStatus(Status::Paid);
  }

  void DisplayInfo() const override {
    spdlog::info("Debit payment processor for order {0}", to_string(new_order_->GetId()));
  }

 private:
  std::shared_ptr<NewOrder> new_order_;
  std::string_view security_code_;
  bool verified_{false};
};
struct PaymentProcessorPaypalISComp final : public PaymentProcessorAbstractIS {
  explicit PaymentProcessorPaypalISComp(const NewOrder &new_order, std::string_view email_address, std::shared_ptr<SMSAuthorizer> sms_authorizer)
      : new_order_{std::make_shared<NewOrder>(new_order)}
      , email_address_{email_address}
      , sms_authorizer_{std::move(sms_authorizer)} {}

  void Pay() const override {
    if (!sms_authorizer_->IsAuthorized()) {
      throw Trouble{"Not authorised"};
    }
    spdlog::info("Processing paypal payment type");
    spdlog::info("Verifying security code: {0}", email_address_);
    new_order_->SetStatus(Status::Paid);
  }

  void DisplayInfo() const override {
    spdlog::info("Paypal payment processor for order {0}", to_string(new_order_->GetId()));
  }

 private:
  std::shared_ptr<NewOrder> new_order_;
  std::string_view email_address_;
  std::shared_ptr<SMSAuthorizer> sms_authorizer_;
};

Теперь пример использования будет таким:

int main() {
  Item item1{"Keyboard", 1, 50.0};
  Item item2{"SSD", 1, 150.0};
  Item item3{"USB cable", 2, 5.0};
  NewOrder an_order{};

  an_order.AddItem(item1);
  an_order.AddItem(item2);
  an_order.AddItem(item3);

  an_order.PrintOrder();

  PaymentProcessorDebitISInh processor1{an_order, "65379"};
  try {
    processor1.DisplayInfo();
    processor1.AuthSMS("264423");
    processor1.Pay();
  } catch (const std::exception &e) {
    spdlog::error(e.what());
  }

  PaymentProcessorCreditISInh processor2{an_order, "65379"};
  try {
    processor2.DisplayInfo();
    processor2.Pay();
  } catch (const std::exception &e) {
    spdlog::error(e.what());
  }

  PaymentProcessorPaypalISInh processor3{an_order, "angelos@in.gr"};
  try {
    processor3.DisplayInfo();
    processor3.AuthSMS("764423");
    processor3.Pay();
  } catch (const std::exception &e) {
    spdlog::error(e.what());
  }

  return 0;
}

Таким образом, мы видим, что можем правильно использовать обработчики платежей. В случаей с оплатой кредитной картой нет необходимости в фиктивных функциях, потому что этот класс наследует интерфейс без функциональности авторизации. Таким образом, мы решаем проблему разделения интерфейса, а также созданное нарушение принципа подстановки Лисков.

Параллельно с наследованием существует композиционный подход. Это еще один метод, который в большинстве случаев приводит к более чистому коду, избегая длинных деревьев наследования, пытающихся охватить все возможные вариации. В этом случае мы снова удаляем из абстрактного класса PaymentProcessor функцию авторизации.

struct PaymentProcessorAbstractIS {
  virtual void Pay() const = 0;
  virtual void DisplayInfo() const = 0;
  virtual ~PaymentProcessorAbstractIS() = default;
};

Затем мы определяем двухфакторный (SMS) авторизатор как конкретный класс:

struct SMSAuthorizer {

  void VerifyCode(std::string_view code) {
    spdlog::debug("Verified SMS {0}", code);
    authorized_ = true;
  }

  [[nodiscard]] bool IsAuthorized() const {
    return authorized_;
  }

 private:
  bool authorized_{false};

};

Таким образом, ситуация с отдельными классами обработчиков платеже снова ясна. Для тех, которым требуется аутентификация, мы добавляем в качестве переменной-члена ссылку на класс авторизатора SMS.

struct PaymentProcessorDebitISComp final : public PaymentProcessorAbstractIS {
  explicit PaymentProcessorDebitISComp(const NewOrder &new_order, std::string_view security_code, std::shared_ptr<SMSAuthorizer> sms_authorizer)
      : new_order_{std::make_shared<NewOrder>(new_order)}
      , security_code_{security_code}
      , sms_authorizer_{std::move(sms_authorizer)}
  {}

  void Pay() const override {
    if (!sms_authorizer_->IsAuthorized()) {
      throw Trouble{"Not authorised"};
    }
    spdlog::info("Processing debit payment type");
    spdlog::info("Verifying security code: {0}", security_code_);
    new_order_->SetStatus(Status::Paid);
  }

  void DisplayInfo() const override {
    spdlog::info("Debit payment processor for order {0}", to_string(new_order_->GetId()));
  }

 private:
  std::shared_ptr<NewOrder> new_order_;
  std::string_view security_code_;
  std::shared_ptr<SMSAuthorizer> sms_authorizer_;
};

и

struct PaymentProcessorPaypalISComp final : public PaymentProcessorAbstractIS {
  explicit PaymentProcessorPaypalISComp(const NewOrder &new_order, std::string_view email_address, std::shared_ptr<SMSAuthorizer> sms_authorizer)
      : new_order_{std::make_shared<NewOrder>(new_order)}
      , email_address_{email_address}
      , sms_authorizer_{std::move(sms_authorizer)} {}

  void Pay() const override {
    if (!sms_authorizer_->IsAuthorized()) {
      throw Trouble{"Not authorised"};
    }
    spdlog::info("Processing paypal payment type");
    spdlog::info("Verifying security code: {0}", email_address_);
    new_order_->SetStatus(Status::Paid);
  }

  void DisplayInfo() const override {
    spdlog::info("Paypal payment processor for order {0}", to_string(new_order_->GetId()));
  }

 private:
  std::shared_ptr<NewOrder> new_order_;
  std::string_view email_address_;
  std::shared_ptr<SMSAuthorizer> sms_authorizer_;
};

А в обработчиках, которые не поддерживают этот функционал, например в оплате кредитной картой, мы просто игнорируем это. Такой переменной-члена в них нет:

struct PaymentProcessorCreditISComp final : public PaymentProcessorAbstractIS {
  explicit PaymentProcessorCreditISComp(const NewOrder &new_order, std::string_view security_code)
      : new_order_{std::make_shared<NewOrder>(new_order)}
      , security_code_{security_code} {}

  void Pay() const override {
    spdlog::info("Processing credit payment type");
    spdlog::info("Verifying security code: {0}", security_code_);
    new_order_->SetStatus(Status::Paid);
  }

  void DisplayInfo() const override {
    spdlog::info("Credit payment processor for order {0}", to_string(new_order_->GetId()));
  }

 private:
  std::shared_ptr<NewOrder> new_order_;
  std::string_view security_code_;
};

Теперь всё чисто. Таким образом, пример использования будет выглядеть так:

int main() {
  Item item1{"Keyboard", 1, 50.0};
  Item item2{"SSD", 1, 150.0};
  Item item3{"USB cable", 2, 5.0};
  NewOrder an_order{};

  an_order.AddItem(item1);
  an_order.AddItem(item2);
  an_order.AddItem(item3);

  an_order.PrintOrder();

  auto authorizer1 = std::make_shared<SMSAuthorizer>();
  PaymentProcessorDebitISComp processor1{an_order, "65379", authorizer1};
  try {
    processor1.DisplayInfo();
    authorizer1->VerifyCode("7987356");
    processor1.Pay();
  } catch (const std::exception &e) {
    spdlog::error(e.what());
  }

  PaymentProcessorCreditISComp processor2{an_order, "65379"};
  try {
    processor2.DisplayInfo();
    processor2.Pay();
  } catch (const std::exception &e) {
    spdlog::error(e.what());
  }

  auto authorizer3 = std::make_shared<SMSAuthorizer>();
  PaymentProcessorPaypalISComp processor3{an_order, "angelos@in.gr", authorizer3};
  try {
    processor3.DisplayInfo();
    authorizer3->VerifyCode("9778555");
    processor3.Pay();
  } catch (const std::exception &e) {
    spdlog::error(e.what());
  }

  return 0;
}

Здесь нет никакой магии. В каждом случае, поддерживающем двухфакторную аутентификацию, мы создаем объекты из соответствующих классов и передаем их в объекты PaymentProcessor. Они работают так, как ожидалось.

Принцип инверсии зависимостей (dependency inversion principle)

Уф, мы подходим к концу, пятому и последнему пункту. Значение инверсии зависимости покажется вам знакомым. Дело в том, что мы хотим, чтобы наши классы зависели от абстрактных классов (интерфейсов), а не от конкретных. Да, это должно быть очевидно. Мы хотим создавать гибкие и открытые архитектуры с повторно используемыми сущностями. Но это не относится к авторизатору SMS в нашем примере. Классы обработчиков платежей зависят от конкретного авторизатора (SMS), а не от красивой абстракции. Как это решить? Легко. Давайте создадим абстрактный класс авторизатора:

struct Authorizer {
  [[nodiscard]] virtual bool IsAuthorized() const = 0;
  virtual ~Authorizer() = default;
};

Абстрактный класс PaymentProcessor остается прежним (что означает отсутствие каких-либо сведений об авторизации).

struct PaymentProcessorAbstractIS {
  virtual void Pay() const = 0;
  virtual void DisplayInfo() const = 0;
  virtual ~PaymentProcessorAbstractIS() = default;
};

Имея всю эту функциональность, пришло время внедрить помимо SMS двухфакторной аутентификации еще и проверку пользователя, что тот не является ботом. Оба они наследуют абстрактный класс Authorizer.

struct SMSAuthorizerDI final : public Authorizer  {

  void VerifyCode(std::string_view code) {
    spdlog::debug("Verified SMS {0}", code);
    authorized_ = true;
  }

  [[nodiscard]] bool IsAuthorized() const override {
    return authorized_;
  }

 private:
  bool authorized_{false};

};

и

struct NotARobotDI final : public Authorizer  {

  void VerifyCode() {
    spdlog::debug("Are you a robot? Naa");
    authorized_ = true;
  }

  [[nodiscard]] bool IsAuthorized() const override {
    return authorized_;
  }

 private:
  bool authorized_{false};

};

Теперь наши классы PaymentProcessor зависят от абстрактного класса Authorizer, а не от какой-либо конкретной реализации. Давайте посмотрим:

struct PaymentProcessorDebitDIComp final : public PaymentProcessorAbstractIS {
  explicit PaymentProcessorDebitDIComp(const NewOrder &new_order, std::string_view security_code, std::shared_ptr<Authorizer> authorizer)
      : new_order_{std::make_shared<NewOrder>(new_order)}
      , security_code_{security_code}
      , authorizer_{std::move(authorizer)}
  {}

  void Pay() const override {
    if (!authorizer_->IsAuthorized()) {
      throw Trouble{"Not authorised"};
    }
    spdlog::info("Processing debit payment type");
    spdlog::info("Verifying security code: {0}", security_code_);
    new_order_->SetStatus(Status::Paid);
  }

  void DisplayInfo() const override {
    spdlog::info("Debit payment processor for order {0}", to_string(new_order_->GetId()));
  }

 private:
  std::shared_ptr<NewOrder> new_order_;
  std::string_view security_code_;
  std::shared_ptr<Authorizer> authorizer_;
};

и

struct PaymentProcessorPaypalDIComp final : public PaymentProcessorAbstractIS {
  explicit PaymentProcessorPaypalDIComp(const NewOrder &new_order, std::string_view email_address, std::shared_ptr<Authorizer> authorizer)
      : new_order_{std::make_shared<NewOrder>(new_order)}
      , email_address_{email_address}
      , authorizer_{std::move(authorizer)} {}

  void Pay() const override {
    if (!authorizer_->IsAuthorized()) {
      throw Trouble{"Not authorised"};
    }
    spdlog::info("Processing paypal payment type");
    spdlog::info("Verifying security code: {0}", email_address_);
    new_order_->SetStatus(Status::Paid);
  }

  void DisplayInfo() const override {
    spdlog::info("Paypal payment processor for order {0}", to_string(new_order_->GetId()));
  }

 private:
  std::shared_ptr<NewOrder> new_order_;
  std::string_view email_address_;
  std::shared_ptr<Authorizer> authorizer_;
};

Обработчик оплаты кредиткой остается прежним (не показан), так как в нем нам не нужно поддерживать авторизацию. Пример использования выглядит следующим образом:

int main() {
  Item item1{"Keyboard", 1, 50.0};
  Item item2{"SSD", 1, 150.0};
  Item item3{"USB cable", 2, 5.0};
  NewOrder an_order{};

  an_order.AddItem(item1);
  an_order.AddItem(item2);
  an_order.AddItem(item3);

  an_order.PrintOrder();

  auto authorizer1 = std::make_shared<SMSAuthorizerDI>();
  PaymentProcessorDebitDIComp processor1{an_order, "65379", authorizer1};
  try {
    processor1.DisplayInfo();
    authorizer1->VerifyCode("7987356");
    processor1.Pay();
  } catch (const std::exception &e) {
    spdlog::error(e.what());
  }

  PaymentProcessorCreditDIComp processor2{an_order, "65379"};
  try {
    processor2.DisplayInfo();
    processor2.Pay();
  } catch (const std::exception &e) {
    spdlog::error(e.what());
  }

  auto authorizer3 = std::make_shared<NotARobotDI>();
  PaymentProcessorPaypalDIComp processor3{an_order, "angelos@in.gr", authorizer3};
  try {
    processor3.DisplayInfo();
    authorizer3->VerifyCode();
    processor3.Pay();
  } catch (const std::exception &e) {
    spdlog::error(e.what());
  }

  return 0;
}

Просто красиво. Максимальная гибкость. Наши платежные системы могут использовать любые виды авторизаторов, которые придерживаются общего интерфейса Authorizer.

Заключение

Поначалу принципы SOLID могут показаться сложными, но если вы сравните неоптимизированную версию с последней, вы быстро поймете, что мы говорим о красавице и чудовище. Обычные инструменты, которое мы используем, – это абстрактные классы (интерфейсы), наследование и (почему бы и нет) композиция. Попробуйте постепенно интегрировать некоторые из этих методов разработки в ваш код, и вы увидите, что качество кода быстро повысится.

Теги

C++ / CppПрограммирование

На сайте работает сервис комментирования DISQUS, который позволяет вам оставлять комментарии на множестве сайтов, имея лишь один аккаунт на Disqus.com.

В случае комментирования в качестве гостя (без регистрации на disqus.com) для публикации комментария требуется время на премодерацию.