When Not to Use an Enum
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 enum
s 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:
- To create an event
- To get the name for the event
- To get any metadata for the event
Notably missing from this list is any switch
ing. This violates my design rule for enums: Enums are for switching.1
Enums are for switching.
— Matt Diephouse (@mdiep) April 6, 2017
If you aren’t going to switch over a value—outside of the type—then use a struct.
If you’re not switch
ing over an enum
, then you’re using it for its properties. We have a better tool for value types with properties: struct
s.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 case
s 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.