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:
- Mantiene mis objetos más débilmente acoplados. Mis objetos pueden encapsular otros objetos, pero nunca dependerán del contexto de la persona que llama, y el contexto nunca dependerá de los objetos encapsulados.
- Mantiene mi flujo de control fácil de razonar. Es bueno poder asumir que el único código que puede cambiar el estado interno de
selfun 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.
- Protege invariantes en los datos encapsulados de mis objetos. Los métodos públicos pueden depender de invariantes, y esos invariantes pueden violarse si un método puede llamarse externamente mientras otro ya se está ejecutando.
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.