Chris Dzombak

Modeling polymorphic relationships in Swift (spoiler: enums seem pretty cool)

In our application we have the need to model relationships between—for example—an article and various other media which related to it. Those could be photos, videos, slideshows, or even other articles. In Objective-C, this is commonly achieved in one of two ways.

Aside: I have never properly learned UML, so if you try to interpret these diagrams as strict UML you’ll probably be disappointed.

A common base class

Imagining this hierarchy with a common base class.

One typical approach is to make all these objects inherit from a common base class. (This is a particularly seductive solution when your persistence library requires your models to inherit from a base class, like Core Data or Realm.)

Then, this relationship may look something like:

@property (nonatomic, readonly, nonnull) NSArray <Media *> * relatedMedia;

This solution has a few downsides, though:

And what if we want to restrict a relationship to only certain types of media? For example, perhaps an article can open with a single visual item—an image or video, but not an entire slideshow. The only way to express this limitation in this property’s type is by introducing another abstract class:

A common abstract base class and another abstract Visual Item class.

@property (nonatomic, readonly, nullable) VisualItem *headerVisualItem;

Clearly, adding a new abstract class to the model layer for each constraint like this is clumsy at best and impossible at worst.

Protocols

We can solve a few of these problems with Objective-C protocols by using (similar to Java’s interfaces) in our model. Let’s turn Media and VisualItem into protocols:

Using protocols instead of a common abstract base class.

@property (nonatomic, readonly, nonnull) NSArray <id<Media>> * relatedMedia;
@property (nonatomic, readonly, nullable) id<VisualItem> headerVisualItem;

Now we can model shared properties in our Media protocol, we’ve eliminated our abstract classes, and we can trivially add more protocols to express additional constraints if necessary.

But code consuming simply a Media or VisualItem will still need to use the runtime to check what concrete types it’s dealing with—still clumsy.

A Swift solution

In Swift, we’re writing our models as structures (value types, without inheritance) and using protocols to express common properties. We could express these relationships in terms of the Media or VisualItem protocols:

public let relatedMedia: [Media]
public let headerVisualItem: VisualItem?

But this approach still suffers from the problems discussed above. Additionally, in Swift:

Luckily, Swift gives us enumerated types, and unlike enums in C they’re very powerful sum types that allow us to attach associated values.

Consider the following enumeration as an addition to the Swift model layer:

public enum RelatedMedia {
    case Article(ContentModels.Article)
    case Slideshow(ContentModels.Slideshow)
    case Photo(ContentModels.Photo)
    case Video(ContentModels.Video)
}

This new type now contains the knowledge about valid relationships; rather than making our models conform to a new protocol each time we need to impose an additional constraint, we can easily add a new enum.

(The definition of a RelatedVisualItem enum, encapsulating a relationship only to either a photo or video, is left as an exercise to the reader.)

This lets us rewrite our Article’s relationships as follows:

public let relatedMedia: [RelatedMedia]
public let headerVisualItem: RelatedVisualItem?

This approach resolves every problem discussed thus far:

This enum can also be used as a sort of Result return type for a function that may return one of several media types. (As an oversimplified example, imagine a database lookup method, mediaWithID(id: MediaID) -> RelatedMedia.)

Conclusions

In traditional Objective-C code, we’d typically use protocols to express a polymorphic relationship. In Swift, we can take advantage of the language’s powerful enumerations to model these relationships, eliminate runtime type checks, and remove room for error in client code. 💯.

Credits

I didn’t come up with this idea! This comes from my coworkers David Galbraith and Steve Matthews.


As always, I welcome discussion and feedback; I’m @cdzombak on Twitter.