Por supuesto que depende de la situación. Pero cuando un objeto o sistema de palanca inferior se comunica con un sistema de nivel superior, ¿deberían preferirse las devoluciones de llamada o los eventos a mantener un puntero a un objeto de nivel superior?
Por ejemplo, tenemos una world
clase que tiene una variable miembro vector<monster> monsters
. Cuando la monster
clase se comunica con el world
, ¿debería preferir usar una función de devolución de llamada o debería tener un puntero a la world
clase dentro de la monster
clase?
architecture
software-engineering
hidayat
fuente
fuente
Respuestas:
Hay tres formas principales en que una clase puede hablar con otra sin estar estrechamente unida a ella:
Los tres están estrechamente relacionados entre sí. Un sistema de eventos en muchos sentidos es solo una lista de devoluciones de llamada. Una devolución de llamada es más o menos una interfaz con un único método.
En C ++, rara vez uso devoluciones de llamada:
C ++ no tiene un buen soporte para las devoluciones de llamada que retienen su
this
puntero, por lo que es difícil usar devoluciones de llamada en código orientado a objetos.Una devolución de llamada es básicamente una interfaz de un método no extensible. Con el tiempo, encuentro que casi siempre termino necesitando más de un método para definir esa interfaz y una sola devolución de llamada rara vez es suficiente.
En este caso, probablemente haría una interfaz. En su pregunta, en realidad no explica a qué
monster
necesita comunicarse realmenteworld
. Adivinando, haría algo como:La idea aquí es que solo pones
IWorld
(que es un nombre horrible) el mínimoMonster
necesario para acceder. Su visión del mundo debería ser lo más estrecha posible.fuente
No use una función de devolución de llamada para ocultar qué función está llamando. Si tuviera que poner una función de devolución de llamada, y esa función de devolución de llamada tendrá una y solo una función asignada, entonces realmente no está rompiendo el acoplamiento. Solo lo estás enmascarando con otra capa de abstracción. No está ganando nada (aparte del tiempo de compilación), pero está perdiendo claridad.
No lo llamaría exactamente una mejor práctica, pero es un patrón común tener entidades contenidas por algo para tener un puntero a su padre.
Dicho esto, podría valer la pena usar el patrón de interfaz para dar a tus monstruos un subconjunto limitado de funcionalidades que pueden invocar en el mundo.
fuente
En general, trato de evitar los enlaces bidireccionales, pero si debo tenerlos, me aseguro absolutamente de que haya un método para crearlos y otro para romperlos, de modo que nunca tenga inconsistencias.
A menudo, puede evitar el enlace bidireccional por completo al pasar los datos según sea necesario. Una refactorización trivial es hacer que, en lugar de que el monstruo mantenga un vínculo con el mundo, pase el mundo por referencia a los métodos de monstruos que lo necesitan. Mejor aún, es pasar solo una interfaz para las partes del mundo que el monstruo necesita estrictamente, lo que significa que el monstruo no depende de la implementación concreta del mundo. Esto corresponde al Principio de segregación de interfaz y al Principio de inversión de dependencia , pero no comienza a introducir el exceso de abstracción que a veces se puede obtener con eventos, señales + ranuras, etc.
En cierto modo, puede argumentar que usar una devolución de llamada es una mini interfaz muy especializada, y eso está bien. Debe decidir si puede lograr sus objetivos de manera más significativa a través de una colección de métodos en un objeto de interfaz o varios varios en diferentes devoluciones de llamada.
fuente
Trato de evitar que los objetos contenidos llamen a su contenedor porque encuentro que conduce a la confusión, se vuelve demasiado fácil de justificar, se usará en exceso y creará dependencias que no se pueden administrar.
En mi opinión, la solución ideal es que las clases de nivel superior sean lo suficientemente inteligentes como para gestionar las clases de nivel inferior. Por ejemplo, el mundo que sabe determinar si ocurrió una colisión entre un monstruo y un caballero sin que ninguno de los dos sepa del otro es mejor para mí que el monstruo que le pregunta al mundo si chocó con un caballero.
Otra opción en su caso, probablemente descubriría por qué la clase de monstruos necesita saber sobre la clase mundial y lo más probable es que descubra que hay algo en la clase mundial que se puede dividir en una clase propia que hace sentido para la clase de monstruos saber.
fuente
Obviamente, no llegarás muy lejos sin eventos, pero antes de comenzar a escribir (y diseñar) un sistema de eventos, debes hacerte la pregunta real: ¿por qué el monstruo se comunicaría con la clase mundial? ¿Debería realmente?
Tomemos una situación "clásica", un monstruo atacando a un jugador.
Monster está atacando: el mundo puede identificar muy bien la situación en la que un héroe está al lado de un monstruo, y decirle al monstruo que ataque. Entonces la función en monstruo sería:
Pero el mundo (que ya conoce al monstruo) no necesita ser conocido por el monstruo. En realidad, el monstruo puede ignorar la existencia misma del mundo de clase, que probablemente sea mejor.
Lo mismo cuando el monstruo se está moviendo (dejo que los subsistemas obtengan la criatura y manejen el cálculo / intención del movimiento para ello, el monstruo es solo una bolsa de datos, pero mucha gente diría que esto no es POO real).
Mi punto es: los eventos (o la devolución de llamada) son geniales, por supuesto, pero no son la única respuesta a cada problema que enfrentarás.
fuente
Siempre que puedo, trato de restringir la comunicación entre objetos a un modelo de solicitud y respuesta. Hay un orden parcial implícito en los objetos en mi programa de tal manera que entre dos objetos A y B, puede haber una forma de que A llame directa o indirectamente a un método de B o que B llame directa o indirectamente a un método de A , pero nunca es posible que A y B se llamen mutuamente los métodos de los demás. A veces, por supuesto, desea tener una comunicación hacia atrás con la persona que llama de un método. Hay un par de formas en que me gusta hacer esto, y ninguna de ellas son devoluciones de llamada.
Una forma es incluir más información en el valor de retorno de la llamada al método, lo que significa que el código del cliente decide qué hacer con él después de que el procedimiento le devuelve el control.
La otra forma es llamar a un objeto hijo mutuo. Es decir, si A llama a un método en B y B necesita comunicar cierta información a A, B llama a un método en C, donde A y B pueden llamar a C, pero C no puede llamar a A o B. El objeto A sería entonces responsable de obtener la información de C después de que B devuelva el control a A. Tenga en cuenta que esto no es fundamentalmente diferente de la primera forma que propuse. El objeto A solo puede recuperar la información de un valor de retorno; B o C. no invocan ninguno de los métodos del objeto A. Una variación de este truco es pasar C como parámetro del método, pero las restricciones en la relación de C con A y B aún se aplican.
Ahora, la pregunta importante es por qué insisto en hacer las cosas de esta manera. Hay tres razones principales:
self
un método en ejecución es ese método y no otro. Este es el mismo tipo de razonamiento que podría conducir a colocar mutexes en objetos concurrentes.No estoy en contra de todos los usos de las devoluciones de llamada. De acuerdo con mi política de nunca "llamar al llamante", si un objeto A invoca un método en B y le pasa una devolución de llamada, la devolución de llamada puede no cambiar el estado interno de A, y eso incluye los objetos encapsulados por A y el objetos en el contexto de A. En otras palabras, la devolución de llamada solo puede invocar métodos en los objetos que le da B. La devolución de llamada, en efecto, está bajo las mismas restricciones que B.
Un último extremo suelto para atar es que permitiré la invocación de cualquier función pura, independientemente de este orden parcial del que he estado hablando. Las funciones puras son un poco diferentes de los métodos, ya que no pueden cambiar o depender de un estado mutable o efectos secundarios, por lo que no hay que preocuparse por cuestiones confusas.
fuente
¿Personalmente? Solo uso un singleton.
Sí, está bien, mal diseño, no está orientado a objetos, etc. ¿Sabes qué? No me importa . Estoy escribiendo un juego, no un escaparate tecnológico. Nadie me va a calificar en el código. El propósito es hacer un juego que sea divertido, y todo lo que se interponga en mi camino dará como resultado un juego que sea menos divertido.
¿Alguna vez tendrás dos mundos funcionando a la vez? ¡Tal vez! Quizás lo hagas. Pero a menos que pueda pensar en esa situación en este momento, probablemente no lo hará.
Entonces, mi solución: hacer un singleton mundial. Llama a funciones en él. Acabar con todo el desastre. Usted podría pasar un parámetro adicional para cada función - y no se equivoque, que es dónde lleva esto. O simplemente podrías escribir código que funcione.
Hacerlo de esta manera requiere un poco de disciplina para limpiar las cosas cuando se vuelve desordenado (eso es "cuándo", no "si"), pero no hay forma de evitar que el código se desordene, ya sea que tenga el problema de los espaguetis o los miles problema de capas de abstracción. Al menos de esta manera no estás escribiendo grandes cantidades de código innecesario.
Y si decides que ya no quieres un singleton, generalmente es bastante simple deshacerte de él. Toma algo de trabajo, toma pasar un billón de parámetros, pero esos son parámetros que necesitaría pasar de todos modos.
fuente