swift in depth

By Tjeerd in ‘t Veen

In this article, we’re going to explore these limitations of modeling our data via subclassing in a real-world scenario and how to address those limitations with the help of enums.

Save 37% off Swift in Depth with code fccveen at manning.com.

Subclassing allows you to build a hierarchy of your data. For example, we could have a fast food restaurant selling burgers, fries, the usual. For that, we’d create a superclass of FastFood, with subclasses like Burger, Fries, and Soda.

One of the limitations of modeling your software with hierarchies—e.g., subclassing—is that you’re constrained into a specific direction which won’t always match your needs.

For example, the aforementioned restaurant has been getting complaints of customers wanting to serve authentic Japanese sushi with their fries. The restaurant intends to accommodate them, but their subclassing model doesn’t fit this new requirement.

In an ideal world, it makes sense to model your data hierarchically, but in practice you’ll sometimes hit edge cases and exceptions which may not fit your model.

Forming a model for a workout app

Tip All code from this article can be found online. It’s more educational and fun if you follow along. You can download the source code at https://github.com/tjeerdintveen/manning-swift-in-depth/tree/master/ch02-enums/

Next up we’re building a model layer for a workout app, which tracks runs and cycles for someone. A workout includes the starttime, endtime and a distance.

We’ll create a Run -and a Cyclestruct that represents the data we’re modeling.

Listing 1.The Run struct.

 
 import Foundation // We need foundation for the Date type.
  
 struct Run {
     let id: String
     let startTime: Date
     let endTime: Date
     let distance: Float
     let onRunningTrack: Bool
 }
 

Listing 2.The Cycle struct.

 
 struct Cycle {
  
 enum CycleType {
         case regular
         case mountainBike
         case racetrack
     }
  
     let id: String
     let startTime: Date
     let endTime: Date
     let distance: Float
     let incline: Int
     let type: CycleType
 }
 

These structs are a good starting point for our data layer.

Admittedly, it can be cumbersome having to create separate logic in our application for both the Run and Cycle types. Let’s start solving this via subclassing. Then we’ll quickly learn which problems subclassing brings, after which you’ll see how enums can solve some of these problems.

Creating a superclass

A lot of similarities exist between Run and Cycle which, at first look, make a good candidate for a superclass. The benefit with a superclass is that we can pass the superclass around in our methods and arrays etc. This saves us from creating specific methods and arrays for each workout type.

We could create a superclass called Workout, then turn Run and Cycle into a class and make them subclass Workout.


Figure 1.A subclassing hierarchy.


Hierarchically, the former structure makes a lot of sense because workouts share many values. We have a superclass called Workout with two subclasses, Run and Cycle, which inherit from Workout.

Our new Workout superclass contains the properties that both Run and Cycle share. Specifically id, startTime, endTime and distance.

The downsides of subclassing

We’ll quickly touch upon issues when it comes down to subclassing.

First of all, we’re forced to use classes. Classes can be favorable, but having the choice between classes, structs or other enums disappears when subclassing.

Being forced to use classes isn’t the biggest problem. Let’s showcase another limitation by adding a new type of workout, called Pushups, which stores multiple repetitions and a single date. Its superclass Workout requires a startTime, endTime and distance value, which Pushups doesn’t.

SubclassingWorkout doesn’t work because some properties on Workout don’t apply to Pushups.

To allow Pushups to subclass Workout, we’d have to refactor the superclass and all its subclasses. We’d do this by moving startTime, endTime, and distance from Workout to the Cycle and Run classes because these properties aren’t part of a Pushups class.


Figure 2.A refactored subclassing hierarchy.


Refactoring an entire data model shows the issue when subclassing. As soon as we introduce a new subclass, we risk the need to refactor the superclass and all its subclasses. That’s a significant impact on existing architecture and a downside of subclassing.

Let’s consider an alternate approach involving enums.

Refactoring a data model with enums

By using enums, we stay away from a hierarchical structure yet we can still keep the option of passing a single Workout around in our application. We’ll also be able to add new workouts without needing to refactor the existing workouts.

We do this by creating a Workoutenum instead of a superclass.

We can contain different workouts inside the Workoutenum.

Listing 3.Workout as an enum.

 
 enum Workout {
     case run(Run)
     case cycle(Cycle)
     case pushups(Pushups)
 }
 

Now Run, Cycle and Pushups won’t subclass Workout anymore. In fact, all the workouts can be any type. Such as a struct, class or even another enum.

We can create a Workout by passing it a Run, Cycle or Pushups workout.

For example, we can create a Pushups constant that contains the repetitions and the date of the workout. Then we can put this pushups constant inside a Workoutenum.

Listing 4.Creating a workout.

 let pushups = Pushups(repetitions: [22,20,10], date: Date())
 let workout = Workout.pushups(pushups)

Now we can pass a Workout around in our application. Whenever we want to extract the workout, we can pattern match on it.

Listing 5.Pattern matching on a workout.

 
 switch workout {
 case .run(let run):
     print("Run: \(run)")
 case .cycle(let cycle):
     print("Cycle: \(cycle)")
 case .pushups(let pushups):
     print("Pushups: \(pushups)")
 }
 

The benefit of this solution is that we can add new workouts without refactoring existing ones. For example, if we’d introduce an Abs workout, we can add it to Workout without touching Run, Cycle or Pushups.

Listing 6.Adding a new workout to the Workout enum.

 
 enum Workout {
     case run(Run)
     case cycle(Cycle)
     case pushups(Pushups)
     case abs(Abs) // New workout introduced.
 }
 

Not having to refactor other workouts to add a new one is a big benefit and worth considering using enums over subclassing.

Deciding on subclassing or enums

It’s not always easy to determine when enums or subclasses fit your data model.

When types share a lot of properties, and if you predict that in the future this won’t change, you can get far with subclassing, but subclassing steers you into a more rigid hierarchy. On top of that, you’re forced to use classes.

When similar types start to diverge, or if you want to keep using enums and structs (as opposed to classes only), then creating an encompassing enum offers more flexibility and could be the better choice.

The downside of enums is that your code needs to match on all cases in your application. Although this may require extra work when adding new cases, it’s also a safety net where the compiler makes sure you haven’t forgotten to handle a case somewhere in your application.

Another downside of enums is that they can’t be extended with new cases. Enums lock down a model to a fixed number of cases, and unless you own the code, you can’t change this rigid structure. For example, perhaps you’re offering an enum via a third-party library, and now its implementers can’t expand on it.

Exercises

    1.   Can you name two benefits of using subclassing over enums with associated types?

    2.   Can you name two benefits of using enums with associated types over subclassing?

Answers

    1.   A superclass prevents duplication; no need to declare the same property twice. With subclassing, you can also override existing functionality.

    2.   No need to refactor anything if you add another type. Whereas with subclassing you risk refactoring a superclass and its existing subclasses. Second, you aren’t forced to use classes.

That’s all for now. For more, check out the 1st chapter of Swift in Depth and see this slide deck.