Misusing enums

This is a response to Matt Diephouse’s article, which is itself a response to John Sundell’s article. You should go read these first.

Matt starts off by saying:

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.

Therefore, I’ll start similarly:

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

As the examples in both articles show, analytic events can have different payloads of information that they’re going to capture. Matt uses a metadata property (a Dictionary<String, String>) to capture this, but this approach imposes a significant burden at the callsite. In order to properly fill out an AnalyticEvent struct, the callsite must know how to destructure the current model/event information and package it up inside the metadata property.

This could be fine if you’re dealing with a very small app, but given that we’re at the point where we’re putting in analytics, the chances that this app is small is pretty remote.

Additionally, it’s often not the case that analytics are defined at the application level. They’re typically brought in as part of an underlying framework. This means that the top-most levels of abstraction in your app (the app itself) have to know intimate, implementation details about how levels far below (the raw analytic communication protocol) work and expect their data to be formatted. Now, it’s possible that the metadata is a very loose bucket of “anything goes here”, but that still requires a lot of extra code at the callsite.

And when you’re putting in analytics, you want them to be as least invasive as possible, because they’re not the point of your app.

So, in my opinion, the better approach would be to define your analytics by a protocol, like this:

protocol AnalyticEvent {
    var name: String { get }
    var payload: Dictionary<String, String> { get }
}

The analytics capture mechanism would then only accept values of type AnalyticEvent (the protocol), and not define concrete types itself.

Here are some of the benefits we get by taking this approach:

Semantically appropriate event definition

Each layer of your app can define its own types (whether an enum, class, or struct) that conform to the AnalyticEvent protocol. Perhaps your Networking layer defines events that capture how often it has to hit the network versus how often it has to hit the cache. It can define those in its own custom NetworkingEvent: AnalyticEvent type.

Minimized callsites

By having each layer define its own type, it can create minimized initializers for its custom AnalyticEvent that would be impractical to do with an enum, and cumbersome with a struct. For example, if I had a UserSessionEvent: AnalyticEvent, I could create an initializer that takes a LoginFailureReason as the parameter, and then the initializer turns it in to the privately-known name and payload.

Type checking events

I could create a whole bunch of extensions on an AnalyticsEvent struct to do the custom initializer for the struct that is contextually appropriate for the callsite, but that explodes the numbers of initializers I have to sort through when creating an AnalyticsEvent. The autocomplete would show me every possible initializer for every possible event type, which is a total pain.

On the other hand, by requiring a custom type that adopts the AnalyticsEvent protocol, I can narrow my focus down to only the autocompletion results that are possible for a UserSessionEvent or a NetworkingEvent or a AwesomeCarouselWidgetInteractionEvent, etc.

With this sort of type-checking in place, refactoring these events also becomes easy. I can search the codebase for just NetworkingEvent to see every place where that type of event is getting generated.

Easy extensibility

Since AnalyticEvent is a protocol, adding a new kind of event is trivial. I don’t have to add a new case to an enum. I don’t have to litter stringly-typed event names throughout my code. I don’t have to add another extension to a struct. The protocol makes it easy to isolate concerns to their respective layers.

So, 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 protocol instead.


EDITED TO ADD

It should go without saying that I really appreciate both Matt and John discussing these patterns in a public forum. In no way should my comments be construed as any sort of personal attack or judgement of Matt or John.


Related️️ Posts️

My First App
Core Data and SwiftUI
Custom Property Wrappers for SwiftUI
The Laws of Core Data
A Better MVC, Part 5: An Evolution
Sometimes I hate being a programmer
Swift Protocols Wishlist
Simplifying Swift framework development
Reading your own entitlements
Level up your debugging skills
A Better MVC, Part 4: Future Directions
A Better MVC, Part 3: Fixing Massive View Controller
A Better MVC, Part 2: Fixing Encapsulation
A Better MVC, Part 1: The Problems
Edit distance and edit steps