Reactive Code is Sequentially Cohesive
Reactive, declarative code is sequentially cohesive: you have a sequence of events and reactions to events, and it’s pieces are tied together real close.
The processing chain itself is then a function or feature of the app. The chain of operators is a thing itself; the step-by-step transformation is then reified into a sequence in one place: it has a beginning, an end, a sequential order of steps.
That makes the encapsulation of the sequence is very clear.
Compared to some object-oriented factorings of code, reactive code is often very good at showing such sequences in one place. It’s “data-driven” inasmuch as you transform data from one format to another, and eventually produce a result or side effect.
With traditional object-oriented factoring techniques, you might be inclined to split up multiple transformational steps into isolated objects, then either couple them sequentially so information flows from object to object by virtue of delegation from A→B→C→D
, which is hard to follow and see at a glance, or have a “master sequence” call each object in succession and pipe the results from one to another.
Back when I took note of this in 2016, Rx was all the rage and looked somewhat different. To preserve the historic example that even predates Swift.URL
, here’s the original live search text field by Marin Todorov that shows the sequential nature of steps:
query.rx_text
.filter { string in
return string.characters.count > 3
}
// Coalesce rapid-firing of events
.debounce(0.51, scheduler: MainScheduler.instance)
.map { string in
let apiURL = NSURL(string: "https://api.github.com/q?=" + string)!
return NSURLRequest(URL: apiURL)
}
.flatMapLatest { request in
return NSURLSession.sharedSession().rx_data(request)
}
.map { data —> Array<AnyObject> in
let json = try NSJSONSerialization.JSONObjectWithData(data,
options: [])
return json as! Array<AnyObject>
}
.map { object in
return Repo(object: object)
}
.bindTo(tableView.rx_itemsWithCellIdentifier("Cell"))
A traditional approach might look like this, for example:
@IBOutlet var tableView: ReposTableView?
lazy var debouncer = Debouncer(interval: 0.51, delegate: self)
lazy var gitHubAPI = GitHubAPI()
lazy var jsonDecoder = JSONDecoder()
// Entry point
func handleTextChange(string: String) {
guard string.count > 3 else { return }
debouncer.debounce(string)
}
// DebouncerDelegate callback
func debouncerDidDebounce(_ debouncer: Debouncer, string: String) {
gitHubAPI.repos(forQuery: string) { [weak self] result in
switch result {
case .success(let repos):
self?.tableView.repos = repos
case .failure(_):
errorHandlingIsAnExerciseForTheReader()
}
}
You see how employing different objects here requires some back and forth already, to hand things off from the caller to the Debouncer
, then get back a debounced result at some later time via a callback: It’s already 2 functions you need to look at, 2 places that are part of a sequence, but the middle portion is invisible here. The part that glues handleTextChange
to debouncerDidDebounce
is not apparent. That’s part of the pain of splitting code into multiple functions or event objects.
You might think that it’d help to make the debouncer block-based – but since the debouncing is stateful, it makes more sense to have one shared callback, too. So you’d pass the block in the Debouncer.init
, not at the call site in debounce
, and still end up with 2 places you need to look at.
It actually is quite nice, though, that GitHubAPI
encapsulates the processing of search strings to URL
queries to Data
results to JSON
and then to Repo
objects. So there’s a sub-sequence hidden inside the repos(forQuery:)
call. (That’s totally possible in the reactive code as well, of course, but would obfuscate the nature of the example Marin provided.)