Blog

Swift Tip: Enums vs. Protocols

This week, we’ll look at how protocols and enums are extensible in different ways. In a previous post, we discussed the expression problem as it relates to classes and structs, and there are several similarities.

In many cases, we can model data as either an enum or a protocol.

Consider the following:

enum Shape {
    case circle(boundingBox: CGRect)
    case rectangle(rect: CGRect)
    case combined(top: Shape, bottom: Shape)
}

extension Shape {
   func draw(in context: CGContext) {
      // ...
   }
}

We can extend the enum in two orthogonal directions: we can add new methods (or computed properties), or we can add new cases. Adding new methods won’t break existing code. Adding a new case, however, will break any switch statement that doesn’t have a default case.

To model this using protocols, we would create a struct for every case, then put the draw method in the protocol:

protocol Drawable {
    func draw(in context: CGContext)
}

struct Rectangle: Drawable {
    let rect: CGRect
    func draw(in context: CGContext) {
        // ...
    }
}

struct Ellipse: Drawable {
    let boundingBox: CGRect
    func draw(in context: CGContext) {
        // ...
    }
}

struct Combined<A, B> {
    let top: A
    let bottom: B
}

extension Combined: Drawable where A: Drawable, B: Drawable {
    func draw(in context: CGContext) {
        bottom.draw(in: context)
        top.draw(in: context)
    }
}

When we consider extensibility, the protocol approach allows us to add new conformances without breaking existing code. However, when we add a new requirement to the protocol, this does break existing code (except in those cases where we can provide a default implementation).

Looking at Swift’s standard library, we can see these extensibility tradeoffs manifested in the types. Result and Optional are defined as enums; both the standard library and our own code can add extensions without having to worry about breaking things. However, we can’t add new cases to the enum. If the standard library implementers were to do so, everyone’s code would break.

Likewise, Collection and Sequence are defined as protocols. Both the standard library and our own code can easily add new “cases” (that is, conform our own types) without breaking existing code. However, we can’t change the protocol ourselves. The standard library implementers have to be very careful about changes. If they were to add another requirement to Collection, for example, that could break every custom collection conformance outside of the standard library.

In most cases, the decision to use an enum or a protocol should be pretty clear. However, when it’s unclear, and especially when you’re writing a framework, consider how you want the type to be extended. Needless to say, there are often other options too!

To learn more about building extensible libraries, watch our public Swift Talk episode, Enums vs. Classes, recorded with Brandon Kase.

The new edition of our book, Advanced Swift, is almost ready for release. It includes a new chapter on enums, and a revised chapter on protocols, all updated for Swift 5 — you can read an excerpt here.


Stay up-to-date with our newsletter or follow us on Twitter.

Back to the Blog

recent posts