A journey of a thousand miles...

Here is some of the history behind one of the features that made into the most recent update of our iOS client: the new media library.

Here in Later we pride ourselves on aiming for a middle ground between our natural inclinations towards craftmanship and our appreciation for nimbleness. In other words, we like to move fast, but we also like to build our stuff the right way.

So, in order to move fast, we decided to rely a third party library to implement our new media library, instead of writing it from scratch.

We already had a well defined model object to represent a media item (photo or video). But the third party library that we chose required its own model object, which obvioulsy makes total sense.

Also, we want to leave our options open as much as possible, so we might or might not end up switching to a different library in the nearly future, therefore it makes sense to have a clear boundary between our code and the third party library. I call it a boundary, some people call it a wrapper, some other people would call it an abstraction layer, but the point is that we don’t want to scatter references to this third party library across our codebase, we want those references to be isolated, encapsulated in just one class.

To the point

First, let’s assume our model object, the one in our codebase, looks like this:

struct MediaItem {
    enum MediaType {
        case Image
        case Movie
    }
    
    let videoURL: NSURL
    let imageURL: NSURL
    let type: MediaType
}

The model objects required by the third party library looks like this:

struct GalleryItem {
    enum GalleryType {
        case Image
        case Video
    }
    
    let url: NSURL
    let type: GalleryType
}

We could discuss if having a property to mark a MediaItem as an image or a video is an indication that MediaItem is the wrong abstraction, but that’s beyond the scope of this post (just for the record: I think it is not the best posible abstraction, that any kind of type property in any class or structs suggests that there we are trying to model two different things with just on type)

Let’s assume also that the boundary we are building a class Called MediaBrowser, that, first of all, transforms our model objects to instances of the model objects required by the third party library:

final class MediaBrowser {
    func galleryData(media: [MediaItem]) -> [GalleryItem]? {
        return media.flatMap{ mediaItem in
            let isMovie = (mediaItem.type == .Movie)
            let url = isMovie ? mediaItem.videoURL : mediaItem.imageURL
            let type: GalleryItem.GalleryType = isMovie ? .Video : .Image
            
            return GalleryItem(url: url, type: type)
        }
    }
}

Now, that code might look quite straightforward, but in plain english, it will read like this:

For each instance of MediaItem, create an instance of GalleryItem, having in mind that, when creating an instance of GalleryItem, first, we need to check if MediaItem is a photo or a video. If it is a video, we need to pass to GalleryItem’s initialiser the value of the videoURL property in MediaItem, mark it as Video. However, it MediaItem is of type Image, we need to provide its imageURL to GalleryItem’s initialiser, as well as mark GalleryItem as Image.

It’s not rocket science. But describing what galleryData() code does requires a paragraph. And every single time you read this function, you need to translate it into that paragraph in order to understand what it is doing.

That happens because the code is quite imperative. There are a lot of specific, unnecessary details in there. Unnecessary because we don’t need to be that specific to describe what we expect the galleryData() function to do.

This is what we expect: we want to map each instance of MediaItem to a brand new instance of GalleryItem. Simple as that. However, the code, again, is telling a different, more complex, story, with plenty of details that we don’t need to be made aware of every time we read it.

The declarative solution

One of the things I like most about Swift is that it provides multiple ways to model abstractions, and multiple tools that can be used to write more declarative code.

For example, we can declare initialisers in extensions. So, we could do something like this, to extend the GalleryItem object (remember, this object is part of a third party library, and therefore out of our control):

extension GalleryItem {
    init(mediaItem: MediaItem) {
        let isMovie = (mediaItem.type == .Movie)
        let url = isMovie ? mediaItem.videoURL : mediaItem.imageURL
        let type: GalleryItem.GalleryType = isMovie ? .Video : .Image
        
        self.init(url: url, type: type)
    }
}

And now, this would be our galleryData() function:

final class MediaBrowser {
    func galleryData(media: [MediaItem]) -> [GalleryItem]? {
        return media.flatMap{GalleryItem(mediaItem: $0)}
    }
}

This function, now, clearly express my expectation: I want to create a new GalleryItem for each MediaItem. Period. I don’t care about how that happens, I don’t care about the details, I just trust GalleryItem to do the right thing and create an instance of itself correctly.

We have turned the previous, imperative code into declarative code.

Now, in plain English the galleryData() function reads:

For each instance of MediaItem, create an instance of GalleryItem.

Rinse and repeat

In the scope of this post, there is not a huge difference in terms of lines of code, or in terms of code complexity between both solutions. This, per se, is not going to turn complex code into simple code, buggy code into robust code.

But now, imagine applying the spirit of this approach, using the tools Swift provides to write declarative code, every single time there is an opportunity to do so. That would make a significant difference. Because a journey of a thousand miles begins with a single step.