Rozwiązywanie diagramu przepływu danych za pomocą PromiseKit
Większość programistów zanim rozpocznie kodowanie konkretnej rzeczy na samym początku tworzy diagram przepływu danych. Dzięki temu można dokładnie zobaczyć, jak ma działać każdy fragment kodu (lub inaczej, jak ma się zachowywać). Przy mało skomplikowanym zachowaniu nie trzeba tego rozrysowywać, chociaż jest to dobry nawyk. Jednak w bardziej złożonych przypadkach, nie jest łatwo poprawnie opisać kodem taki przepływ.
Diagram przepływu danych – osobne bloki dla każdej funkcji
Siadamy i klepiemy kod, wszystko ok… ale jak to zapisać, żeby miało to ręce i nogi? I co w momencie, kiedy musimy wywołać coś asynchronicznie i np. pobrać jakieś dane z API? Na pewno dobrym rozwiązaniem będzie wydzielenie każdego elementu i zapisanie go w osobnej metodzie. W ten sposób stworzymy bloki kodu odpowiedzialne za konkretną funkcjonalność, przez co całość stanie się bardziej przejrzysta.
Wyrażenie warunkowe – dobry czy zły pomysł?
Mamy bloki kodu i jak to teraz poskładać w całość? Pierwsza myśli zwykle jest taka, że skoro są to bloki warunkowe, to po prostu opakujemy całość w if / else i będzie w porządku. Otóż nie do końca. Samo opakowanie naszych bloków w wyrażenie warunkowe zadziała, ale nie będzie wyglądało dobrze – będzie po prostu mało czytelne. Przykład:
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)->()) {
}
}
Jak widać, już przy pierwszych kilku blokach kod staje się mało czytelny, a nasz diagram specjalnie skomplikowany nie jest. Jak temu zaradzić? Otóż możemy skorzystać z tak zwanej “obietnicy”. Osoby spragnione szczegółów zachęcam do zajrzenia tutaj. No dobrze, ale jak się do tego zabrać?
Tworzenie diagramu z PromiseKit
Zaczniemy od opakowania naszych bloków w nasze “obietnice” korzystając z 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)
}
}
Jeśli chodzi o asynchroniczne wywołanie danych, to w celu uproszczenia nie łączymy się z żadnym API tylko odpalamy pętlę z dużym licznikiem. Nic jednak nie stoi na przeszkodzie, aby zamiast pętli wrzucić tam odwołanie do API np. za pomocą URLSession czy Alamofire itp.
Ok, mamy kod ładnie opakowany w bloki, ale to jeszcze nie zadziała. Tym bardziej, że póki co, to rozwiązanie niewiele różni się od standardowego. Otóż nie do końca, poniżej przykład wywołania całego flow. Dla lepszej enkapsulacji opakowałem je w metodę, która wystawia interesujące nas dane wynikowe na zewnątrz:
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! I to wszystko. Prawda, że jest czytelniej? Dodatkowo można bezproblemowo dorzucić obsługę błędów w dowolnym miejscu łańcucha wywołań. Zachęcam do bliższego zapoznania się z PromiseKitem lub innymi bibliotekami korzystającymi z “obietnicy”.
Działający kod do pobrania tutaj (Swift 3.0)