I want to understand basic, abstract and correct architectural approach for networking applications in iOS: nie ma „najlepszego” lub „najbardziej poprawnego” podejścia do budowania architektury aplikacji. To bardzo kreatywna praca. Zawsze powinieneś wybierać najbardziej prostą i rozszerzalną architekturę, która będzie zrozumiała dla każdego programisty, który zacznie pracować nad twoim projektem lub dla innych programistów w zespole, ale zgadzam się, że może być „dobra” i „zła” „architektura.
Powiedziałeś: „ collect the most interesting approaches from experienced iOS developersNie sądzę, że moje podejście jest najbardziej interesujące lub poprawne, ale wykorzystałem je w kilku projektach i jestem z niego zadowolony. Jest to hybrydowe podejście, o którym wspomniałeś powyżej, a także ulepszenia z moich własnych badań. Interesują mnie problemy budowania podejść, które łączą kilka dobrze znanych wzorców i idiomów. Myślę, że wiele wzorców korporacyjnych Fowlera można z powodzeniem zastosować w aplikacjach mobilnych. Oto lista najciekawszych, które możemy złożyć wniosku o utworzenie architektury aplikacji iOS ( moim zdaniem ): Warstwa usługi , Jednostka pracy , Zdalna fasada , obiektu przesyłania danych ,Brama , typ warstwy , przypadek specjalny , model domeny . Zawsze należy poprawnie zaprojektować warstwę modelu i zawsze nie zapominać o trwałości (może to znacznie zwiększyć wydajność aplikacji). Możesz Core Datado tego użyć . Nie należy jednak zapominać, że Core Datanie jest to ORM ani baza danych, ale menedżer grafów obiektowych z trwałością jako dobrą opcją. Tak więc bardzo często Core Datamoże być zbyt ciężki dla twoich potrzeb i możesz spojrzeć na nowe rozwiązania, takie jak Realm i Couchbase Lite , lub zbudować własną lekką warstwę mapowania / trwałości obiektów, opartą na surowym SQLite lub LevelDB. Radzę również zapoznać się z projektami opartymi na domenach i CQRS .
Na początku myślę, że powinniśmy stworzyć kolejną warstwę dla sieci, ponieważ nie chcemy kontrolerów tłuszczu ani ciężkich, przytłoczonych modeli. Nie wierzę w te fat model, skinny controllerrzeczy. Ale wierzę w skinny everythingpodejście, ponieważ żadna klasa nie powinna być gruba, nigdy. Wszystkie sieci można generalnie wyodrębnić jako logikę biznesową, w związku z czym powinniśmy mieć kolejną warstwę, w której możemy to umieścić. Warstwa serwisowa jest tym, czego potrzebujemy:
It encapsulates the application's business logic, controlling transactions
and coordinating responses in the implementation of its operations.
W naszym MVCkrólestwie Service Layerjest coś w rodzaju pośrednika między modelem domeny a kontrolerami. Istnieje dość podobna odmiana tego podejścia zwana MVCS, gdzie a Storejest w rzeczywistości naszą Servicewarstwą. Storevending instancje modelu i obsługuje tworzenie sieci, buforowanie itp. Chcę wspomnieć, że nie powinieneś pisać całej sieci i logiki biznesowej w warstwie usług. Można to również uznać za zły projekt. Aby uzyskać więcej informacji, zobacz modele domen Anemic i Rich . Niektóre metody serwisowe i logika biznesowa mogą być obsługiwane w modelu, więc będzie to model „bogaty” (z zachowaniem).
Zawsze intensywnie korzystam z dwóch bibliotek: AFNetworking 2.0 i ReactiveCocoa . Myślę, że jest to niezbędne dla każdej nowoczesnej aplikacji, która współdziała z siecią i usługami internetowymi lub zawiera złożoną logikę interfejsu użytkownika.
ARCHITEKTURA
Najpierw tworzę APIClientklasę ogólną , która jest podklasą AFHTTPSessionManager . Jest to koń roboczy całej sieci w aplikacji: wszystkie klasy usług delegują do niego rzeczywiste żądania REST. Zawiera wszystkie dostosowania klienta HTTP, których potrzebuję w konkretnej aplikacji: przypinanie SSL, przetwarzanie błędów i tworzenie prostych NSErrorobiektów ze szczegółowymi przyczynami niepowodzenia i opisami wszystkich APIbłędów oraz błędów połączenia (w takim przypadku kontroler będzie mógł wyświetlać poprawne komunikaty dla użytkownika), ustawianie serializatorów żądań i odpowiedzi, nagłówków http i innych rzeczy związanych z siecią. Potem logicznie podzielić wszystkie wnioski API język subservices lub, bardziej poprawnie, microservices : UserSerivces, CommonServices, SecurityServices,FriendsServicesi tak dalej, zgodnie z logiką biznesową, którą wdrażają. Każda z tych mikrousług jest odrębną klasą. Razem tworzą Service Layer. Klasy te zawierają metody dla każdego żądania API, przetwarzają modele domen i zawsze zwracają a RACSignalz analizowanym modelem odpowiedzi lub NSErrorwywołującym.
Chcę wspomnieć, że jeśli masz złożoną logikę serializacji modelu - utwórz dla niej kolejną warstwę: coś takiego jak Data Mapper, ale bardziej ogólnie, np. JSON / XML -> Model mapper. Jeśli masz pamięć podręczną: utwórz ją również jako osobną warstwę / usługę (nie należy mieszać logiki biznesowej z buforowaniem). Dlaczego? Ponieważ poprawna warstwa buforująca może być dość złożona z własnymi problemami. Ludzie stosują złożoną logikę, aby uzyskać prawidłowe, przewidywalne buforowanie, takie jak np. Buforowanie monoidalne z projekcjami opartymi na profpraktorach. Możesz przeczytać o tej pięknej bibliotece o nazwie Carlos, aby dowiedzieć się więcej. I nie zapominaj, że Core Data może naprawdę pomóc ci we wszystkich problemach z buforowaniem i pozwoli ci napisać mniej logiki. Ponadto, jeśli masz trochę logiki między repozytoriumNSManagedObjectContext modelami żądań serwera a serwerami, możesz użyćWzorzec , który oddziela logikę, która pobiera dane i mapuje je do modelu encji od logiki biznesowej, która działa na model. Dlatego radzę używać wzorca repozytorium, nawet jeśli masz architekturę opartą na podstawowych danych. Repozytorium może abstrakcyjne rzeczy, jak NSFetchRequest, NSEntityDescription, NSPredicatei tak dalej do prostych metod, takich jak getczy put.
Po wszystkich tych czynnościach w warstwie usług osoba wywołująca (kontroler widoku) może wykonać złożone złożone operacje asynchroniczne z odpowiedzią: manipulowanie sygnałem, tworzenie łańcuchów, mapowanie itp. Za pomocą operacji ReactiveCocoapodstawowych lub po prostu subskrybować i wyświetlać wyniki w widoku . I wstrzyknąć z wstrzykiwania zależności We wszystkich tych klas usług moi APIClient, co przełoży konkretnego zgłoszenia serwisowego w odpowiednie GET, POST, PUT, DELETE, itd żądania do punktu końcowego REST. W takim przypadku APIClientjest niejawnie przekazywany do wszystkich kontrolerów, można to jednoznacznie sparametryzować poprzez APIClientklasy usług. Może to mieć sens, jeśli chcesz użyć różnych dostosowańAPIClientdla określonych klas usług, ale jeśli z jakichś powodów nie chcesz dodatkowych kopii lub masz pewność, że zawsze użyjesz jednej konkretnej instancji (bez dostosowań) APIClient- zrób to singleton, ale NIE, proszę NIE „Czyń klasy usług jako singletony.
Następnie każdy kontroler widoku ponownie z DI wstrzykuje potrzebną klasę usługi, wywołuje odpowiednie metody usługi i zestawia ich wyniki z logiką interfejsu użytkownika. Do wstrzykiwania zależności lubię używać BloodMagic lub bardziej zaawansowanego frameworka Typhoon . Nigdy nie używam singletonów, APIManagerWhateverklasy Bożej ani innych niewłaściwych rzeczy. Ponieważ jeśli zadzwonisz do swojej klasy WhateverManager, oznacza to, że nie znasz jej celu i jest to zły wybór projektu . Singletony to również anty-wzór, aw większości przypadków (z wyjątkiem rzadkich) jest złym rozwiązaniem. Singleton należy rozważyć tylko wtedy, gdy wszystkie trzy z następujących kryteriów są spełnione:
- Własności pojedynczej instancji nie można racjonalnie przypisać;
- Pożądana jest leniwa inicjalizacja;
- Globalny dostęp nie jest inaczej przewidziany.
W naszym przypadku własność pojedynczej instancji nie stanowi problemu, a także nie potrzebujemy globalnego dostępu po podzieleniu naszego boga managera na usługi, ponieważ teraz tylko jeden lub kilka dedykowanych kontrolerów potrzebuje konkretnej usługi (np. UserProfilePotrzebuje kontrolera UserServicesitd.) .
Zawsze powinniśmy szanować Szasadę w SOLID i stosować separację problemów , więc nie umieszczaj wszystkich metod obsługi i połączeń sieciowych w jednej klasie, ponieważ to szalone, szczególnie jeśli tworzysz dużą aplikację dla przedsiębiorstw. Dlatego powinniśmy rozważyć zastrzyk zależności i podejście do usług. Uważam to podejście za nowoczesne i post-OO . W tym przypadku podzieliliśmy naszą aplikację na dwie części: logikę sterowania (sterowniki i zdarzenia) oraz parametry.
Jednym rodzajem parametrów byłyby zwykłe parametry „danych”. To właśnie przekazujemy funkcjom, manipulujemy, modyfikujemy, utrzymujemy itp. Są to byty, agregaty, kolekcje, klasy spraw. Drugi rodzaj to parametry „serwisowe”. Są to klasy, które zawierają logikę biznesową, umożliwiają komunikację z systemami zewnętrznymi, zapewniają dostęp do danych.
Oto ogólny przepływ pracy mojej architektury według przykładów. Załóżmy, że mamy FriendsViewController, która wyświetla listę znajomych użytkownika i mamy opcję usunięcia z listy znajomych. W mojej FriendsServicesklasie tworzę metodę o nazwie:
- (RACSignal *)removeFriend:(Friend * const)friend
gdzie Friendjest obiekt modelu / domeny (lub może to być tylko Userobiekt, jeśli mają podobne atrybuty). Underhood tej metody parsowań Frienddo NSDictionaryparametrów JSON friend_id, name, surname, friend_request_idi tak dalej. Zawsze używam biblioteki Mantle dla tego rodzaju szablonów i dla mojej warstwy modelu (parsowanie w przód iw tył, zarządzanie hierarchiami obiektów zagnieżdżonych w JSON i tak dalej). Po parsowania wywołuje APIClient DELETEmetodę dokonania faktycznej żądania REST i powraca Responsew RACSignalosobie dzwoniącej ( FriendsViewControllerw naszym przypadku), aby wyświetlić odpowiedni komunikat dla użytkownika lub cokolwiek.
Jeśli nasza aplikacja jest bardzo duża, musimy rozdzielić naszą logikę jeszcze wyraźniej. Np. Nie zawsze dobrze jest łączyć Repositorylub modelować logikę z Servicejednym. Kiedy opisałem swoje podejście, powiedziałem, że removeFriendmetoda powinna znajdować się w Servicewarstwie, ale jeśli będziemy bardziej pedantyczni, zauważymy, że lepiej do niej należy Repository. Pamiętajmy, czym jest repozytorium. Eric Evans podał dokładny opis w swojej książce [DDD]:
Repozytorium reprezentuje wszystkie obiekty określonego typu jako zbiór pojęciowy. Działa jak kolekcja, z wyjątkiem bardziej skomplikowanych funkcji zapytań.
A zatem Repositoryjest zasadniczo fasadą, która wykorzystuje semantykę stylu Kolekcji (Dodaj, Aktualizuj, Usuń) w celu zapewnienia dostępu do danych / obiektów. Dlatego, gdy masz coś takiego: getFriendsList, getUserGroups, removeFriendmożna umieścić go w Repository, ponieważ kolekcja podobny semantyki jest całkiem jasne tutaj. I kod jak:
- (RACSignal *)approveFriendRequest:(FriendRequest * const)request;
jest zdecydowanie logiką biznesową, ponieważ wykracza poza podstawowe CRUDoperacje i łączy dwa obiekty domeny ( Friendi Request), dlatego należy ją umieścić w Servicewarstwie. Chcę też zauważyć: nie twórz niepotrzebnych abstrakcji . Mądrze stosuj wszystkie te podejścia. Ponieważ jeśli przytłoczysz swoją aplikację abstrakcjami, zwiększy to jej przypadkową złożoność, a złożoność powoduje więcej problemów w systemach oprogramowania niż cokolwiek innego
Opisuję wam „stary” przykład Celu C, ale to podejście można bardzo łatwo dostosować do języka Swift z dużo większą liczbą ulepszeń, ponieważ ma ono więcej przydatnych funkcji i funkcjonalny cukier. Bardzo polecam skorzystać z tej biblioteki: Moya . Pozwala stworzyć bardziej elegancką APIClientwarstwę (nasz koń roboczy, jak pamiętasz). Teraz nasz APIClientdostawca będzie typem wartości (wyliczeniem) z rozszerzeniami zgodnymi z protokołami i wykorzystującymi dopasowanie wzorca destrukcji. Szybkie wyliczanie + dopasowanie wzorców pozwala nam tworzyć algebraiczne typy danych, jak w klasycznym programowaniu funkcjonalnym. Nasze mikrousługi wykorzystają tego ulepszonego APIClientdostawcę, jak w zwykłym podejściu Celu C. Do warstwy modelu zamiast Mantlemożesz użyć biblioteki ObjectMapperlub lubię używać bardziej eleganckiej i funkcjonalnej biblioteki Argo .
Opisałem więc moje ogólne podejście architektoniczne, które, jak sądzę, można dostosować do każdego zastosowania. Oczywiście można wprowadzić znacznie więcej ulepszeń. Radzę nauczyć się programowania funkcjonalnego, ponieważ możesz z niego wiele skorzystać, ale nie posuwaj się za daleko. Wyeliminowanie nadmiernego, wspólnego, globalnego stanu mutable, stworzenie niezmiennego modelu domeny lub stworzenie czystych funkcji bez zewnętrznych skutków ubocznych jest ogólnie dobrą praktyką, a nowy Swiftjęzyk zachęca do tego. Ale zawsze pamiętaj, że przeciążanie kodu ciężkimi czystymi wzorcami funkcjonalnymi, podejście teoretyczne do kategorii jest złym pomysłem, ponieważ inne programiści będą czytać i wspierać Twój kod, i mogą być sfrustrowani lub przerażeniprismatic profunctors i tego rodzaju rzeczy w niezmiennym modelu. To samo dotyczy ReactiveCocoa: nie RACifykod za bardzo , ponieważ może stać się bardzo nieczytelny, szczególnie dla początkujących. Użyj go, gdy naprawdę może uprościć twoje cele i logikę.
Więc read a lot, mix, experiment, and try to pick up the best from different architectural approaches. To najlepsza rada, jaką mogę ci dać.