Chain of responsibility is a well-known pattern that you probably ran into without realising it. In this article we’ll continue our journey on design patterns in swift and learn more about chain of responsibility pattern. We’ll cover some theory at first and then we’ll implement a cascading cache with an API failover to demonstrate a potential usage of the pattern.
Chain of Responsibility
Let’s start with a definition:
Chain of responsibility decouples a sender of a request and a receiver and it gives more than one object a chance to handle the request.
Think of this pattern as the actual chain of objects (technically it’s a linked list) where every object knows about its successor (but not predecessor). So every object has a link to the next object in the chain. When a sender creates a request the first object in the chain is called and it has the opportunity to process the request. If the first object in the chain can’t process the request it will pass on the request to the next object. And so on until the request reaches the end of the chain.
Custom Cache
We got the theory out of the way, time for some examples. We’ll try to solve the following problem… We’ll fetch a movie from our controller, and if it’s not cached in memory we’ll try to load it from the disk. If it’s not on the disk, we’ll load it from the API. And if the API call fails, we’ll fall back to a backup API. If that one fails, well, tough cookie.
Here we can see the chain of events as they’re supposed to happen. Let’s visualise them:
Every rectangle on the diagram will actually be one class that will have a chance to handle the request. For example, if we can’t find the movie in the primary cache or in the secondary cache we make a call to the API endpoint. The call succeeds, and feeds the results back up the chain. This way the primary and secondary cache will have a chance to store the newly fetched object if they choose to.
The Code
It’s time to see some code 🙂 All our classes in the diagram above will implement the same protocol. Here’s the general idea on how the protocol will look like:
The actual protocol will be slightly different, but the idea is the same. We will have a pointer to the next responder in the chain and we’ll have a function to implement.
Protocols
For the purposes of this example, we’ll be using two main protocols. First one is for the handler:
protocol GetMovieHandler { init(nextHandler: GetMovieHandler?) func getMovie(_ movieID: Int, onCompleted: ((MovieItem?) -> ())?) }
All the objects in the chain will implement this one. We’ll also be using a ‘MovieItem’:
protocol MovieItem { var movieID: Int { get } var title: String { get } var synopsis: String { get} var year: Int { get } }
The other protocols in the example project are less important. These two are the main ones.
Classes
We’ll start creating the classes that we’ll be using in the main chain. Let’s start from the top. The primary cache will be fetching objects from memory and it could look something like this:
class GetMovieFromCache: GetMovieHandler { private let nextHandler: GetMovieHandler? required init(nextHandler: GetMovieHandler?) { self.nextHandler = nextHandler } func getMovie(_ movieID: Int, onCompleted: ((MovieItem?) -> ())?) { // You could use NSCache here or a Dictionary... What ever floats your boat. let success = Bool.random // Not all movies will be cached, so let's simulate some failures. if success { let movieItem = CachedMovieItem(movieID: movieID, title: "Very Fast Movie", synopsis: "You wanted a movie really fast... Well, you got it", year: 2018) onCompleted?(movieItem) } else { nextHandler?.getMovie(movieID) { item in if item != nil { // Since we didn't have the movie cached in the first place, maybe we'll cache it if the next responder has it. } onCompleted?(item) } } } // For testing only private struct CachedMovieItem: MovieItem { var movieID: Int var title: String var synopsis: String var year: Int } }
These examples won’t include any business logic. That will be specific to your project. They will illustrate how the pattern works on a real world example. In the constructor we saved our next handler. When the ‘getMovie’ function is called we’re simulating a cache failure. If the object is not in the local cache, we’re simply calling the next handler with the same parameters. When the ‘nextHandler’ calls the closure with the result, we’ll check if we got a new movie and save it in memory. As a final step, we’re calling the ‘onCompleted’ closure with the movie item.
All the classes in the chain will follow a pretty similar pattern. Check if the object is available, if not, call the next handler, if you get an object back from it, save it and pass it on. API handler is slightly different, it’s not saving the item, it’s just passing it on:
func getMovie(_ movieID: Int, onCompleted: ((MovieItem?) -> ())?) { // This should be a real API request let success = Bool.random // Simulate random API failures if success { let movieItem = APIMovieItem(movieID: movieID, title: "Real Movie", synopsis: "This is, without a doubt, a real movie", year: 1999) onCompleted?(movieItem) } else { nextHandler?.getMovie(movieID, onCompleted: onCompleted) } }
The only exception here is the ‘EndOfChain’ class implementation, which is just a stub implementation. If your request reaches this class, there’s nothing to do but to print an error message:
class EndOfChain: GetMovieHandler { required init(nextHandler: GetMovieHandler?) { // Stub Implementation } func getMovie(_ movieID: Int, onCompleted: ((MovieItem?) -> ())?) { print("If you're reading this, it't the end of the road my friend :)") onCompleted?(nil) } }
Building the Chain
We have to build the chain next. And we’ll start from the end of the chain. It’s quite simple really. Here’s the whole class for building the main and dummy chains:
class MovieChainBuilder { static func mainChain() -> GetMovieHandler { let endOfChain = EndOfChain(nextHandler: nil) let secondaryEndpoint = GetMovieFromSecondaryEndpoint(nextHandler: endOfChain) let primaryEndpoint = GetMovieFromEndpoint(nextHandler: secondaryEndpoint) let secondaryCache = GetMovieFromDisk(nextHandler: primaryEndpoint) let primaryCache = GetMovieFromCache(nextHandler: secondaryCache) return primaryCache } static func dummyChain() -> GetMovieHandler { return DummyGetMovie(nextHandler: nil) } }
You can see a function here that returns a ‘dummyChain’. It’s only here to illustrate that you can dynamically create many different types of chains. You should keep all the dummy classes in your test target 🙂
Using the Chain
In this example I assumed you’ll have a controller of some sort that will use this chain. Your controller will probably have a lot of business logic in it. The skeleton of it might look something like this:
class MovieController: MovieControllable { // This is your main controller for the movie. You'll probably do some fancy business logic here. private let movieHandler: GetMovieHandler required init(movieHandler: GetMovieHandler) { self.movieHandler = movieHandler } func getMovie(_ movieID: Int, onCompleted: ((MovieItem?) -> ())?) { self.movieHandler.getMovie(movieID, onCompleted: onCompleted) } }
We’re just passing the results here to the caller, so we can print them in the console. You’ll probably want to do something with your movie object that you got back from the handler. Notice how we can initialise this controller with many different chains and it will just work. From the controller’s point of view, it’s working with a single object. When, in reality, it’s working with five different objects. The controller is oblivious, as it should be.
Test Ride
In the ‘AppDelegate’ we’ll have a very fancy method for testing this out:
private func testMoviesController() { let movieController = MovieController(movieHandler: MovieChainBuilder.mainChain()) for i in 0...10000 { movieController.getMovie(i) { (item) in print("movie item: \(item?.title)") } } }
We build the controller with the main chain and simply try and get 10k movies. If we look at the console output we’ll see how the chain works:
movie item: Optional("Cached Movie") movie item: Optional("Real Movie") movie item: Optional("Cached Movie") movie item: Optional("Very Fast Movie") movie item: Optional("Very Fast Movie") movie item: Optional("Very Fast Movie") movie item: Optional("Very Fast Movie") movie item: Optional("Cached Movie") If you're reading this, it't the end of the road my friend :) movie item: nil movie item: Optional("Very Fast Movie") movie item: Optional("Cached Movie")
Sometimes we got a movie from the primary cache, sometimes from the API. And some of the calls even managed to reach the end of the chain.
Conclusion
Chain of responsibility is a great pattern when you need to give multiple objects a chance to process a request. The example we used here was focusing around caching and fetching objects from the API. This is only one example of how you could use this pattern. One other famous example is processing touch events on the UI. I’ll let you research that one yourself 🙂
This was a fun little article with a practical example on how to use the pattern. I hope you had as much fun reading it as I did writing it. And I really hope you learned something new today 🙂 All the code is available on the GitHub repo.
Have a nice day 🙂
~D;
Pingback: Building the Enigma Machine in Swift | agostini.tech
Great practical example! Thanks for sharing.
Thanks a lot and you’re welcome 🙂