Widzę dwa typowe wzorce bloków w Objective-C. Jedna to para sukcesów: / porażka: bloki, druga to pojedyncze zakończenie: blok.
Załóżmy na przykład, że mam zadanie, które zwróci obiekt asynchronicznie, a to zadanie może się nie powieść. Pierwszy wzór to -taskWithSuccess:(void (^)(id object))success failure:(void (^)(NSError *error))failure
. Drugi wzór to -taskWithCompletion:(void (^)(id object, NSError *error))completion
.
Brak powodzenia:
[target taskWithSuccess:^(id object) {
// W00t! I've got my object
} failure:^(NSError *error) {
// Oh noes! report the failure.
}];
ukończenie:
[target taskWithCompletion:^(id object, NSError *error) {
if (object) {
// W00t! I've got my object
} else {
// Oh noes! report the failure.
}
}];
Jaki jest preferowany wzór? Jakie są mocne i słabe strony? Kiedy użyjesz jednego nad drugim?
design-patterns
objective-c
Jeffery Thomas
źródło
źródło
Odpowiedzi:
Wywołanie zwrotne zakończenia (w przeciwieństwie do pary sukces / porażka) jest bardziej ogólne. Jeśli musisz przygotować jakiś kontekst przed zajęciem się statusem zwrotu, możesz to zrobić tuż przed klauzulą „if (object)”. W przypadku powodzenia / niepowodzenia musisz zduplikować ten kod. Zależy to oczywiście od semantyki wywołania zwrotnego.
źródło
-task…
mógłby zwrócić obiekt, ale obiekt nie jest w poprawnym stanie, nadal trzeba będzie obsługiwać błędy w warunkach powodzenia.Powiedziałbym, że to, czy interfejs API zapewnia jedną procedurę obsługi zakończenia, czy parę bloków sukcesu / niepowodzenia, jest przede wszystkim kwestią osobistych preferencji.
Oba podejścia mają zalety i wady, choć różnice są tylko nieznacznie.
Weź pod uwagę, że istnieją również inne warianty, na przykład gdy jeden moduł obsługi zakończenia może mieć tylko jeden parametr łączący ostateczny wynik lub potencjalny błąd:
Celem tego podpisu jest to, że moduł obsługi zakończenia może być używany ogólnie w innych interfejsach API.
Na przykład w kategorii dla NSArray istnieje metoda,
forEachApplyTask:completion:
która sekwencyjnie wywołuje zadanie dla każdego obiektu i przerywa IFF pętli, wystąpił błąd. Ponieważ ta metoda sama w sobie jest również asynchroniczna, ma również moduł obsługi zakończenia:W rzeczywistości,
completion_t
jak zdefiniowano powyżej, jest wystarczająco ogólny i wystarczający do obsługi wszystkich scenariuszy.Istnieją jednak inne sposoby wykonywania zadania asynchronicznego w celu zasygnalizowania powiadomienia o zakończeniu do strony wywołującej:
Obietnice
Obietnice, zwane także „kontraktami terminowymi”, „odroczonymi” lub „opóźnionymi” reprezentują ostateczny wynik zadania asynchronicznego (patrz także: wiki Kontrakty terminowe i obietnice ).
Początkowo obietnica jest w stanie „w toku”. Oznacza to, że jego „wartość” nie została jeszcze oceniona i nie jest jeszcze dostępna.
W Celu C obietnica byłaby zwykłym przedmiotem, który zostanie zwrócony z metody asynchronicznej, jak pokazano poniżej:
Tymczasem zadania asynchroniczne zaczynają oceniać swój wynik.
Zauważ też, że nie ma modułu obsługi zakończenia. Zamiast tego obietnica zapewni skuteczniejsze środki, dzięki którym strona wywołująca może uzyskać ostateczny wynik zadania asynchronicznego, co wkrótce zobaczymy.
Zadanie asynchroniczne, które utworzyło obiekt obietnicy, MUSI ostatecznie „rozwiązać” obietnicę. Oznacza to, że ponieważ zadanie może albo zakończyć się sukcesem, albo porażką, MUSI albo „spełnić” obietnicę przekazując mu oceniany wynik, albo MUSI „odrzucić” przekazaną obietnicę błąd wskazujący przyczynę niepowodzenia.
Kiedy obietnica została rozwiązana, nie może już zmienić swojego stanu, w tym jego wartości.
Po rozwiązaniu obietnicy strona wywołująca może uzyskać wynik (niezależnie od tego, czy się nie powiodła, czy też powiodła). Sposób realizacji zależy od tego, czy obietnica jest realizowana przy użyciu stylu synchronicznego, czy asynchronicznego.
Obietnicę można wdrożyć w stylu synchronicznym lub asynchronicznym, co prowadzi do blokowania lub nieblokowania semantyki.
W stylu synchronicznym w celu odzyskania wartości obietnicy strona wywołująca użyłaby metody, która zablokuje bieżący wątek, dopóki obietnica nie zostanie rozwiązana przez zadanie asynchroniczne, a ostateczny wynik będzie dostępny.
W stylu asynchronicznym strona wywołująca rejestruje połączenia zwrotne lub bloki obsługi, które są wywoływane natychmiast po rozstrzygnięciu obietnicy.
Okazało się, że styl synchroniczny ma wiele istotnych wad, które skutecznie pokonują zalety zadań asynchronicznych. Interesujący artykuł na temat obecnie wadliwej implementacji „futures” w standardowej bibliotece C ++ 11 można przeczytać tutaj: Złamane obietnice - C ++ 0x futures .
Jak w Objective-C strona wywołująca uzyska wynik?
Cóż, prawdopodobnie najlepiej jest pokazać kilka przykładów. Istnieje kilka bibliotek, które realizują obietnicę (patrz linki poniżej).
Jednak w przypadku kolejnych fragmentów kodu użyję konkretnej implementacji biblioteki Promise, dostępnej w GitHub RXPromise . Jestem autorem RXPromise.
Inne implementacje mogą mieć podobny interfejs API, ale mogą występować niewielkie i prawdopodobnie subtelne różnice w składni. RXPromise to wersja Objective-C specyfikacji Promise / A +, która definiuje otwarty standard dla solidnych i interoperacyjnych implementacji obietnic w JavaScript.
Wszystkie wymienione poniżej biblioteki obietnic implementują styl asynchroniczny.
Istnieją dość znaczące różnice między różnymi implementacjami. RXPromise wewnętrznie wykorzystuje bibliotekę wysyłkową, jest w pełni bezpieczny dla wątków, wyjątkowo lekki, a także zapewnia szereg dodatkowych przydatnych funkcji, takich jak anulowanie.
Witryna wywołująca uzyskuje ostateczny wynik zadania asynchronicznego poprzez „rejestrację” procedur obsługi. „Specyfikacja Promise / A +” określa metodę
then
.Metoda
then
Z RXPromise wygląda to następująco:
gdzie SuccessHandler to blok, który jest wywoływany, gdy obietnica została „spełniona”, a errorHandler to blok, który jest wywoływany, gdy obietnica została „odrzucona”.
W RXPromise bloki procedury obsługi mają następującą sygnaturę:
Program obsługi sukcesu ma wynik parametru, który jest oczywiście ostatecznym wynikiem zadania asynchronicznego. Podobnie moduł obsługi błędów zawiera błąd parametru, który jest błędem zgłaszanym przez zadanie asynchroniczne w przypadku niepowodzenia.
Oba bloki mają wartość zwracaną. O co chodzi w tej wartości zwrotnej, wkrótce stanie się jasne.
W RXPromise
then
jest właściwością, która zwraca blok. Ten blok ma dwa parametry: blok obsługi sukcesu i blok obsługi błędów. Procedury obsługi muszą być zdefiniowane przez stronę wywołującą.Tak więc wyrażenie
promise.then(success_handler, error_handler);
jest krótką formąMożemy napisać jeszcze bardziej zwięzły kod:
Kod brzmi: „Wykonaj doSomethingAsync, gdy się powiedzie, a następnie uruchom moduł obsługi sukcesu”.
W tym przypadku procedura obsługi błędów
nil
oznacza, że w przypadku błędu nie będzie obsługiwana w tej obietnicy.Innym ważnym faktem jest to, że wywołanie bloku zwróconego z właściwości
then
zwróci obietnicę:Podczas wywoływania bloku zwróconego z właściwości
then
„odbiorca” zwraca nową obietnicę, obietnicę podrzędną . Odbiorca staje się obietnicą rodzica .Co to znaczy?
Dzięki temu możemy „łączyć” zadania asynchroniczne, które skutecznie są wykonywane sekwencyjnie.
Ponadto wartość zwracana przez jedną z procedur obsługi stanie się „wartością” zwróconej obietnicy. Tak więc, jeśli zadanie powiedzie się z ostatecznym wynikiem @ „OK”, zwrócona obietnica zostanie „rozwiązana” (to znaczy „spełniona”) o wartości @ „OK”:
Podobnie, gdy zadanie asynchroniczne nie powiedzie się, zwrócona obietnica zostanie rozwiązana (czyli „odrzucona”) z błędem.
Przewodnik może również zwrócić inną obietnicę. Na przykład, gdy ten moduł obsługi wykonuje inne zadanie asynchroniczne. Za pomocą tego mechanizmu możemy „łączyć” zadania asynchroniczne:
Jeśli nie ma przyrzeczenia podrzędnego, wartość zwracana nie ma wpływu.
Bardziej złożony przykład:
Tutaj wykonujemy
asyncTaskA
,asyncTaskB
,asyncTaskC
iasyncTaskD
kolejno - i każdego kolejnego zadania wykonuje wynik z poprzedniego zadania jako dane wejściowe:Taki „łańcuch” nazywa się również „kontynuacją”.
Obsługa błędów
Obietnice sprawiają, że szczególnie łatwo jest obsługiwać błędy. Błędy będą „przekazywane” od rodzica do dziecka, jeśli w obietnicy nadrzędnej nie zdefiniowano modułu obsługi błędów. Błąd będzie przekazywany w górę łańcucha, dopóki dziecko go nie obsłuży. Zatem mając powyższy łańcuch, możemy zaimplementować obsługę błędów po prostu dodając kolejną „kontynuację”, która dotyczy potencjalnego błędu, który może wystąpić gdziekolwiek powyżej :
Jest to podobne do prawdopodobnie bardziej znanego stylu synchronicznego z obsługą wyjątków:
Obietnice mają ogólnie inne przydatne funkcje:
Na przykład, mając odniesienie do obietnicy,
then
można „zarejestrować” dowolną liczbę osób zajmujących się obsługą. W RXPromise rejestrowanie procedur obsługi może nastąpić w dowolnym momencie i z dowolnego wątku, ponieważ jest w pełni bezpieczny dla wątków.RXPromise ma kilka bardziej użytecznych funkcji, które nie są wymagane przez specyfikację Promise / A +. Jednym z nich jest „anulowanie”.
Okazało się, że „anulowanie” jest nieocenioną i ważną cechą. Na przykład strona wywołująca zawierająca odniesienie do obietnicy może wysłać do niej
cancel
wiadomość w celu wskazania, że nie jest już zainteresowany ostatecznym wynikiem.Wyobraź sobie asynchroniczne zadanie, które ładuje obraz z sieci i które powinno być wyświetlane w kontrolerze widoku. Jeśli użytkownik odejdzie od bieżącego kontrolera widoku, programista może zaimplementować kod, który wysyła komunikat anulowania do imagePromise , co z kolei uruchamia procedurę obsługi błędów zdefiniowaną przez Operację żądania HTTP, w której żądanie zostanie anulowane.
W RXPromise komunikat anulowania będzie przekazywany tylko od rodzica do jego dzieci, ale nie odwrotnie. Oznacza to, że obietnica „root” anuluje wszystkie obietnice dla dzieci. Ale obietnica dziecięca anuluje „oddział”, w którym jest rodzicem. Wiadomość anulowania zostanie również przekazana dzieciom, jeśli obietnica została już rozwiązana.
Zadanie asynchroniczne może samo zarejestrować moduł obsługi dla własnej obietnicy, a tym samym może wykryć, kiedy ktoś go anulował. Może wtedy przedwcześnie przestać wykonywać możliwie długie i kosztowne zadania.
Oto kilka innych implementacji Obietnic w Objective-C znalezionych na GitHub:
https://github.com/Schoonology/aplus-objc
https://github.com/affablebloke/deferred-objective-c
https://github.com/bww/FutureKit
https://github.com/jkubicek/JKPromises
https://github.com/Strilanc/ObjC-CollapsingFutures
https://github.com/b52/OMPromises
https://github.com/mproberts/objc-promise
https://github.com/klaaspieter/Promise
https: //github.com/jameswomack/Promise
https://github.com/nilfs/promise-objc
https://github.com/mxcl/PromiseKit
https://github.com/apleshkov/promises-aplus
https: // github.com/KptainO/Rebelle
i moja własna implementacja: RXPromise .
Ta lista prawdopodobnie nie jest kompletna!
Wybierając trzecią bibliotekę do swojego projektu, sprawdź dokładnie, czy implementacja biblioteki spełnia poniższe wymagania wstępne:
Niezawodna biblioteka obietnic MUSI być bezpieczna dla wątków!
Chodzi o przetwarzanie asynchroniczne, a my chcemy wykorzystywać wiele procesorów i wykonywać je jednocześnie w różnych wątkach, gdy tylko jest to możliwe. Bądź ostrożny, większość implementacji nie jest bezpieczna dla wątków!
Procedury obsługi MUSZĄ być wywoływane asynchronicznie w odniesieniu do strony wywołującej! Zawsze i bez względu na wszystko!
Każda przyzwoita implementacja powinna również przestrzegać bardzo ścisłego wzorca podczas wywoływania funkcji asynchronicznych. Wielu implementatorów ma tendencję do „optymalizacji” przypadku, w którym moduł obsługi zostanie wywołany synchronicznie, gdy obietnica zostanie już rozwiązana, gdy moduł obsługi zarejestruje się. Może to powodować różnego rodzaju problemy. Zobacz Nie uwalniaj Zalgo! .
Powinien również istnieć mechanizm anulowania obietnicy.
Możliwość anulowania zadania asynchronicznego często staje się wymaganiem o wysokim priorytecie w analizie wymagań. Jeśli nie, to na pewno zostanie złożone żądanie rozszerzenia przez użytkownika jakiś czas później po wydaniu aplikacji. Powód powinien być oczywisty: każde zadanie, które może zostać zatrzymane lub potrwać zbyt długo, powinno zostać anulowane przez użytkownika lub po przekroczeniu limitu czasu. Biblioteka godnych obietnic powinna wspierać anulowanie.
źródło
Zdaję sobie sprawę, że to stare pytanie, ale muszę na nie odpowiedzieć, ponieważ moja odpowiedź jest inna niż inne.
Dla tych, którzy mówią, że to kwestia osobistych preferencji, muszę się nie zgodzić. Jest dobry, logiczny powód, aby preferować jedno od drugiego ...
W przypadku zakończenia, twojemu blokowi przekazane są dwa obiekty, jeden reprezentuje sukces, a drugi porażkę ... Więc co robisz, jeśli oba są zerowe? Co robisz, jeśli oba mają wartość? Są to pytania, których można uniknąć w czasie kompilacji i jako takie powinny być. Unikasz tych pytań, mając dwa oddzielne bloki.
Posiadanie osobnych bloków sukcesu i niepowodzenia sprawia, że kod jest statystycznie weryfikowalny.
Pamiętaj, że wraz ze Swiftem sytuacja się zmienia. Możemy w nim zaimplementować pojęcie
Either
wyliczenia, aby zagwarantować, że pojedynczy blok uzupełniający zawiera obiekt lub błąd i musi mieć dokładnie jeden z nich. W przypadku Swift pojedynczy blok jest lepszy.źródło
Podejrzewam, że skończy się to osobistymi preferencjami ...
Ale wolę osobne bloki sukcesu / niepowodzenia. Lubię rozdzielać logikę sukcesu / niepowodzenia. Gdybyś zagnieździł sukces / porażkę, skończyłbyś z czymś, co byłoby bardziej czytelne (przynajmniej moim zdaniem).
Jako stosunkowo ekstremalny przykład takiego zagnieżdżenia, oto niektóre Ruby pokazujące ten wzór.
źródło
To wydaje się kompletnym wyrzutem, ale nie sądzę, że tutaj jest poprawna odpowiedź. Poszedłem z blokiem ukończenia po prostu dlatego, że obsługa błędów może nadal wymagać wykonania w przypadku powodzenia przy użyciu bloków powodzenia / niepowodzenia.
Myślę, że końcowy kod będzie wyglądał podobnie
lub po prostu
Nie najlepsza część kodu i zagnieżdżanie go pogarsza się
Myślę, że przez jakiś czas będę szaleć.
źródło