web-dev-qa-db-fra.com

Avantages et inconvénients d'un rappel (std :: function/std :: bind) par rapport à une interface (classe abstraite)

Je crée une application serveur en C++ 11 à l'aide de Boost.Asio. J'ai créé une classe, Server, qui accepte de nouvelles connexions. C'est fondamentalement juste:

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();
}

J'ai trouvé deux façons (je suis sûr qu'il y en a d'autres) de "réparer" le commentaire TODO, c'est-à-dire de déplacer le socket vers l'endroit où il devrait aller. Dans mon cas, je veux juste le ramener à l'instance de classe qui possède l'instance Server (qui l'enveloppe ensuite dans une classe Connection et l'insère dans une liste).

  1. Server a un paramètre dans son constructeur: std::function<void(socket)> OnAccept qui est appelé dans HandleAccept.
  2. Je crée une classe abstraite, IServerHandler ou autre, qui possède une méthode virtuelle OnAccept. Server prend IServerHandler comme paramètre dans son constructeur et l'instance de classe propriétaire de l'instance de serveur étend IServerHandler et construit Server avec *this en paramètre.

Quels sont les avantages et les inconvénients de l'option 1 par rapport à l'option 2? Y a-t-il de meilleures options? J'ai le même problème dans ma classe Connection (OnConnectionClosed). De plus, selon la façon dont je décide de concevoir le système, il peut nécessiter un rappel OnPacketReceived et OnPacketSent.

34
gurka

Je préfère fortement le premier moyen pour plusieurs raisons:

  • La représentation des concepts/fonctionnalités via des interfaces/hiérarchies de classes rend la base de code moins générique, plus flexible et plus difficile à conserver ou à mettre à l’échelle à l’avenir. Ce type de conception impose au type (le type implémentant la fonctionnalité requise) un ensemble d’exigences qui le rend difficile à modifier à l’avenir, et qui risque le plus d’échouer en cas de modification du système (Pensez à ce qui se passe lorsque ce type de dessins).

  • Ce que vous avez appelé la méthode de rappel n’est que l’exemple classique de la frappe au canard. La classe de serveur n'attend qu'un objet appelable qui implémente les fonctionnalités requises, rien de plus, rien de moins . Non "votre type doit être associé à cette hiérarchie" une condition est requise, donc le type qui implémente la gestion est totalement libre .

  • En outre, comme je l’ai dit, le serveur s’attend uniquement à ce que soit une chose appelable : il peut s'agir de tout ce qui a la signature de fonction attendue. Cela donne plus de liberté à l'utilisateur lors de la mise en œuvre d'un gestionnaire. Pourrait être une fonction globale, une fonction membre liée, un foncteur, etc.

Prenons la bibliothèque standard comme exemple:

  • Presque tous les algorithmes de bibliothèque standard sont basés sur des plages d'itérateurs. Il n'y a pas d'interface iterator en C++ . Un itérateur est n'importe quel type qui implémente le comportement d'un itérateur (être déréférencable, comparable, etc.). Les types d'itérateur sont complètement libres, distincts et découplés (non verrouillés à une hiérarchie de classe donnée).

  • Un autre exemple pourrait être les comparateurs: qu'est-ce qu'un comparateur? N'importe quoi avec la signature d'une fonction de comparaison booléenne , quelque chose appelable qui prend deux paramètres et renvoie une valeur booléenne indiquant si les deux valeurs d'entrée sont égales (inférieure à, supérieure à, etc.) du point de vue de un critère de comparaison spécifique. Il n'y a pas d'interface Comparable.

43
Manu343726

Tout se résume à vos intentions.

D'une part, si vous souhaitez attendez qu'une fonctionnalité appartient à un type spécifique, vous devez l'implémenter en fonction de sa hiérarchie, telle qu'une fonction virtuelle ou un pointeur de membre, etc. Ce sens est utile car il contribue à rendre votre code facile à utiliser correctement et difficile à utiliser incorrectement.

D'un autre côté, si vous voulez juste une fonctionnalité abstraite "allez-y et faites-la" sans avoir à vous soucier de son couplage étroit à une classe de base spécifique, il est clair que quelque chose d'autre va mieux fonction libre, ou une fonction std ::, etc.

Tout dépend de ce qui convient le mieux à la conception spécifique de toute partie spécifique de votre logiciel.

1
Brandon

Il suffit de mentionner que, dans de nombreux cas, vous préférez lier un type spécifique.
Par conséquent, dans ce cas, déclarer que votre classe DOIT avoir une IServerHandler vous aide, ainsi que les autres développeurs, à comprendre quelle interface ils doivent implémenter pour fonctionner avec votre classe.
Dans les développements futurs, lorsque vous ajoutez plus de fonctionnalités à IServerHandler, vous obligez vos clients (classes dérivées) à suivre votre développement.
Cela pourrait être le comportement souhaité.

1
Ran Regev

Quelle version de boost utilisez-vous? La meilleure façon, à mon humble avis, consiste à utiliser des coroutines. Le code sera plus facile à suivre. Cela ressemblera à du code synchrone, mais je ne peux plus faire de comparaison car je suis en train d'écrire depuis un appareil mobile.

0
Germán Diago