When developing APIs, we strive to eliminate interdependencies between software components. Sometimes doing so completely is unachievable so one pattern we use is the delegation pattern to limit how much components can know about each other. This pattern consists of the
delegator which is typically a reusable library component, and a
delegate which is typically a custom controller1.
Very often, delegators pass themselves as a reference when sending a message to their delegate. Adopting this practice, however, should be avoided or at least be carefully considered.
Let’s take a look at an example used in Cocoa:
Inside our custom controller, we first set its table view’s data source to our
self. When an object value needs to be displayed in the table, a method in
NSTableView is invoked, which then queries our controller for the object to display at a particular column and row.
Note that the controller owns a reference to the table view, but the table view passes itself to the controller when making its query. This turns out to be redundant; the controller already has a reference to the table view, so the delegator doesn’t need to be passed here.
But.. A Reason Exists!
One reason the table view may be passed is because our controller could be responsible for multiple table views, and would need the sender to distinguish them. This turns out to be poor though because we would be usually better off creating distinct controllers for managing different table views instead.
Another reason may be that our code may not easily have a reference to the table view when its delegate method is invoked. However in most cases, we may end up having a reference or be able to obtain one easily. In fact, delegator & sender type arguments are often found to be unused or inconvenient because they are poorly typed (e.g.
id). Worse yet, we write conditional checks or assertions like the code above for confirming the table view’s identity even knowing we just have a single one.
Regarding types, what if we subclassed
NSTableView and used our own subclass instead. We would then want to pass
ZGAwesomeTableView but we may not be able to because of conflicting type parameters. Even if we could do this with no complaints, the method signature now strays from the protocol’s intent. Some event based systems may try to pass a generic type, but as discussed above using these is often inconvenient and ignored.
Briefly speaking, cohesion refers to how much the parts that make up a module are related to each other. Passing the delegator as is in the case of
NSTableView encourages a low (bad) amount of cohesion because our controller could be responsible for different types of tables and logic. This is what we have touched up on.
Coupling though refers to how much components in a system need to depend on each other; loose coupling is better. By implementing the delegate pattern, we already decrease coupling by using a protocol or constrained interface. However, by passing the delegator, this can lead to increasing coupling.
For example, we start to realize
NSTableView.m is becoming too big. We may want to move the drag & drop implementation into a private subcomponent which
NSTableView owns. This subcomponent may need to send messages to the table view’s data source, so we pass the data source to the component. However, in order for this subcompont to communicate to the data source it must also pass the table view reference. Herein lies our problem: we now have the table view and subcomponent depend on each other even when the subcomponent may not need to otherwise. This can be mitigated to some degree, but we should not need to work around this obstacle.
When designing an API that makes use of delegates, the delegator is often passed “just in case” the other side needs it. We implement the delegate pattern by creating a protocol or typed interface because we do not have to expose any more design than necessary; this reasoning should apply to what parameters we need to provide for procedures as well. We do not strive to create more flexible interfaces that become underutilized just to incur a design cost later.
When implementing the delegation pattern, make a concious decision before passing the delegator, and hopefully that decision is to not pass it.