Rozwiązywanie diagramu przepływu danych za pomocą PromiseKit

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)

Wycena projektu

Opowiedz nam o swoim projekcie i napisz, jak możemy Ci pomóc.

Dlaczego warto rozwijać z nami projekty?

Logo Mobile Trends Awards

Mobile Trends Awards 2021

Wygrana w kategorii
ŻYCIE CODZIENNE

Nagroda Legalnych Bukmacherów

Nagroda Legalnych Bukmacherów 2019

Najlepsza aplikacja mobilna

Mobile Trends Awards logo

Mobile Trends Awards 2023

Wygrana w kategorii
MCOMMERCE ROZWÓJ

23

opinie klientów

Clutch logo