Подтвердить что ты не робот

Плюсы и минусы обратного вызова (std:: function/std:: bind) против интерфейса (абстрактный класс)

Я создаю серверное приложение на С++ 11 с помощью Boost.Asio. Я создал класс Server, который заботится о принятии новых соединений. Это в основном просто:

void Server::Accept() {
  socket_.reset(new boost::asio::ip::tcp::socket(*io_service_));
  acceptor_.async_accept(*socket_,
                         boost::bind(&Server::HandleAccept, this, boost::asio::placeholders::error));
}

void Server::HandleAccept(const boost::system::error_code& error) {
  if (!error) {
    // TODO
  } else {
    TRACE_ERROR("Server::HandleAccept: Error!");
  }
  Accept();
}

Я нашел два способа (я уверен, что их больше), чтобы "исправить" комментарий TODO, т.е. переместить сокет туда, куда он должен идти. В моем случае я просто хочу вернуться к экземпляру класса, которому принадлежит экземпляр Server (который затем переносит его в класс Connection и вставляет его в список).

  • Server имеет параметр в своем конструкторе: std::function<void(socket)> OnAccept, который вызывается в HandleAccept.
  • Я создаю абстрактный класс IServerHandler или что угодно, у которого есть один виртуальный метод OnAccept. Server принимает IServerHandler как параметр в своем конструкторе, а экземпляр класса, владеющий экземпляром сервера, расширяет IServerHandler и строит Server с параметром *this.

Каковы плюсы и минусы варианта 1 по сравнению с вариантом 2? Есть ли лучшие варианты? У меня такая же проблема в классе Connection (OnConnectionClosed). Кроме того, в зависимости от того, как я решаю создать систему, может потребоваться обратный вызов OnPacketReceived и OnPacketSent.

4b9b3361

Ответ 1

Я предпочитаю первый способ по нескольким причинам:

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

  • То, что вы назвали методом обратного вызова, - это просто классический пример утиной печати. Класс сервера ожидает только вызываемую вещь, которая реализует требуемую функциональность, не более, не менее. Нет необходимости связывать ваш тип с этой иерархией, поэтому тип, который реализует обработку, полностью свободен.

  • Кроме того, как я уже сказал, сервер ожидает только вызываемой вещи: это может быть что угодно с ожидаемой сигнатурой функции. Это дает пользователю больше свободы при реализации обработчика. Может быть глобальной функцией, связанной функцией-членом, функтором и т.д.

Возьмите стандартную библиотеку в качестве примера:

  • Почти все стандартные библиотечные алгоритмы основаны на диапазонах итераторов. В С++ нет интерфейса iterator. Итератор - это любой тип, который реализует поведение итератора (будучи различимым, сопоставимым и т.д.). Типы итераторов полностью бесплатны, различны и развязаны (не заблокированы для данной иерархии классов).

  • Другим примером может быть компаратор: что такое компаратор? Является только что-либо с сигнатурой функции boolean сравнения, что-то вызываемое, которое принимает два параметра и возвращает логическое значение, говорящее, что если два входных значения равны (меньше, больше и т.д.) из точки с точки зрения конкретных критериев сравнения. Нет интерфейса Comparable.

Ответ 2

Просто упомянем, что во многих случаях вы привязываете PREFER к определенному типу.
Поэтому в этом случае объявление о том, что ваш класс ДОЛЖЕН иметь IServerHandler, поможет вам и другим разработчикам понять, какой интерфейс они должны реализовать для работы с вашим классом.
В будущем при добавлении дополнительных функций в IServerHandler вы вынуждаете своих клиентов (т.е. производные классы) идти в ногу с вашей разработкой.
Это может быть желаемое поведение.

Ответ 3

Все это сводится к вашим намерениям.

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

С другой стороны, если вы просто хотите, чтобы какая-то абстрактная функция "идите сюда и делайте это", не заботясь о том, чтобы она была тесно связана с определенным базовым классом, тогда ясно, что что-то еще будет более подходящим, как указатель на свободную функцию или функцию std:: и т.д.

Все это касается того, что больше подходит для конкретного дизайна любой конкретной части вашего программного обеспечения.

Ответ 4

Какую версию вы используете? Лучший способ ИМХО использует сопрограммы. Код будет более простым. Это будет выглядеть как синхронный код, но теперь я не могу дать сравнение, так как я пишу с мобильного устройства.