Why I don’t love Swift Enums any more

Robert j Chatfield
4 min readOct 29, 2016

--

At the last two Sydney Cocoaheads, any time I see a use of Enums in a talk, I make sure to ask “Why an enum and not a protocol?”. This blog post will hopefully explain the smell of enums.

Firstly, I have to say that enums are great if you never need to extend the number of cases. However, incorrect usages of enums break the O in SOLID Principles.

Open Close Principle

A Type should be open to extension, but closed for edits.

And I believe the Expression Problem best explains the violation.

The Expression Problem

Add new methods; add new cases. Pick one.

I will attempt to walk you through the Expression Problem with enums, then proceed to “solve” it with the Visitor Pattern.

A Good Enum Type

I believe Optional is a great use of enums because there is a fixed number of cases that is unlikely going to be extended. Here is my implementation:

enum Opt<T> {
case some(T)
case none
}

Let’s add an expression map:

extension Opt {
func map<U>(_ f: (T) -> U) -> Opt<U> {
switch self {
case .some(let value): return .some(f(value))
case .none: return .none
}
}
}

You can see that Opt is open for extension, while map is closed for edits. If we wanted to add a second method to Opt, we can do so without touching the implementation of map.

Very clean. 🙂

A Gross Enum Type

Now let’s consider a gross Enum type. (Thanks to @karlbowden for the inspiration.)

enum Route {
case screen1
case screen2tabA
case screen2tabB
case screen3
}

Let’s add an expression…

extension Route {
func goTo(with viewController: UIViewController) {
switch self {
case .screen1:
...
case .screen2:
...
case .screen2taba:
...
case .screen2tabb:
...
case .screen3(params: [String: String]):
...
}
}
}

Let’s ignore the code smell of how complicated each one of those cases would be.

Much like Opt, if we wanted to add a second method to Route, we can do so without touching the implementation of goTo. However, what happens when you want to add screen4 or change to screen3tabA? We would need to go back and edit each of the working (bug-free) methods.

While it’s great that Swift is exhaustive, that doesn’t mean that you can’t introduce bugs while editing working code.

How can we do this differently?

Introducing the age-old Visitor Pattern.

This pattern seems very “Java-ish”, which I’m typically against. So if it makes you feel better, just call it “Protocol Oriented Programming”.

Let’s start with a list of all the expressions we want our Types to have:

protocol Route {
func goTo(with viewController: UIViewController)
}

Let’s create a Type for each case:

struct Screen1 {}
extension Screen1: Route {
func goTo(with viewController: UIViewController) {
...
}
}
struct Screen3 {
let params: [String: String]
}
extension Screen3: Route {
func goTo(with viewController: UIViewController) {
...
}
}

I love the separation of concerns here, let each Type own the implementation of each expression.

API feels the same:

// Enum
Route.screen3(params: ["id": "123"]).goTo(with: vc)
// Struct
Screen3(params: ["id": "123"]).goTo(with: vc)

So what’s the big deal?

Benefit 1: Expression Problem == .solved

Now look at how easy it is to add a case:

struct Screen4: Route {
func goTo(with viewController: UIViewController) {
...
}
}

Now let’s add a second expression…

protocol Route {
...
init?(path: String)
}
extension Screen1: Route {
...
init?(path: String) { ... }
}
extension Screen3: Route {
...
init?(path: String) { ... }
}

I can hear you say…

“But Rob, we have to edit all the Types one by one. Isn’t that just as bad?!”

I want you to notice where the edits are happening. Are we touching the behaviour of any existing methods? No. 🙂 Therefore, we’re avoiding the introduction of bugs to working code.

Benefit 2: Multiple Conformance

Here’s the kicker, what if I want to conform to something else?

Say you have this Type:

struct Screen2 {
struct TabA {}
struct TabB {}
}
extension Screen2: Route { ... }
extension Screen2.TabA: Route { ... }
extension Screen2.TabB: Route { ... }

… and wanted to add this conformance:

protocol TabScreen { ... }

extension Screen2: TabScreen { ... }

or

extension Screen2.TabB: UITableViewDataSource { ... } 
// don't actually do that

Now show me an enum case that does THAT!

Benefit 3: Play as a Team

One less obvious benefit is that it does wonders for your source control. If you have multiple people adding cases to an enum, you are just asking for merge conflicts. All of your enum cases and methods are all in the same files. With the Visitor Pattern you can keep them apart, reducing conflicts.

Summary

  • Enums are clean and expressive… so long as you can live the pain of adding a case.
  • Protocols are far more powerful.

So next time you see an enum, think about the your cases and expressions, and ask yourself if you want to add new cases in the future.

I got a lot of inspiration from Jed Wesley Smith, and from this blog which I wrote in Swift here.

If you liked this, read my other rants and follow me on Twitter.

--

--