A case of premature abstractions

Warning: This article contains unpopular opinions. I also recommend reading this post first to get acquainted with associated types.

Blindly following an acronym to determine your programming techniques can be tricky; Whether this acronym is P.O.P. (Protocol-Oriented Programming), V.I.P.E.R. (View, Interactor, Presenter, Entity and Router), D.R.Y. (Don’t Repeat Yourself), or anything else, there’s a risk you’re shoehorning your architecture into a general “best practice”.

Catchy acronyms sell well and can be a useful crutch, but let’s not have them dictate our work too much. Considering that there are always edge-cases, counterpoints, and the occasional it depends when doing A.O.P. (Acronym-Oriented Programming—yes, I made that up),

Let’s use this moment to explore the dangers when we try to use Swift’s protocols too much in the name of P.O.P. and D.R.Y.

Abstractions have a cost

Abstractions and protocols definitely have their place. What I love about abstractions is that they can hide internal complexities from us. When we are dealing with a Bluetooth type, we can implement cool features while being blissfully unaware of a phone’s radio waves. However, in this article, I am talking about abstracting code away by generalizing it too early; Because then we are introducing an abstraction before knowing we need one.

Your coworkers may find it easier to reason about a Couch and a Chair versus a more versatile Sittable<T: Furniture> generic struct with custom operators. A generic Sittable struct with a Furniture protocol might have a good reason to exist and can definitely warrant the abstraction. However, this abstraction has a cost in the shape of cognitive load. The more generalized you make your code, the more the readers of your code have to spend their brainpower to understand this abstraction and when to use it.

Then there’s the fact that Swift is marketed as a Protocol-Oriented Programming language, where Apple is inviting you to start developing protocol-first; which is—in my experience—a sure-fire way to add unnecessary complexity and architectural puzzle-solving in the shape of resolving associated types, decyphering where clauses, and figuring out which protocol extensions are called.

Modeling data with D.R.Y. and P.O.P.

Let’s see how changing requirements can affect us in practice. We are going to model the types for a fictional app, and see how D.R.Y. and P.O.P. may be more detrimental than useful once time progresses.

Imagine that you’re working at a young hot startup that offers an app which allows customers to store workouts. This app incentivizes customers to store body-selfies so that each customer can keep track of their physical transformation. The app may even generate fancy progress-over-time transformation gifs and videos!

To store both photos and workouts, we will need two stores: a PhotoStore and a WorkoutStore. PhotoStore stores Photo types, WorkoutStore stores Workout types. Both stores share the same CRUD (Create, Read, Update, Deleted) actions.

Being the pattern-recognizing-master that you are, you think “I know, I’ll introduce a protocol! Then I’ll only need to write the functionality once for both stores!”

Let’s try it out. First we create a new Store protocol and define all the methods on it for both the WorkoutStore and PhotoStore. In a moment we will make WorkoutStore and PhotoStore conform to Store. On the Store protocol we define some useful methods, such as being able to page through elements with an offset and amount per page, or finding a single element.

protocol Store {
    var all: [Element] { get }
    func fetchElements(offset: Int, amount: Int) -> [Element]
    func findElement(id: String) -> Element?
    func addElement(_ element: Element)
    func updateElement(_ element: Element)
    func removeElement(_ element: Element)
}

Okay, so what is this Element? We can introduce an Element protocol, but then we need to make multiple types conform to this Element protocol which gets awkward and doesn’t neatly scale. Instead, let’s do it the Swift way™ and make Element an associated type; just like how the collection protocols do it!

By introducing an associated type, each store gets to define what Element represents. For WorkoutStore this Element represents a Workout, and for PhotoStore it represents a Photo type. In Swift, we use the typealias Element = X notation to define what this element resolves to.

In Store we add the associated type:

protocol Store {
  associatedtype Element
  //... rest omitted
}

Tip: Want to master associated types? Check out Swift in Depth, Chapter 8: Putting the Pro in Protocol-Oriented Programming

Next, we need to make PhotoStore and WorkoutStore adhere to the Store protocol. We’ll leave the details out so that we can focus on the design, not the code.

extension PhotoStore: Store {
  // We state that Element is of type Photo for PhotoStore
  typealias Element = Photo 

  // implementation omitted
}

extension WorkoutStore: Store {
  // We state that Element is of type Workout for WorkoutStore
  typealias Element = Workout 

  // implementation omitted
}

Great, we can move on. But uh-oh, since we introduced an associated type, our store can only be used as a compile-time constraint—see this blog post why. In other words, passing a Store around will probably mean other developers will have to use generics sooner or later. Oh well, that’s a fair price to pay for this clever protocol, right?

Getting free functionality

Now, we’re going to utilize the power of Swift, and add extensions so that all stores get shared functionality. We’ll only need to write this code once. Let’s D.R.Y. it up!

We are introducing some properties on Store that will help with pagination. Then, we extend Store so that we can offer default implementations, so that both WorkoutStore and PhotoStore get pagination for free!

Inside the protocol, we add the elementsPerPage and numberOfPages properties and a func page(index: Int) method to request a page of data. Via an extension we get these implementations for free.

protocol Store {
  // rest omitted
  var elementsPerPage: Int { get }
  var numberOfPages: Int { get }
  func page(index: Int) -> [Element]
}

// We offer a default implementation
extension Store {
    var elementsPerPage: Int { return 50 } // Default number of elements
    var numberOfPages: Int {
        guard elementsPerPage > 0 else { return 0 }
        let pageCount = Float(all.count) / Float(elementsPerPage)
        return Int(pageCount.rounded(.up))
    }

    func page(index: Int) -> [Element] {
        return fetchElements(offset: index * elementsPerPage, amount: elementsPerPage)
    }
}

With a protocol extension it sure feels good to be able to capture the essence of a store so neatly. But, are we being too clever perhaps? Let’s not think about the consequences and keep going, we are on a roll.

Let’s give our code a test run and witness how both PhotoStore and WorkoutStore get free functionality.

let photoStore = PhotoStore()
// Sweet, we get free functionality.
photoStore.numberOfPages // 4
photoStore.page(3) // [Photo, Photo, Photo...]

// WorkoutStore also gets pagination for free!
let workoutStore = WorkoutStore()
workoutStore.page(4) // [Workout, Workout, Workout...]

The power of generalized code

Since we have a protocol, we can work with the generic Store type, allowing us to write code once which will work for all Store types, including future types!

For instance, let’s say we want to render a list of elements in an iOS UIViewController. We can use a single viewcontroller for iOS that can render elements from the Store protocol. This way, we only need to come up with a single viewcontroller that can render both workouts and photos, another reusable type, EZPZ!

Okay, maybe not so easypeasy, Store has an associated type — called Element— so we can only use Store as a generic constraint—or we need a type-erased store which is a different topic.

No big deal, we introduce a generic AStore and constrain it to the Store protocol and the compiler gods are pleased again. The AStore type is a generic name that we made up, then inside StoreViewController we can refer to this type.

final class StoreViewController<AStore: Store> {
  // We can refer to AStore internally.
  let store: AStore
  
  // ... rest omitted
}

Since it’s generic, we can store our viewcontroller with a generic notation, inside we need to specify the concrete WorkoutStore or PhotoStore type.

let storeViewController: StoreViewController<WorkoutStore>
// or
let storeViewController: StoreViewController<PhotoStore>

The StoreViewController uses a UITableView to showcase a list of elements. In the next step, we implement the UITableViewDatasource to create cells from elements. Such as the cellForRowAt method, amongst others. Inside this method we try to set the textLabel of a cell by referring to an element’s title.

override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {

   let cell = tableView.dequeueReusableCell(withIdentifier: "Element", for: indexPath)
   
  // We try to set the title
   cell.textLabel?.text = elements[indexPath.row].title
       
   return cell
}

Note that internally, the viewcontroller will be able to use the handy paging mechanisms that the store offers.

But wait, this doesn’t compile. We try to set the element’s title to the cell’s textLabel, but, element is an unconstrained associated type. It’s… anything, so it doesn’t have a title property.

Let’s solve this next.

Time for a new protocol

Our abstraction isn’t complete. To offer properties on Element types, we need to introduce another protocol.

Let’s introduce the Presentable protocol and inside we offer a title property. Then, we constrain Element to Presentable so that we can call title from the store’s elements.

Are you with me so far? It’s tricky, so here’s the diagram which sheds some light on the current architecture.

In code we introduce Presentable, constrain Element to it, and make both Photo and Workout conform to Presentable.

// The new Presentable protocol
protocol Presentable {
    var title: String { get }
}

protocol Store {
    // We constrain Element to Presentable
    associatedtype Element: Presentable

    // ... snip
}

// We make Photo conform to Presentable
struct Photo: Presentable {
    // implementation omitted
}

// We make Workout conform to Presentable
struct Workout: Presentable {
    // implementation omitted
}

Let’s stop for a second. I’d like to point out that you have to know about Store, WorkoutStore, PhotoStore, Element constrained to Presentable, and a StoreViewController which has a generic Store. This is a lot to remember for rendering two tableviews: one for workouts and one for photos. The cognitive load is quite high. Is the abstraction worth this cost?

New requirement: Being able to favorite photos

“Wouldn’t it be cool if we can favorite photos?” The startup CEO says. “That sure would be useful”, you say, “let’s do a little proof-of-concept”.

So inside the StoreViewController we need to set element.favorited to true or false.

We already added the title property to Presentable, we might as well add a favorited property, right?

Hold your ponies, workouts can not be favorited. Perhaps we abstracted our code a bit too early, because even though our code is generic, we lost some flexibility. Turns out, generic code is quite constraining sometimes! Should we make an optional favorited property? We could, but then all workouts gets an optional favorited property which is kinda messy.

We want to keep the generic StoreViewController because we try to avoid duplication, but Workout isn’t favoritable while Photo is. We painted ourselves into a corner. Let’s solve our predicament by being even more clever. We introduce another protocol, called Favoritable. Then we can use this protocol to add special methods for when an element is Favoritable.

Note how we are only making Photo conform to the new Favoritable protocol since Photo is the only thing that can be favorited.

The protocol itself is about as small as it can get.

protocol Favoritable {
  var favorited: Bool { get set }
}

Now, we need to make Photo conform to Favoritable so it can be favorited, either directly as well as from the StoreViewController.

// Photo is now Favoritable
struct Photo: Presentable, Favoritable {
    var favorited: Bool = false
    // Rest omitted
}

Next, we can specialize the cellForRowAt method, so that if the Element confoms to Favoritable, we can use the favorited property on Element.

We do this by creating an extension on StoreViewController where Element conforms to Favoritable, then we’ll put the method in there.

// We offer a special extension where the favorited property is available
extension StoreViewController where Element: Favoritable {
  override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {

     let cell = tableView.dequeueReusableCell(withIdentifier: "Element", for: indexPath)
   
     // We can now set both the title and favorited state
     let element = elements[indexPath.row].title
     cell.textLabel?.text = store.all[indexPath.row].title
     cell.favoritedIcon.hidden = !element.favorited
       
     return cell
  }
}

This code above basically states “When we have elements that conform to Favoritable, use this method instead of the default one.”

We have some duplication because cellForRow is implemented twice, but at least we spare ourselves from implementing a whole new viewcontroller.

Note that in reality, we’d probably have to add more methods in this extension, all related to cell selections and favorite toggling.

The bomb drops

We sure have been doing things the Swift way™ and pat ourselves on the back for being so clever. We use D.R.Y. to prevent duplicated code—such as a pagination system on both stores—and we did some Protocol-Oriented Programming.

Here comes the bad news. Customers haven’t been using the photos feature very much. Turns out, customers don’t like uploading progress pictures of their half-naked bodies to another startup that may not have privacy in mind. The CEO, CTO, and C-whatever-O decide to get the feature removed.

That’s a shame. But that’s okay, we merely have to delete PhotoStore and its related code, after that everything still works. However, there’s a lot of generic code remaining for only WorkoutStore, but it’s too costly to refactor it now, we have other features to deliver! So other teammembers will need to learn all about Favoritable, Presentable, Store, Element, and a bunch of extensions, just to render workouts in a tableviewcontroller.

Bomb number two

Another bomb drops, we have made a custom store with useful pagination. But, a new requirement comes in; We need to store workouts locally for offline use.

This is a tough one. The team decides to go forward with Core Data. We can try and abstract away Core Data so that our app is blissfully unaware, and then we can try and make it adhere to our Store protocol. By abstracting Core Data we can easily swap it out in the future, right?

Before we continue, let’s think about this: how often have you experienced a project where you completely swapped databases? You may consider the option to swap to Realm or SQLite; but going from Core Data to a SQL or NoSQL solution significantly impacts the app and is rare to happen without a major rewrite.

Once you go with a database, you basically marry it. So we decide that it’s okay to have Core Data exposed to our types to make our day-to-day work easier, and we kill off our Store protocol.

M.O.I.S.T.

Clever abstractions have hurt us. We were so focused on being neat and tidy with our code—and it is truly neat—that we forgot to wonder which way our project is headed.

The truth is, we just started, so we don’t really know yet which way the project can go. Still, we made abstraction on top of abstraction, which ultimately increased complexity, made the barrier higher for other devs, made us inflexible, and we risked rewriting our architecture, which we ultimately did.

As a middleground between D.R.Y. and W.E.T. (Write Everything Twice) I’d like to propose M.O.I.S.T., also known as Maybe it’s Okay to Implement the Same thing Twice.

As an alternative, we could have written a separate WorkoutViewController and PhotosViewController and avoid making a Store protocol altogether. And yes, we would have duplicate code. But it really wouldn’t have been that bad because at that stage we didn’t know yet where the project is headed. To make photos favoritable, we merely needed to add favorited to Photo and use it in PhotosViewController. Also, the cognitive load to work with workouts and photos would be much lower.

In the story, workouts needed to be stored in a Core Data store. That’s okay, we would need to remove the WorkoutStore, but that’s not as hard-hitting as undoing the generic work.

If we wanted to reduce duplication, we could first look at things that are less risky, perhaps there are other things we can reuse instead. Maybe we can share UI components, such as a common cell type, or perhaps a shared transition into the viewcontroller. Or perhaps we merely need a small generic nextPage function to help us with pagination, instead of a full-blown P.O.P. solution.

Let the code marinate for a while

When modeling your architecture, I recommend starting with duplicate code or lightweight abstractions, and let the code exist for a while. Learn what works and what does not, understand the feature on a deeper level, understand the contexts of a feature and where it might be headed. Then, and only then, consider making rich abstractions when things are more set in stone.

Some thinking traps

Some common thinking traps to detect if you’re abstracting too prematurely are:

Counter-argument

Let’s consider some trade-offs; Perhaps sometimes you do want to start with a protocol. Perhaps you’re integrating a web API that’s not finished yet so you can’t integrate it, in which case you can use a protocol instead until the API is ready. Meanwhile, you can work with mock data.

Or you already know that you’re going to mock something for testing, in which case you may want to start with a protocol too.

In my humble opinion: when using lightweight abstractions, such as using a protocol as a type—that is, no fancy extensions or constraints, perhaps a delegate—then you lower the risk of shooting yourself in the foot by starting with protocols.

Nobody is a fortune teller

Knowing upfront which direction your project is headed takes a lot of skill, business-sense and customers insights, and even then the project might take unforeseen turns.

We can’t fully predict the direction of our software, but what we can do is make educated guesses based on experience, being in touch with our customers’ needs, and ultimately keeping our fingers crossed and hope that we picked the correct abstractions that will last us a long time.

Abstracted code has a price. I’m not saying you should avoid it, on the contrary. I just want to make you aware that generalized code should be carefully taken into consideration. The benefits are reduced lines of code, code that works with types that don’t exist yet, and sometimes elegance. The downsides are the increased cognitive load required to work with and understand a codebase, which might even alienate newcomers to a project.

With duplication we get agility in return.


avatar

Thanks for reading, I hope you enjoyed the article!

I'm Tjeerd (@tjeerdintveen), an iOS freelancer, avid Swift fan, and author.

Want to keep improving your Swift skills? Get the Swift in Depth book, published by Manning. Swift in Depth


Subscribe to receive updates, discounts and an immediate 15% discount on the Swift in Depth book.

Top