Whenever I can, I try to restrict communication between objects to a request-and-respond model. There is an implied partial ordering on the objects in my program such that between any two objects A and B, there may be a way for A to directly or indirectly call a method of B or for B to directly or indirectly call a method of A, but it is never possible for A and B to mutually call each others' methods. Sometimes, of course, you want to have backward communication to the caller of a method. There are a couple ways I like to do this, and neither of them are callbacks.
One way is to include more information in the return value of the method call, which means the client code gets to decide what to do with it after the procedure returns control to it.
The other way is to call out to a mutual child object. That is, if A calls a method on B, and B needs to communicate some information to A, B calls a method on C, where A and B can both call C, but C cannot call A or B. Object A would then be responsible for getting the information from C after B returns control to A. Note that this isn't really fundamentally different from the first way I proposed. Object A can still only retrieve the information from a return value; none of object A's methods are invoked by B or C. A variation of this trick is to pass C as a parameter to the method, but the restrictions on the relationship of C to A and B still apply.
Now, the important question is why I insist on doing things this way. There are three main reasons:
- It keeps my objects more loosely coupled. My objects may encapsulate other objects, but they will never depend on the context of the caller, and the context will never depend on the encapsulated objects.
- It keeps my control flow easy to reason about. It's nice to be able to assume that the only code that can change the internal state of
self while a method executing is that one method and no other. This is the same kind of reasoning that might lead one to put mutexes on concurrent objects.
- It protects invariants on the encapsulated data of my objects. Public methods are allowed to depend on invariants, and those invariants may be violated if one method can be called externally while another is already executing.
I'm not against all uses of callbacks. In keeping with my policy on never "calling the caller," if an object A invokes a method on B and passes a callback to it, the callback may not change the internal state of A, and that includes the objects encapsulated by A and the objects in A's context. In other words, the callback may only invoke methods on objects given to it by B. The callback, in effect, is under the same restrictions that B is.
One last loose end to tie up is that I will allow the invocation of any pure function, regardless of this partial ordering I've been talking about. Pure functions are a bit different from methods in that they can't change or depend on mutable state or side effects, so there is no worry about them confusing matters.