Delegates vs Observers

A well structured app consists of simple objects with well-defined responsibilities talking to each other. When you design an object, you might think of an object's properties, and the actions they take, but it's just as important to decide how they'll communicate.

Cocoa provides a handful of patterns without much guidance on their use. Today, let's contrast delegates with observers.

When I don't know what pattern to use, I start by trying delegation. It's great for one-to-one relationships between objects. It's straightforward to debug, and you get more compile-time checks than other patterns.

Notifications are ideal in one-to-many relationships, with mostly one-directional communication. Consider the iOS keyboard. Imagine if iOS used the app delegate to notify of keyboard events:

func application(application: UIApplication, willShowKeyboardAtFrame frame:CGRect) {
  homeViewController.adjustKeyboardAtFrame(frame)
  profileController.adjustKeyboardAtFrame(frame)
  messagesController.adjustKeyboardAtFrame(frame)
}

With each of those controllers dealing with child controllers, we've got brittle, error prone boilerplate.{{ fn }} If you refactor into a more maintainable "global dispatcher" object, you realize you reinvented notification center.

When abused, notifications are like a Cocoa programmer's goto statement. When you fire a notification, side effects can appear anywhere in your app, and it's hard to predict their order. This creates control flow that's maddening to debug.

Protocols scale better as communication gets more complex. Notifications require a lot of unsafe, mindless runtime wiring.

Use observers for broadcasting, and delegation for conversations.

Imagine your app as a store. Most of the time you should have one-on-one conversations, like when a clerk tells a customer where a product is located. Occasionally you need to broadcast to a room full of people, "We will close in a half hour."

If you broadcast when you should be using one-on-one conversations, you play the telephone game; it's inefficient, and sometimes messages are dropped. If someone has conversations through megaphones, you quickly shout over each other, and have to worry about eavesdroppers.

Examples

Let's build a shopping app like Amazon's. It displays products. Each product has a photo, name, and price. A single product instance may show up in multiple places, such as the homepage and your shopping cart.

class Product {
  var photo:UIImage?
  private var photoURL:URL
  var price:Float
  var name:String
}

We decide to lazy load photos. The first time you access the photo property, it kicks off a download and returns nil. A few moments later the photo is available. Let's break down the responsibilities and wire them up.

First, where should we put the network code? We could throw it in our entity; maybe create some NetworkEntity, and subclass it for all objects in our app?

We should prefer composition over inheritance. Making some other object responsible for downloading resources reduces Product's responsibilities, and makes things easier to test.

Since we're looking at dozens of independent instances through our app, it would be great to funnel all requests through a single instance of this resource downloader thing, so we can throttle or cancel requests. How do we connect it? Hint: it isn't a singleton.

We'll use a delegate. Well, technically a data source here, but it's the same deal.

typealias ImageResponse = (UIImage?, NSError?) -> ()

protocol ProductDataSource:class {
  func product(product:Product, requestedPhoto url:NSURL, callback:ImageResponse)
}

class Product {
  weak var dataSource:ProductDataSource?
  var photo:UIImage?
  private var photoURL:URL
  var price:Float
  var name:String
}

Notice we don't say "network" anywhere in the delegate. It could be fetching from HTTP or just loading it from an on-disk cache.

As an exercise, how would this API look with notifications? You could build something around a PhotoCacheMissNotification, but that sure feels wrong.

Notifications expose state to the whole app. Does everyone need to know when a product requests a photo? It's easy for logic to sprawl, and publishing this information to the world endorses it as an API. It's easy for hidden side effects to crop up.

Consider a tableview cell that represents the Product. It has an "Add to Cart" button that tells our view controller to add the product to the shopping cart.

class ProductCell:UITableViewCell {
  var addToCartButton:UIButton!
  var product:Product!
}

It's hard to use target/action, since you need to know which product should be added. There's a common antipattern to connect views to controllers via notifications, like a AddToCardNotification that includes the Product in userInfo.

What happens if you have two instances of the same view controller class in two different tabs? One tab might have "Best Sellers," and another "Recommended For You." Both view controllers could display the same product, which would add the product to the shopping cart twice.

Filtering notifications gets tricky. The "object" parameter used for filtering is useless with Swift value-types, and filtering through conditionals can lead to a big monolithic router.

Instead, I'd wire the button to the tableview cell, which just calls:

protocol ProductCellDelegate:class {
  func productCell(cell:ProductCell, didTapAddToCartForProduct product:Product)
}

Now consider a situation where observers shine: the one-to-many relationship of a product to views. When the product image updates, we want both its product page and shopping cart view to load the latest image. So when our product image loads, we fire:

let ProductDidUpdateNotification:String

If we want to reduce what each view has to keep track of to follow changes, we could provide context about the update through the userInfo dictionary.

Summary

For simplicity of my examples, I used delegation, but you could construct similar relationships with callbacks. I used NSNotificationCenter instead of KVO, and I'll explain why in a future post.

If there's one takeaway today, prefer isolated conversation over broadcasting.

Subscribe to Sandofsky

Don’t miss out on the latest issues. Sign up now to get access to the library of members-only issues.
jamie@example.com
Subscribe