clases abstractas c++ c++11 interface callback boost-asio

c++ - abstractas - Pros y contras de una devolución de llamada(std:: function/std:: bind) frente a una interfaz(clase abstracta)



clases abstractas c# (4)

Estoy creando una aplicación de servidor en C ++ 11 usando Boost.Asio. Creé una clase, Server , que se encarga de aceptar nuevas conexiones. Básicamente es solo:

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

He encontrado dos maneras (estoy seguro de que hay más) para "corregir" el comentario TODO , es decir, mover el socket a donde debería ir. En mi caso, solo quiero volver a la instancia de la clase que posee la instancia del Server (que luego la envuelve en una clase de Connection y la inserta en una lista).

  1. Server tiene un parámetro en su constructor: std::function<void(socket)> OnAccept que se llama en HandleAccept .
  2. Creo una clase abstracta, IServerHandler o lo que sea, que tiene un método virtual OnAccept . Server toma IServerHandler como parámetro en su constructor y la instancia de clase propietaria de la instancia del servidor amplía IServerHandler y construye Server con *this como parámetro.

¿Cuáles son los pros y los contras de la opción 1 frente a la opción 2? ¿Hay mejores opciones? Estoy teniendo el mismo problema en mi clase de Connection ( OnConnectionClosed ). Además, dependiendo de cómo decida diseñar el sistema, podría necesitar una devolución de llamada OnPacketReceived y OnPacketSent .


¿Qué versión de impulso estás usando? La mejor manera en mi humilde opinión es usar corutinas. El código será más fácil de seguir. Se verá como un código sincrónico, pero ahora no puedo ofrecer una comparación ya que estoy escribiendo desde un dispositivo móvil.


Solo mencionar que en muchos casos haces PREFEROR vinculante a un tipo específico.
Por lo tanto, en este caso, declarar que su clase DEBE tener un IServerHandler ayuda a usted y a otros desarrolladores a comprender qué interfaz deben implementar para poder trabajar con su clase.
En el desarrollo futuro, cuando agrega más funcionalidad a IServerHandler , obliga a sus clientes (es decir, clases derivadas) a mantenerse al día con su desarrollo.
Este podría ser el comportamiento deseado.


Todo se reduce a tus intenciones.

Por un lado, si desea esperar que una funcionalidad pertenezca a un tipo específico, debe implementarse en términos de su jerarquía, como una función virtual, un puntero miembro, etc. La limitación en este sentido es buena porque ayuda a hacer que su código sea fácil de usar y difícil de usar de manera incorrecta.

Por otro lado, si solo quiere una funcionalidad abstracta de "acá y haga esto" sin tener que preocuparse por la carga de estar estrechamente unida a una clase base específica, entonces claramente otra cosa será más apropiada como un puntero a un función libre, o una función std ::, etc.

Se trata de cuál es más apropiado para el diseño específico de cualquier parte específica de su software.


Yo prefiero la primera manera por varias razones:

  • La representación de conceptos / funcionalidad a través de interfaces / jerarquías de clases hace que la base del código sea menos genérica, flexible y, luego, más difícil de mantener o escalar en el futuro. Ese tipo de diseño impone un conjunto de requisitos sobre el tipo (el tipo que implementa la funcionalidad requerida) que hace que sea difícil modificarlo en el futuro, y más propenso a fallar cuando el sistema cambia (Considere lo que sucede cuando la clase base se modifica en este tipo de diseños).

  • Lo que llamaste el enfoque de devolución de llamada es solo el clásico ejemplo de la tipificación de patos. La clase de servidor solo espera una cosa invocable que implemente la funcionalidad requerida, nada más y nada menos . No se requiere la condición "su tipo debe estar acoplado a esta jerarquía" , por lo que el tipo que implementa el manejo es completamente gratuito .

  • Además, como dije, el servidor solo espera una cosa invocable : podría ser cualquier cosa con la firma de función esperada. Esto le da al usuario más libertad al implementar un controlador. Podría ser una función global, una función de miembro enlazado, un funtor, etc.

Tome la biblioteca estándar como un ejemplo:

  • Casi todos los algoritmos de la biblioteca estándar se basan en rangos de iteradores. No hay interfaz de iterator en C ++ . Un iterador es cualquier tipo que implemente el comportamiento de un iterador (ser referenciable, comparable, etc.). Los tipos de iterador son completamente libres, distintos y desacoplados (no bloqueados para una jerarquía de clases determinada).

  • Otro ejemplo podría ser comparadores: ¿qué es un comparador? Es cualquier cosa con la firma de una función de comparación booleana , algo invocable que toma dos parámetros y devuelve un valor booleano que dice si los dos valores de entrada son iguales (menor que, mayor que, etc.) desde el punto de vista de un criterio de comparación específico . No hay una interfaz Comparable .