A lot of talk about good coding practices centers around the idea of loose coupling. That is, writing classes that interact based on behavior, not by identity. If class A needs to interact with class B, it should do so on the basis of what class B does, not because of what it is. That way we can easily swap out class B for another one that behaves the same and not worry about breaking class A. This makes our applications easier to maintain and update. However, when it comes to events, we also have to worry about decoupling our classes too much. Completely decoupled events are harder to trace, debug, and maintain. Let’s review the 6 types of event handling patterns available in the Objective-C runtime and the degree of coupling they encourage:
- Template Method Pattern. This pattern is the most common way that events are handled in OOP, and probably one that you hardly ever think about because it is so transparent. Whenever you subclass UIViewController, for example, you override certain methods like viewDidLoad. But how is your viewDidLoad method itself invoked? There is actually code inside the UIViewController superclass that invokes viewDidLoad in response to certain conditions (in this case, when all views have been fully loaded into the view hierarchy). By overriding viewDidLoad, you effectively tap into this event “for free.” Coupling: high. The sender and receiver are intertwined into a single static compiled class.
- Target-Action Pattern. This pattern is used by UIControl and its subclasses. The sender is configured to invoke a specific method on the receiver whenever a certain event occurs, such as TouchUpInside. Coupling: low. The sender can be configured dynamically to invoke any method on any receiver. However, there is no compile-time assurance that the receiver or its method actually exist.
- Delegation Pattern. The receiver sets itself as the sender’s delegate, and the sender invokes specific methods on the receiver when an event occurs. Offers compile-time safety. Coupling: high or low. High if the sender uses a specific class as its delegate. Low if the sender expects a protocol implementation as its delegate.
- Callback Pattern. The receiver passes arbitrary code via an argument into a method on the sender, which executes it when a certain event occurs. This is accomplished in Objective-C using blocks. Coupling: low. The sender can execute any instructions provided to it by the receiver in response to an event. Special care must be taken to avoid memory retain cycles.
- Observer Pattern. The receiver sets itself up to monitor a particular property on the sender. When the property changes, the receiver is notified. This is accomplished using Key-Value Observing (KVO). Coupling: low. A receiver is able to monitor any KVO-compliant property on any sender.
- Publish-Subscribe Pattern. The sender “publishes” its event to a message bus, where one or more receivers can “subscribe” to them. This is accomplished in Objective-C using NSNotificationCenter. Coupling: none. Receiver has no access to the sender, and vice-versa.
Notice that the last option, NSNotificationCenter, offers no coupling at all. This is its strength as well as its weakness. So why is this bad?
- No compile-time checks. The notification keys and data dictionaries used to link publishers and subscribers are susceptible to silent runtime failures. There is also the risk of messages getting intercepted by the wrong subscriber.
- Limited traceability. Using the debugger, there is no line-of-sight access between the subscriber and publisher on the call stack. This makes debugging application flow extremely difficult. The subscriber has no access to the publishing object for further processing. There is an innovative tool called Spark Inspector that allows monitoring of the message bus in real-time, but this just masks the underlying problem.
- It violates the Inversion of Control principle. Publisher and subscribers have to call out to an external dependency (the message bus). This makes it difficult to unit test because NSNotificationCenter must be replaced with a mock implementation.
- Decoupled in time. Unlike the other event types which are synchronous, the delivery of a notification from the message bus is non-deterministic. It is delayed at minimum by one run loop, which can cause problems by creating opportunities for inconsistent states to exist.
- Difficult to understand application flow. It is not immediately clear how publishers and subscribers are connected.
As a general rule of thumb, message buses such as NSNotificationCenter should be used sparingly, and only when other types of event handling are not possible. Many times a message bus can be prevented simply by using an improved application architecture. By switching from decoupled to loosely coupled events, your application becomes easier to trace, debug, and maintain.