Building a network cache with the facade pattern

Let's take the example of a resource-caching library. When building our applications or servers, we often need to leverage caching to avoid repeatedly making the same network calls, impacting on the performance of our programs. Server-side, it's particularly important to reduce the number of database or external calls, making our servers faster by saving network round-trips.

Let's call this system CachedNetworking, and we'll implement it with the facade pattern.

Such a system may be composed of:

A cache will have a simple interface that abstracts away the storage of the data. With simple setters, removers, and getters, this cache can be used in multiple scenarios, and could potentially be refactored in a generic way:

class Cache {
func set(response: URLResponse, data: Data, for request: URLRequest) {
// TODO: Implement me
}

func get(for request: URLRequest) -> (URLResponse, Data)? {
// TODO: Implement me
}

func remove(for request: URLRequest) {
// TODO: Implement me
}

func allData() -> [URLRequest: (URLResponse, Data)] {
return [:]
}
}

CacheCleaner is a simple object that periodically visits the cache and removes stale data:

class CacheCleaner {
let cache: Cache
var isRunning: Bool {
return timer != nil
}

private var timer: Timer?
init(cache: Cache) {
self.cache = cache
}

func startIfNeeded() {
if isRunning { return }
timer = Timer.scheduledTimer(withTimeInterval: 10.0, repeats:
true) { [unowned self] (timer) in
let cacheData = self.cache.allData()
// TODO: inspect cache data, and remove cached elemtns
//that are too old
}
}

func stop() {
timer?.invalidate()
timer = nil
}
}

Now that we have the base objects that have the complex logic, let's implement our facade on a CachedNetworking class. The facade should have a very simple interface, in order to yield the most benefit:

class CachedNetworking {
let session: URLSession
private
let cache = Cache()
private lazy var cleaner = CacheCleaner(cache: cache)

In our case, it's reasonable to let the user of the facade configure the URLSession to be used, or at least the configuration for the URLSession:

    init(configuration: URLSessionConfiguration) {
session = URLSession(configuration: configuration)
}

init(session: URLSession) {
self.session = session
}

init() {
self.session = URLSession.shared
}

With the initializers implemented, we can now get to the run(...) method. Note that we didn't keep the same naming and signature as the URLSession, as the dataTask will be run automatically. There's is no similar method in URLSession.

Other implementations would perhaps use a subclass of URLSession, and the implementations may be different in this case:

    func run(with request: URLRequest, completionHandler: @escaping (Data?, URLResponse?, Error?, Bool) -> Void) {

First, when a request comes in, let's poke the cache and check if there is a valid (response, data) pair, and, if there is, call back immediately with the last argument set to true to indicate it came from the cache:

        if let (response, data) = cache.get(for: request) {
completionHandler(data, response, nil, true)
return
}

Otherwise, perform the original request:

cleaner.startIfNeeded()
session.dataTask(with: request) { [weak self] (data, response, error) in
if let data = data,
let response = response {

If we're successful, we have data and a response to cache, so let's save it:

self?.cache.set(response: response, data: data, for: request)
}

Finally, call completionHandler to notify the caller that the response is available to consume:

 completionHandler(data, response, error, false)
}.resume()
}

We finish with a deinit call to stop the cleaner and ensure there is nothing leaking:

    deinit {
cleaner.stop()
}
}

Now you can use URLSessionFacade, and benefit from the caching capabilities. The facade pattern abstracted away three complex components in a simple interface:

Those three components are testable on their own, and can evolve independently, reducing the overall maintenance cost.

Let's now have a look at how we could leverage the proxy pattern in order to add logging capabilities to the CachedNetworking class.