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
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:
- We have an abstract superclass which isn’t useful on its own. This isn’t as much of a problem in some other languages, but Objective-C has no formal notion of “abstract” classes, so enforcement of restrictions on using this abstract base class is ad-hoc and at runtime.
- How do we use these related media? Photos, videos, and articles are obviously surfaced differently in the user interface. Code that consumes this array then needs to check each object via
-isKindOfClass:
or something similar at runtime, which is unfortunate.
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:
@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:
@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:
- Explicitly checking a value’s type at runtime feels very wrong.
- Given that, consuming a heterogeneous array of media feels clumsy at best.
- If the
Media
protocol has an associated type, it can’t be used like this; we can only use it as a generic constraint.
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:
- There’s still no abstract base class.
- Adding new relationships with different constraints requires only defining a new enumeration, instead of introducing a new protocol and making the appropriate model structs conform to it. Much simpler.
- Code consuming these relationships will use Swift’s pattern matching to unpack and display the related model in a type-safe manner, with no casting whatsoever. And the compiler will ensure the resulting
switch
statement is exhaustive, meaning a client cannot forget to handle every possible relationship.
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.