web-dev-qa-db-ja.com

コールバック(std :: function / std :: bind)とインターフェイス(抽象クラ​​ス)の長所と短所

Boost.Asioを使用してC++ 11でサーバーアプリケーションを作成しています。新しい接続の受け入れを処理するクラス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コメントを「修正」する2つの方法(もっとあると確信しています)を見つけました。つまり、ソケットをどこにでも移動する方法です。私の場合は、Serverインスタンスを所有するクラスインスタンスに戻したいだけです(次に、それをConnectionクラスにラップして、リストに挿入します)。

  1. Serverのコンストラクターには、HandleAcceptで呼び出されるstd::function<void(socket)> OnAcceptというパラメーターがあります。
  2. 抽象クラスIServerHandlerなどを作成します。このクラスには、1つの仮想メソッドOnAcceptがあります。 ServerはコンストラクターのパラメーターとしてIServerHandlerを取り、サーバーインスタンスを所有するクラスインスタンスはIServerHandlerを拡張し、_*this_をパラメーターとしてServerを構築します。

オプション1とオプション2の長所と短所は何ですか?より良いオプションはありますか? Connectionクラス(OnConnectionClosed)でも同じ問題が発生しています。また、システムの設計方法によっては、OnPacketReceivedおよびOnPacketSentコールバックが必要になる場合があります。

35
simon

私はいくつかの理由で最初の方法を強く好みます:

  • インターフェイス/クラス階層を介して概念/機能を表現すると、コードベースの汎用性と柔軟性が低下し、将来的に維持または拡張することがより困難になります。この種の設計では、タイプ(必要な機能を実装するタイプ)に一連の要件が課せられるため、将来の変更が困難になり、システムが変更されたときに失敗する可能性が最も高くなります(基本クラスが変更されたときに何が起こるかを検討してください)このタイプのデザイン)。

  • あなたが呼んだものコールバックアプローチは、ダックタイピングの典型的な例です。サーバークラスは、必要な機能を実装する呼び出し可能なもののみを期待しますそれ以上でもそれ以下でもありません。いいえ「タイプはこの階層に結合する必要があります」条件が必要なので、処理を実装するタイプは完全に無料です

  • また、私が言ったように、サーバーは呼び出し可能なもののみを期待します:それは期待される関数シグネチャを持つものであれば何でもかまいません。これにより、ハンドラーを実装する際の自由度が高まります。グローバル関数、バインドされたメンバー関数、ファンクターなどである可能性があります。

例として標準ライブラリを取り上げます。

  • ほとんどすべての標準ライブラリアルゴリズムは、イテレータ範囲に基づいています。 C++にはiteratorインターフェイスはありません。イテレータは、イテレータの動作を実装する任意のタイプです(逆参照可能、比較可能など)。イテレータタイプは完全に自由で、区別され、分離されています(特定のクラス階層にロックされていません)。

  • 別の例としては、コンパレータがあります。コンパレータとは何ですか。 ブール比較関数のシグネチャを持つもの、2つのパラメーターを受け取り、2つの入力値がポイントから等しい(より小さい、より大きいなど)かどうかを示すブール値を返す呼び出し可能なもの特定の比較基準の見方。 Comparableインターフェースはありません。

46
Manu343726

それはすべてあなたの意図に要約されます。

一方では、機能が特定のタイプに属することをexpectにしたい場合は、仮想関数やメンバーポインターなど、その階層の観点から実装する必要があります。制限この意味で、コードを正しく使用しやすくし、誤って使用しにくくするのに役立つため、優れています。

一方、特定の基本クラスに緊密に結合されているという負担を気にせずに、抽象的な「ここに移動してこれを行う」機能が必要な場合は、明らかに、へのポインタなど、他の何かがより適切です。無料の関数、またはstd :: functionなど。

それはすべて、ソフトウェアの特定の部分の特定の設計により適しているかどうかです。

1
Brandon

多くの場合、特定のタイプにバインドするPREFERを実行します。
したがって、この場合、クラスにIServerHandlerが必要であると宣言すると、クラスを操作するために実装する必要のあるインターフェイスを理解するのに役立ちます。
将来の開発でIServerHandlerに機能を追加すると、クライアント(つまり派生クラス)が開発に遅れずについていくように強制されます。
これは望ましい動作である可能性があります。

1
Ran Regev

どのバージョンのブーストを使用していますか? IMHOがコルーチンを使用する最良の方法です。コードは、より簡単にフォローできます。同期コードのように見えますが、モバイルデバイスから書き込んでいるため、比較できません。

0
Germán Diago