Solving a Data Flow Diagram Using PromiseKit
Before they start coding, most programmers begin by creating a data flow diagram to know exactly how each piece of code is supposed to work (in other words, how it should behave). If the behavior of the application is not complex, you do not have to sketch it, although it is a good habit. However with more complicated behavior, it is hard to properly code such flow.
Data flow diagram – dedicated blocks for each function
We sit down and code, everything is fine… but how to write it down so that it makes sense? And what to do when we need to call something asynchronously (e.g., download some data from the API)? It is certainly a good idea to divide it into items and save them each in a separate method. This way we will create code blocks that are responsible for specific functionalities, which will make the whole thing cleaner.
Conditional expression – good or bad idea?
We already have code blocks, so now, how do we put them together? We have conditional blocks here, so I will wrap the whole thing in if / else and it should be ok, right? Well, yes and no. Packaging our blocks in a conditional expression will work, but it will not look good, it will simply be not very readable. Example:
class HelloDiagramFlow {
func flow() {
if isSessionValid {
userInfoBlock {
}
} else {
refreshSession { sessionKey in
userInfoBlock {
}
}
}
}
func userInfoBlock(closure:()->()) {
getUserInfoAsync { userInfo in
if isPremiumUser {
if isPremiumPlanExpired {
} else {
}
} else {
}
}
}
func getUserInfoAsync(closure:(UserInfo)->()) {
}
func refreshSession(closure:(String)->()) {
}
}
As you can see, already with the first few blocks, the code becomes quite unreadable, and our diagram is not particularly complicated. How can we fix it? We can use the so-called “promise” (I recommend to look here). Ok, but how do we go about it?
Creating a diagram with PromiseKit
We will begin by packing our blocks into our “promises” using PromiseKit.
// -- 1
//
fileprivate func sessionValid() -> Promise {
return Promise{fulfill, reject in
fulfill(helper.sessionExpired)
}
}
// -- 2
//
fileprivate func refreshToken() -> Promise {
return firstly {
Promise(value: self.helper.sessionToken)
}.then { token -> Promise in
debugPrint("- Token refreshed: \(token)")
return self.getUserInfo()
}
}
// -- 3
//
fileprivate func getUserInfo() -> Promise {
return Promise{fulfill, reject in
self.helper.queue.async {
for _ in 0...1000000000 {}
DispatchQueue.main.sync {
fulfill(self.helper.userInfo)
}
}
}
}
// -- 4
//
fileprivate func checkAccountType() -> Promise {
if let userInfo = userInfo {
debugPrint("- Account type checked: \(userInfo.accountType)")
switch userInfo.accountType {
case .premium:
return self.premiumAccountFlow()
case .regular:
return self.regularAccountFlow()
}
} else {
return Promise{fulfill, reject in reject(NSError.cancelledError())}
}
}
// -- 4.1
//
fileprivate func premiumAccountFlow() -> Promise {
return firstly {
self.isPremiumPlanExpired()
}.then { expired -> Promise in
if expired {
return self.renewPremiumAccount()
} else {
return self.premiumUserAccount()
}
}
}
// -- A
//
fileprivate func regularAccountFlow() -> Promise {
return Promise {fulfill, reject in
if let closure = finishedClosure {
closure(.regular)
}
fulfill()
}
}
// -- B
//
fileprivate func premiumUserAccount() -> Promise {
return Promise {fulfill, reject in
if let closure = finishedClosure {
closure(.premium)
}
fulfill()
}
}
// -- C
//
fileprivate func renewPremiumAccount() -> Promise {
return Promise {fulfill, reject in
if let closure = finishedClosure {
closure(.premiumRenew)
}
fulfill()
}
}
fileprivate func regularAccountFlow() -> Promise {
return Promise {fulfill, reject in
if let closure = finishedClosure {
closure(.regular)
}
fulfill()
}
}
// -- B
//
fileprivate func premiumUserAccount() -> Promise {
return Promise {fulfill, reject in
if let closure = finishedClosure {
closure(.premium)
}
fulfill()
}
}
// -- C
//
fileprivate func renewPremiumAccount() -> Promise {
return Promise {fulfill, reject in
if let closure = finishedClosure {
closure(.premiumRenew)
}
fulfill()
}
}
fileprivate func regularAccountFlow() -> Promise {
return Promise {fulfill, reject in
if let closure = finishedClosure {
closure(.regular)
}
fulfill()
}
}
// -- B
//
fileprivate func premiumUserAccount() -> Promise {
return Promise {fulfill, reject in
if let closure = finishedClosure {
closure(.premium)
}
fulfill()
}
}
// -- C
//
fileprivate func renewPremiumAccount() -> Promise {
return Promise {fulfill, reject in
if let closure = finishedClosure {
closure(.premiumRenew)
}
fulfill()
}
}
// -- 5
//
fileprivate func isPremiumPlanExpired() -> Promise {
return Promise{fulfill, reject in
fulfill(self.helper.premiumExpired)
}
}
As for the asynchronous data call, to make things simpler we do not connect to any API, but just start a loop with a large counter. However, instead of using a loop you can also put there a call to API, by using URLSession, Alamofire or something like this.
Okay, the code is nicely packaged in blocks, but it will not work yet, especially since it is not much different from the usual method. Well not exactly, see below an example of calling the whole flow, for better encapsulation I packaged this flow into a method that outputs the data we are interested in:
func start(closure:@escaping (ViewType)->()) {
finishedClosure = closure
debugPrint("Start")
firstly {
return self.sessionValid()
}.then { sessionExpired -> Promise in
debugPrint("- Is session expired: \(sessionExpired)")
if sessionExpired {
return self.refreshToken()
} else {
return self.getUserInfo()
}
}.then { userInfo -> Promise in
debugPrint("- User info downloaded: \(userInfo.email)")
self.userInfo = userInfo
return self.checkAccountType()
}.always {
debugPrint("Promises chain finished - hide activity indicator etc.")
}.catch { error in
debugPrint(error)
}
}
Boom! And that’s it? Isn’t it more readable? You can also easily add error handling anywhere in the chain of calls. So, I encourage you to get acquainted with PromiseKit or other libraries using “promises”.
You can download running code here (Swift 3.0).