Skip to content

Instantly share code, notes, and snippets.

@bgulanowski
Last active August 29, 2015 14:02
Show Gist options
  • Save bgulanowski/ef266a44e6af108920b4 to your computer and use it in GitHub Desktop.
Save bgulanowski/ef266a44e6af108920b4 to your computer and use it in GitHub Desktop.
Notes about object collaboration patterns in Cocoa

Delegate Protocols

Delegates are now the de-facto way for two objects to communicate. Delegation is good, because it ostensibly reduces coupling. An object which has a delegate can work with any object that implements the protocol. But this has led to a situation where delegate protocols are being over-relied upon. It has been used in places where coupling is unavoidable, or whether other, simpler mechanisms are available. Some protocols are also being defined before the functional requirements are even understood, leading to overly verbose protocols with methods that aren't used.

The primary case for delegation is when an object of one class needs support from an additional object to do its work successfully. The word "delegate" was originally meant specifically for this case. The first object, the delegator, uses the delegate to make decisions which the delegator can't make itself.

Notifications

If you create a delegate protocol where none of the methods actually involves delegating responsibility (in other words, the delegate methods are all declared to return void), then a delegate protocol may be overkill. Consider whether NSNotifications would suffice.

Granted, lots of delegate protocols (especially those that are part of Cocoa and Cocoa Touch) include notification-style messages. What is often also the case, however, is that those messages are special cases of pre-existing NSNotifications which are also sent through the notification centre.

A completely different issue is the use of delegates to provide the illusion of decoupling between two classes which are, under close examination, in fact very tightly coupled. This is often the case between a close pair of views, view controllers, or a view controller and its view. If you have a situation where two classes exist, but neither could exist without the other, you are better not to use a delegate protocol. Be honest about the inter-dependence. Cross-import the class interfaces in the implementations, and use the interface provided.

Nevertheless, it's still good to decouple notification-style messages from imperative messages. What do I mean by this?

A notification message includes information about something that has happened. An imperative message specifies an operation that should happen.

Frequently, the two are so closely linked that they are merged. But if possible, it is better to keep them separate. While two classes might be close collaborators, each will necessarily have different responsibilities. In a view/controller relationship, the controller is usually the one that makes decisions on when things should happen, while the view is more likely to simply describe when things have happened.

The decoupled approach is to have one method for the imperative operation, and a separate method which responds to a notification message by invoking the operation. Sometimes this seems excessively ornate, but it makes code that is more self-documenting, easier to test (you can invoke the operations directly instead of relying on sending notification messages), and is easier to refactor.

In a close coupled design, whether between controller and its view, or between a primary and secondary controller, one object will usually play the decision maker, while the other will play support. Controllers generally send imperative (command-style) messages to views or subordinate controllers, while views and subordinate controllers usually send notification-style messages back to their supervisors or autonomous collaborators. (Controllers jobs mostly involving ordering other objects around, even themselves.)

KVO

Finally, we come to KVO. Key-value observation was introduced early on in OS X, along with Cocoa Bindings (an OS X-only technology) partly as a substitute for excessive use of the notification centre, which can otherwise get very bogged down.*

If you have a controller that needs to know when a simple value changes in a subordinate object, don't be afraid to try KVO. Granted it's a tricky technology and you must be careful, but that comes with practise.

On the other hand, don't use KVO to respond to changes to properties of self. That's overkill. Although using side effects in setters is not always ideal, KVO is itself just a side effect, so stick with explicit side effects if you need them. Otherwise, use explicit methods which wrap a setter and describe the side effect they will use. For example, methods which optionally animate a property. (On this issue, I've seen some designs where the normal setter relies on the composite method, instead of the other way around. That's probably asking for trouble.)

Conclusion

Generally, you want to use a messaging strategy which is most clear in its intention. Consider the flow of information and the division of responsibility. In any collaborative relationship between two or more objects, remember to be very clear in your mind (and in your design) which object is playing the supervisor, and which is (are) subordinate. As in real life, be careful not to have any one object having too much or too little responsibility. A supervisor should not have too many direct subordinates, or too many supervisors. One object should be the clear decision maker for any choice. Other objects should act as support, or have a mechanism to learn about changes, without interfering with one another.

--

* In OS X, especially in the days of multi-window design, keeping data consistent across multiple views and windows—which were, unlike in iOS, often active and visible at the same time—required a lot of work. Much of the time a delegate wasn't enough—synchronization required broadcast messages to multiple interested objects. So lots of apps relied heavily on notifications, often excessively. Early versions of Xcode were infamous for slowing to a crawl for no apparent reason, but it was often because of notification load (I profiled it).

@sebmartin
Copy link

Another method that is not covered here and is actually not that popular in Cocoa (probably due to the recent-ish support for it) is blocks. Closures are powerful ways to collaborate between objects. They retain context and are not tied to a one to many relationship (i.e. an object can only have one assigned delegate). When compared to KVO and notifications, closures have less sticky state as they do not require subscribing/unsubscribing. The syntax usually leaves us cursing but this looks a lot better in Swift.

I think that when the relationship is of the type “I’m done, here is the result (or error)” then delegates are not really a good fit. They were sometimes used this way in Cocoa while ObjC lacked support for closures.

Anyway, I’d be interested to hear your take on it and see how blocks could be added to this gist and compared with the other methods.

@bgulanowski
Copy link
Author

Yes, you are completely right. Blocks was a huge oversight.

Blocks are the most flexible technique, but also the easiest to abuse. Also, they often lead to another level of asynchronous execution, which means a trade-off in locality between code and runtime.

The real problem with blocks is that it's not always easy to tell where to stop when using them. You could conceivable convert everything to blocks and dispatch all work to queues. And I often see blocks being dispatched async unnecessarily, because, hey, why not! And then debugging is a nightmare because the causality between events gets lost.

I have occasionally used blocks to replace delegate protocols completely. When they fit the bill, they are terrific. But I haven't always found them to be better, in that context. Blocks are best for fire-and-forget work, especially concurrent work. Work that requires sequential operation and has a lot of inter-class dependency is just as difficult to work with in blocks as in messaging. The trick, of course, is to work out how to avoid the sequential dependencies.

A key factor in deciding to use blocks is whether you want the block to capture local state. If you don't want it to, because you're referencing instance variables and thus capturing self, then you aren't getting much benefit over normal message passing. Most of the time, for clarity, my completion blocks consist solely of sending a message to self which then performs the work. Then at least I don't have to convert to a weak reference and back to a strong reference again, or risk implicitly capturing self by accident.

Blocks don't offer a lot beyond classic delegation, but I love completion blocks after indeterminate asynchronous operations, especially when supporting multiple completions.*

* One of my biggest gripes with AFNetworking is its crappy completion support. Firstly, separate success and failure blocks is just annoying. But it also doesn't support multiple completion blocks. For the Score mobile, we used a custom in-house HTTP REST library with support for multiple completion blocks, so subclasses could simply append new completion blocks, instead of having to wrap them in layers.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment