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 developers
Nie 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 Data
do tego użyć . Nie należy jednak zapominać, że Core Data
nie 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 Data
moż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 controller
rzeczy. Ale wierzę w skinny everything
podejś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 MVC
królestwie Service Layer
jest coś w rodzaju pośrednika między modelem domeny a kontrolerami. Istnieje dość podobna odmiana tego podejścia zwana MVCS, gdzie a Store
jest w rzeczywistości naszą Service
warstwą. Store
vending 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ę APIClient
klasę 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 NSError
obiektów ze szczegółowymi przyczynami niepowodzenia i opisami wszystkich API
błę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
,FriendsServices
i 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 RACSignal
z analizowanym modelem odpowiedzi lub NSError
wywoł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
, NSPredicate
i tak dalej do prostych metod, takich jak get
czy 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 ReactiveCocoa
podstawowych 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 APIClient
jest niejawnie przekazywany do wszystkich kontrolerów, można to jednoznacznie sparametryzować poprzez APIClient
klasy usług. Może to mieć sens, jeśli chcesz użyć różnych dostosowańAPIClient
dla 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, APIManagerWhatever
klasy 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. UserProfile
Potrzebuje kontrolera UserServices
itd.) .
Zawsze powinniśmy szanować S
zasadę 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 FriendsServices
klasie tworzę metodę o nazwie:
- (RACSignal *)removeFriend:(Friend * const)friend
gdzie Friend
jest obiekt modelu / domeny (lub może to być tylko User
obiekt, jeśli mają podobne atrybuty). Underhood tej metody parsowań Friend
do NSDictionary
parametrów JSON friend_id
, name
, surname
, friend_request_id
i 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
DELETE
metodę dokonania faktycznej żądania REST i powraca Response
w RACSignal
osobie dzwoniącej ( FriendsViewController
w 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ć Repository
lub modelować logikę z Service
jednym. Kiedy opisałem swoje podejście, powiedziałem, że removeFriend
metoda powinna znajdować się w Service
warstwie, 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 Repository
jest 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
, removeFriend
moż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 CRUD
operacje i łączy dwa obiekty domeny ( Friend
i Request
), dlatego należy ją umieścić w Service
warstwie. 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ą APIClient
warstwę (nasz koń roboczy, jak pamiętasz). Teraz nasz APIClient
dostawca 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 APIClient
dostawcę, jak w zwykłym podejściu Celu C. Do warstwy modelu zamiast Mantle
moż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 Swift
ję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 RACify
kod 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ć.