Matt Diephouse

When Not to Use an Enum

7 December 2017

Earlier this week, John Sundell wrote a nice article about building an enum-based analytics system in Swift. He included many fine suggestions, but I believe he’s wrong about one point: that enums are the right choice.

John’s design met all of his goals, but left some gnarly computed properties that switch over self:

extension AnalyticsEvent {
    var name: String {
        switch self {
        case .loginScreenViewed, .loginAttempted,
             .loginSucceeded, .messageListViewed:
            return String(describing: self)
        case .loginFailed:
            return "loginFailed"
        case .messageSelected:
            return "messageSelected"
        case .messageDeleted:
            return "messageDeleted"
        }
    }

    var metadata: [String : String] {  }
}

With just 7 event types, these properties are already getting large. Most apps have many more analytics events, so this would quickly become unwieldy in a real world app.

This also exhibits poor locality. If you want to look up the name and the metadata for an event, or change them for some reason, you need to search through two switch statements. It’d be better if we could specify the name and the metadata together.

We can achieve John’s goals in another way that improves on both points.

Examine the interface for AnalyticsEvent:

enum AnalyticsEvent {
    case loginFailed(reason: LoginFailureReason)
    case loginSucceeded
    

    var name: String {  }
    var metadata: [String : String] {  }
}

This interface is used for 3 things:

  1. To create an event
  2. To get the name for the event
  3. To get any metadata for the event

Notably missing from this list is any switching. This violates my design rule for enums: Enums are for switching.1

If you’re not switching over an enum, then you’re using it for its properties. We have a better tool for value types with properties: structs.2 So let’s try that:

struct AnalyticsEvent {
    var name: String
    var metadata: [String : String]

    private init(name: String, metadata: [String: String] = [:]) {
        self.name = name
        self.metadata = metadata
    }

    static func loginFailed(reason: LoginFailureReason) -> AnalyticsEvent {
        return AnalyticsEvent(
            name: "loginFailed"
            metadata: ["reason" : String(describing: reason)]
        )
    }
    static let loginSucceeded = AnalyticsEvent(name: "loginSucceeded")
    
}

The struct version is identical in usage to John’s enum version. Events can be created by name and asked for their name and metadata. By using a private init, arbitrary events can’t be created.

// Enum case with associated values?
// Or struct with a static function?
// You can't tell from this line of code. :)
analytics.log(.messageDeleted(index: index, read: message.read))

But where the enum required two large computed properties, the struct has a simple init and its data is localized to a small func or let. (And as of today, it’s much easier to make the struct version Equatable/Hashable.)

The boilerplate to create the loginFailed function above might seem a little annoying or verbose, but it’s no more verbose than adding cases to two different switch statements.

Next time you reach for an enum, ask yourself whether you’re going to switch over it. If you're not, you're probably better off with a struct instead.


  1. Really I should say pattern matching here to include if case let … = …

  2. Using a sum type as a product type, or vice-versa, is generally not a good idea.