28 votes

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

Je suis en train de créer une application serveur en C++11 en utilisant Boost.Asio. J'ai créé une classe, Server qui se charge d'accepter les nouvelles connexions. En gros, c'est 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 problème de l'utilisation de l'eau. TODO c'est à dire déplacer la socket là où elle doit aller. Dans mon cas, je veux juste qu'il revienne à l'instance de la classe qui possède le fichier Server (qui l'englobe ensuite dans une instance 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 la classe possédant l'instance du serveur étend IServerHandler et construit Server con *this comme paramètre.

Quels sont les avantages et les inconvénients de l'option 1 par rapport à l'option 2 ? Existe-t-il de meilleures options ? J'ai le même problème dans mon Connection classe ( OnConnectionClosed ). De plus, en fonction de la façon dont je décide de concevoir le système, il pourrait avoir besoin d'une OnPacketReceived y OnPacketSent le rappel.

36voto

Manu343726 Points 8803

Je préfère nettement la première méthode pour plusieurs raisons :

  • La représentation des concepts/fonctionnalités par le biais d'interfaces/hiérarchies de classes rend la base de code moins générique, moins flexible, et donc plus difficile à maintenir ou à faire évoluer à l'avenir. Ce type de conception impose un ensemble d'exigences au type (le type qui implémentera la fonctionnalité requise), ce qui le rend difficile à modifier à l'avenir et le rend plus susceptible d'échouer lorsque le système change (considérez ce qui se passe lorsque la classe de base est modifiée dans ce type de conception).

  • Ce que vous avez appelé l'approche du rappel est l'exemple classique de la saisie en canard. La classe serveur attend seulement une chose appelable qui implémente la fonctionnalité requise, rien de plus, rien de moins . Non "votre type doit être couplé à cette hiérarchie" est nécessaire, alors le type qui met en œuvre la manipulation est entièrement libre .

  • De plus, comme je l'ai dit, le serveur ne s'attend à ce que une chose appelable : Cela pourrait être n'importe quoi avec la signature de fonction attendue. Cela donne à l'utilisateur plus de liberté lors de l'implémentation d'un gestionnaire. Il peut s'agir d'une fonction globale, d'une fonction membre liée, d'un foncteur, etc.

Prenons l'exemple de la bibliothèque standard :

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

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

0voto

Germán Diago Points 1396

Quelle version de Boost utilisez-vous ? Le meilleur moyen, à mon avis, est d'utiliser des coroutines. Le code sera plus facile à suivre. Il ressemblera à du code synchrone, mais je ne peux pas faire de comparaison puisque j'écris depuis un appareil mobile.

0voto

Ran Regev Points 84

Je tiens à préciser que, dans de nombreux cas, il est préférable de se lier à un type spécifique.
Par conséquent, dans ce cas, déclarer que votre classe DOIT avoir un objet de type IServerHandler vous aide, ainsi que les autres développeurs, à comprendre quelle interface ils doivent mettre en œuvre pour travailler avec votre classe.
Dans les développements futurs, lorsque vous ajouterez des fonctionnalités à IServerHandler vous obligez vos clients (c'est-à-dire les classes dérivées) à suivre votre développement.
C'est peut-être le comportement souhaité.

0voto

Brandon Points 125

Tout dépend de vos intentions.

D'un côté, si vous voulez s'attendre à qu'une fonctionnalité appartient à un type spécifique, alors elle doit être implémentée en fonction de sa hiérarchie, comme une fonction virtuelle, ou un pointeur de membre, etc. La limitation dans ce sens est une bonne chose car elle contribue à rendre votre code facile à utiliser correctement et difficile à utiliser incorrectement.

D'un autre côté, si vous voulez simplement une fonctionnalité abstraite "aller ici et faire ceci" sans avoir à vous soucier du fait qu'elle soit étroitement liée à une classe de base spécifique, alors il est clair que quelque chose d'autre sera plus approprié, comme un pointeur vers une fonction libre, ou une std::function, etc.

Il s'agit de savoir lequel est le plus adapté à la conception spécifique d'une partie quelconque de votre logiciel.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X