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)

Dowiedz się więcej

Clean-Swift – ogólny zarys architektury

Architektura Clean-Swift jest bardzo prostą architekturą, niewymagającą żadnych dodatkowych bibliotek. Składa się z 3 warstw, które bardzo dobrze rozdzielają widoki od logiki. Sprawdź, czym się charakteryzują i jakie są między nimi zależności.
Przeczytaj

Dynamiczna lokalizacja powiadomień na iOS-a, czyli jak wysyłać wiadomości w różnych językach

Jak ustanowić łącznik pomiędzy użytkownikiem, dostawcą a aplikacją? Wykorzystaj powiadomienia systemowe. Dzięki nim użytkownik szybko otrzymuje najnowsze informacje. To rozwiązanie łatwo zastosujesz w aplikacjach zorientowanych na jeden rynek. Problemy zaczynają się, gdy treści muszą być tworzone w różnych językach. Wtedy pomocą służy dynamiczna lokalizacja zdalnych powiadomień bez wykorzystania własnego serwera.
Przeczytaj

WebSockets na iOS-a – komunikacja w czasie rzeczywistym, która nie spowalnia aplikacji

Kiedy chcesz pobrać dane do aplikacji, zwykle pewnie wykorzystujesz interfejs API RESTful. Wystarczy zapytać serwer o paczkę danych i gotowe. To dobra metoda, jeśli aplikacja nie potrzebuje stałego dostępu do nowych informacji. A co jeśli treści muszą się odświeżać w czasie rzeczywistym? Wtedy opóźnienie może np. uniemożliwiać dokonanie zakupu albo postawienie zakładu sportowego. Na szczęście z pomocą przychodzą WebSockets na iOS-a. Sprawdź, jak je zaimplementować i dlaczego warto to zrobić.
Przeczytaj

Wycena projektu

Sprawdź, jak wykorzystujemy naszą wiedzę w praktyce i stwórz z nami swój projekt.

Dlaczego warto rozwijać z nami projekty?

Logo Mobile Trends Awards

Mobile Trends Awards 2017

Nominacja w kategorii
M-COMMERCE

17

opinii klientów

Clutch logo
Logo Legalni bukmacherzy

Nagroda Legalnych Bukmacherów 2019

Najlepsza aplikacja mobilna

60+

zrealizowanych projektów