#1 Diary of a Secret App: Networking

On my current project1, I had the following goals for my network layer:

  1. Easy to use and understand
  2. No dependency with external 3rd party libraries
  3. Easy to test

1. Easy to use and understand

The main class is 10 lines of code:

class Network : Connection {

    let session : NSURLSession
    let baseURL : NSURL

    init(session: NSURLSession = NSURLSession(configuration: NSURLSessionConfiguration.defaultSessionConfiguration()), baseURL: NSURL) {
        self.session = session
        self.baseURL = baseURL
    }

    deinit {
        self.cancelAllConnections()
    }
}

And, as you can see, most of the work is done by the Connection via Protocol Extension:

protocol Connection {

  var session : NSURLSession { get }
  var baseURL : NSURL { get }

  func makeConnection(resource: Resource) -> SignalProducer<NSData, Error>
  func makeConnection(resource: Resource) -> SignalProducer<(NSData, NSURLResponse), Error>

  func isConnected() -> SignalProducer<Bool, Error>
  func cancelAllConnections()
}

The extension itself has 50 lines of code and the methods are quite straightforward.

2. No dependency with external 3rd party libraries2

When I say no external dependency, I am referring to specialised network libs like Alamofire. I think NSURLSession gives me everything I need, so I prefer to keep the number of dependencies to a bare minimum.

3. Easy to test

With Venmo's DVR, this is particular easy to achieve. Because the Network takes a NSURLSession, I can just pass the one that DVR provides. A test setup looks like this:

let session = Session(cassetteName: "feed_cassette")  
let network = Network(session: session, baseURL: NSURL(string: "http://api.co.uk)!)  
let resource = Resource(path: "feed", method: .GET, body: nil, headers: nil)

// Let the test begin

The Problem

With all this in place, this brings me to my current challenge. Recently, I found out that the API would return a 304 if the resource wasn't modified:

  • When a new request needs to be sent, if it's not the first time it was sent, a If-Modified-Since header should be passed to the API, with the Date of the last time the same request was made.

Implementation wise, it's quite easy:

  • Use a dictionary [NSURL:String] to map from URLs to dates. To keep it simple, I use a String to represent NSDates. I think it would be an overkill to translate it to a NSDate when it comes in the response, and then translate it again to a String3, when it is about to be inserted in the request's header.

Obviously, the problem is not the implementation itself, but where this work should reside. After some time meditating about it, I settled with composition:

class Network304Modifier : Connection, ComposedConnection {}  

The new element here is the ComposedConnection protocol:

protocol ComposedConnection {  
    var connection : Connection { get }
    init(connection: Connection)
}

The idea is quite simple: I will use dependency injection and pass a Connection instance to the Network304Modifier class. The former will then be used to make the connection. The implementation of the makeConnection(resource: Resource) -> SignalProducer<(NSData, NSURLResponse), Error> looks like this:

func makeConnection(resource : Resource) -> SignalProducer<(NSData, NSURLResponse), Error>{  
   let modifiedResource = self.addIfModifiedSinceHeaders(resource)
   return connection.makeConnection(modifiedResource)
            .filter { return self.statusCodeNot304($0.1) }
            .then { self.saveLastModifiedDate($1) }
}

This method does two things:

  1. It creates a new Resource with the new header (if it's not the first time being made).
  2. Saves the last modified date as a side effect - next time the same request is made, it will then be modified.

The benefits I can see in this approach, versus others I contemplated, are:

  • It preserves the Network class (it's untouched). All the unit tests, won't need any tweaks to accommodate changes. One of my first thoughts, was to just add the 304 logic to the vanilla Network class.
  • It's testable, since I am using dependency injection, I can pass a stubbed Connection instance to the Network304Modifier:
let network = Network304ModifierStub()  
let network304Modifier = Network304Modifier(connection: network)
  • It's easy to reason about it. The responsibilities of making the request and modifying the headers are neatly separated.
  • Any place in the code that uses the original Network class, can immediately start using the Network304Modifier.
  • It's extensible. If I need to add a 301 handler, I can create Network301Modifier that will handle that:
let network = Network(baseURL: NSURL(string: "http://api.co.uk/")!)  
let network304Modifier = Network304Modifier(connection: network)  
let network301Modifier = Network301Modifier(connection: network304Modifier)

// Pass the network301Modifier to the ViewModel
  • In fact I can pretty much create any modifier, for example one to log me errors to Logstash, or one for Analytics, the original Network class is completely oblivious about it (or any intervenient).

Protocol extensions were crucial for this design, since some of the methods have the same implementation. The makeConnection(resource: Resource) -> SignalProducer<NSData, Error>method, has the following default implementation:

func makeConnection(resource: Resource) -> SignalProducer<NSData, Error> {  
   return self.makeConnection(resource) . map { $0.0 }
   }

To conclude:

  1. Your classes should do one thing and one thing only 4.
  2. Create your architecture with modularity/extensibility in mind.
  3. Your code should be testable
  4. Protocols and Dependency Injection will help you achieving all the above.5
  1. Imagine Uber, but for Instagram.

  2. This is almost true, since I use ReactiveCocoa's NSURLSession extension.

  3. Any feedback on this approach is appreciated.

  4. Single Responsibility Principle

  5. Of course, it's not a silver bullet.