I want to understand basic, abstract and correct architectural approach for networking applications in iOS
: non esiste un approccio "il migliore" o "il più corretto" per la creazione di un'architettura applicativa. È un lavoro molto creativo. Dovresti sempre scegliere l'architettura più semplice ed estensibile, che sarà chiara per qualsiasi sviluppatore, che inizi a lavorare sul tuo progetto o per altri sviluppatori nel tuo team, ma sono d'accordo che ci può essere un "buono" e un "cattivo" " architettura.
Hai detto: collect the most interesting approaches from experienced iOS developers
non penso che il mio approccio sia il più interessante o corretto, ma l'ho usato in diversi progetti e ne sono soddisfatto. È un approccio ibrido di quelli che hai menzionato sopra, e anche con miglioramenti dai miei sforzi di ricerca. Sono interessante per i problemi di costruzione di approcci, che combinano diversi schemi e modi di dire noti. Penso che molti dei modelli aziendali di Fowler possano essere applicati con successo alle applicazioni mobili. Ecco un elenco di quelli più interessanti, che possiamo applicare per creare un'architettura di applicazione iOS ( a mio avviso ): Livello di servizio , Unità di lavoro , Facciata remota , Oggetto trasferimento dati ,Gateway , supertipo di livello , caso speciale , modello di dominio . Dovresti sempre progettare correttamente un livello di modello e non dimenticare sempre la persistenza (può aumentare significativamente le prestazioni della tua app). Puoi usarlo Core Data
per questo. Ma non dovresti dimenticare, che Core Data
non è un ORM o un database, ma un gestore di grafici a oggetti con persistenza come una buona opzione di esso. Quindi, molto spesso Core Data
può essere troppo pesante per le tue esigenze e puoi guardare a nuove soluzioni come Realm e Couchbase Lite o creare il tuo livello di mappatura / persistenza degli oggetti leggeri, basato su SQLite grezzo o LevelDB. Inoltre ti consiglio di familiarizzare con Domain Driven Design e CQRS .
All'inizio, penso, dovremmo creare un altro livello per il networking, perché non vogliamo controller di grasso o modelli pesanti e sopraffatti. Non credo in quelle fat model, skinny controller
cose. Ma io credo nel skinny everything
metodo, perché nessuna classe deve essere grasso, mai. Tutte le reti possono essere generalmente astratte come logica aziendale, di conseguenza dovremmo avere un altro livello, dove possiamo metterlo. Il livello di servizio è ciò di cui abbiamo bisogno:
It encapsulates the application's business logic, controlling transactions
and coordinating responses in the implementation of its operations.
Nel nostro MVC
regno Service Layer
c'è qualcosa come un mediatore tra modello di dominio e controller. C'è una variazione piuttosto simile di questo approccio chiamato MVCS in cui a Store
è in realtà il nostro Service
livello. Store
modello di istanze vends e gestisce la rete, la cache ecc. Voglio menzionare che non dovresti scrivere tutta la tua rete e la logica di business nel tuo livello di servizio. Anche questo può essere considerato come un cattivo design. Per ulteriori informazioni, guarda i modelli di dominio Anemic e Rich . Alcuni metodi di servizio e la logica di business possono essere gestiti nel modello, quindi sarà un modello "ricco" (con comportamento).
Uso sempre ampiamente due librerie: AFNetworking 2.0 e ReactiveCocoa . Penso che sia un must per qualsiasi applicazione moderna che interagisce con la rete e i servizi Web o contiene una complessa logica dell'interfaccia utente.
ARCHITETTURA
All'inizio creo una APIClient
classe generale , che è una sottoclasse di AFHTTPSessionManager . Questo è un cavallo di battaglia di tutte le reti nell'applicazione: tutte le classi di servizio delegano ad esso richieste REST effettive. Contiene tutte le personalizzazioni del client HTTP, di cui ho bisogno nella specifica applicazione: pinning SSL, elaborazione degli errori e creazione di NSError
oggetti semplici con motivi dettagliati di errore e descrizioni di tutti API
e errori di connessione (in tal caso il controller sarà in grado di mostrare i messaggi corretti per l'utente), l'impostazione di serializzatori di richieste e risposte, intestazioni http e altri elementi relativi alla rete. Poi ho logicamente dividere tutti gli richieste API in sottoservizi o, più correttamente, microservices : UserSerivces
, CommonServices
, SecurityServices
,FriendsServices
e così via, secondo la logica aziendale che implementano. Ognuno di questi microservizi è una classe separata. Insieme formano un Service Layer
. Queste classi contengono metodi per ogni richiesta API, elaborano modelli di dominio e restituiscono sempre un RACSignal
modello di risposta analizzato o NSError
al chiamante.
Voglio menzionare che se si dispone di una logica di serializzazione del modello complessa, quindi creare un altro livello per esso: qualcosa come Data Mapper ma più generale, ad es. JSON / XML -> Model maper. Se si dispone di cache: crearla anche come livello / servizio separato (non è necessario combinare la logica di business con la cache). Perché? Perché il corretto livello di memorizzazione nella cache può essere piuttosto complesso con i propri gotcha. Le persone implementano una logica complessa per ottenere una cache valida e prevedibile come, ad esempio, la memorizzazione nella cache monoideale con proiezioni basate su profunctor. Puoi leggere di questa bellissima biblioteca chiamata Carlos per capire di più. E non dimenticare che i Core Data possono davvero aiutarti con tutti i problemi di cache e ti permetteranno di scrivere meno logica. Inoltre, se si dispone di una logica tra NSManagedObjectContext
i modelli di richieste del server e, è possibile utilizzareModello di repository , che separa la logica che recupera i dati e li mappa al modello di entità dalla logica aziendale che agisce sul modello. Pertanto, consiglio di utilizzare il modello di repository anche quando si dispone di un'architettura basata su dati di base. Repository può cose astratte, come NSFetchRequest
, NSEntityDescription
, NSPredicate
e così via per i metodi semplici come get
o put
.
Dopo tutte queste azioni nel livello di servizio, il chiamante (controller di visualizzazione) può eseguire alcune complesse operazioni asincrone con la risposta: manipolazioni del segnale, concatenamento, mappatura, ecc. Con l'aiuto di ReactiveCocoa
primitive o semplicemente iscriversi e mostrare i risultati nella vista . Inietto con il Dependency Injection in tutte queste classi di servizio le mie APIClient
, che si tradurrà una particolare chiamata di servizio in corrispondenti GET
, POST
, PUT
, DELETE
, ecc richiesta al endpoint REST. In questo caso APIClient
viene passato in modo implicito a tutti i controller, è possibile renderlo esplicito con un parametro su APIClient
classi di servizio. Questo può avere senso se si desidera utilizzare diverse personalizzazioni diAPIClient
per particolari classi di servizio, ma se per qualche motivo non vuoi copie extra o sei sicuro di usare sempre un'istanza particolare (senza personalizzazioni) di APIClient
- rendila un singleton, ma NON, per favore DON Preparo lezioni di servizio come singoli.
Quindi ogni controller di visualizzazione con il DI inietta la classe di servizio di cui ha bisogno, chiama i metodi di servizio appropriati e compone i risultati con la logica dell'interfaccia utente. Per l'iniezione di dipendenza mi piace usare BloodMagic o un Typhoon framework più potente . Non uso mai singoli, APIManagerWhatever
classe di Dio o altre cose sbagliate. Perché se chiami la tua classe WhateverManager
, questo indica che non conosci il suo scopo ed è una cattiva scelta nel design . Singletons è anche un anti-pattern, e nella maggior parte dei casi (tranne quelli rari) è una soluzione sbagliata . Singleton dovrebbe essere considerato solo se tutti e tre i seguenti criteri sono soddisfatti:
- La proprietà della singola istanza non può essere ragionevolmente assegnata;
- L'inizializzazione lenta è desiderabile;
- L'accesso globale non è altrimenti previsto.
Nel nostro caso la proprietà della singola istanza non è un problema e inoltre non abbiamo bisogno dell'accesso globale dopo aver diviso il nostro god manager in servizi, perché ora solo uno o più controller dedicati hanno bisogno di un servizio particolare (ad es. UserProfile
Esigenze del controller UserServices
e così via) .
Dobbiamo sempre rispettare i S
principi di SOLID e utilizzare la separazione delle preoccupazioni , quindi non mettere tutti i metodi di servizio e le chiamate di rete in una classe, perché è pazzesco, soprattutto se si sviluppa un'applicazione aziendale di grandi dimensioni. Ecco perché dovremmo considerare l'iniezione di dipendenza e l'approccio ai servizi. Considero questo approccio moderno e post-OO . In questo caso abbiamo diviso la nostra applicazione in due parti: logica di controllo (controller ed eventi) e parametri.
Un tipo di parametri sarebbero normali parametri di "dati". Questo è ciò che passiamo intorno a funzioni, manipolazione, modifica, persistenza, ecc. Queste sono entità, aggregati, raccolte, classi di casi. L'altro tipo sarebbe parametri di "servizio". Si tratta di classi che incapsulano la logica aziendale, consentono la comunicazione con sistemi esterni, forniscono l'accesso ai dati.
Ecco un flusso di lavoro generale della mia architettura per esempio. Supponiamo di avere un FriendsViewController
, che mostra l'elenco degli amici dell'utente e abbiamo un'opzione per rimuoverlo dagli amici. Creo un metodo nella mia FriendsServices
classe chiamato:
- (RACSignal *)removeFriend:(Friend * const)friend
dove si Friend
trova un oggetto modello / dominio (o può essere solo un User
oggetto se hanno attributi simili). Underhood questo metodo analizza Friend
a NSDictionary
parametri JSON friend_id
, name
, surname
, friend_request_id
e così via. Uso sempre la libreria Mantle per questo tipo di boilerplate e per il mio livello di modello (analizzando avanti e indietro, gestendo gerarchie di oggetti nidificati in JSON e così via). Dopo l'analisi si chiama APIClient
DELETE
metodo per fare una richiesta di un riposo effettivo e ritorna Response
a RACSignal
al chiamante ( FriendsViewController
nel nostro caso) per visualizzare un messaggio appropriato per l'utente o qualsiasi altra cosa.
Se la nostra applicazione è molto grande, dobbiamo separare la nostra logica ancora più chiara. Ad esempio, non è sempre buono mescolare Repository
o modellare la logica con Service
una. Quando ho descritto il mio approccio, avevo detto che il removeFriend
metodo dovrebbe essere nel Service
livello, ma se saremo più pedanti possiamo notare che appartiene meglio Repository
. Ricordiamo cos'è il repository. Eric Evans ha dato una descrizione precisa nel suo libro [DDD]:
Un repository rappresenta tutti gli oggetti di un certo tipo come un insieme concettuale. Funziona come una raccolta, tranne con una capacità di query più elaborata.
Quindi, a Repository
è essenzialmente una facciata che utilizza la semantica in stile Collection (Aggiungi, Aggiorna, Rimuovi) per fornire accesso a dati / oggetti. Ecco perché quando si ha qualcosa come: getFriendsList
, getUserGroups
, removeFriend
è possibile inserirlo in Repository
, poiché la raccolta-come la semantica è abbastanza chiaro qui. E codice come:
- (RACSignal *)approveFriendRequest:(FriendRequest * const)request;
è sicuramente una logica aziendale, perché va oltre le CRUD
operazioni di base e collega due oggetti di dominio ( Friend
e Request
), ecco perché dovrebbe essere collocato nel Service
livello. Inoltre voglio notare: non creare astrazioni inutili . Usa saggiamente tutti questi approcci. Perché se travolgerai la tua applicazione con astrazioni, ciò aumenterà la sua complessità accidentale e la complessità causerà più problemi nei sistemi software di ogni altra cosa
Ti descrivo un "vecchio" esempio di Objective-C, ma questo approccio può essere adattato molto facilmente al linguaggio Swift con molti più miglioramenti, perché ha caratteristiche più utili e funzioni funzionali. Consiglio vivamente di usare questa libreria: Moya . Ti consente di creare un livello più elegante APIClient
(il nostro cavallo di battaglia come ricordi). Ora il nostro APIClient
fornitore sarà un tipo di valore (enum) con estensioni conformi ai protocolli e sfruttando la corrispondenza del modello destrutturante. Swift enums + pattern matching ci consente di creare tipi di dati algebrici come nella classica programmazione funzionale. I nostri microservizi useranno questo APIClient
provider migliorato come nel solito approccio Objective-C. Per il layer modello invece di Mantle
te puoi usare la libreria ObjectMappero mi piace usare la libreria Argo più elegante e funzionale .
Quindi, ho descritto il mio approccio architettonico generale, che può essere adattato per qualsiasi applicazione, credo. Ci possono essere molti più miglioramenti, ovviamente. Ti consiglio di imparare la programmazione funzionale, perché puoi trarne un grande vantaggio, ma non esagerare troppo. L'eliminazione di uno stato mutabile eccessivo, condiviso e globale, la creazione di un modello di dominio immutabile o la creazione di funzioni pure senza effetti collaterali esterni è generalmente una buona pratica e il nuovo Swift
linguaggio lo incoraggia. Ma ricorda sempre che sovraccaricare il tuo codice con pesanti schemi funzionali puri, approcci teorici di categoria è una cattiva idea, perché altri sviluppatori leggeranno e supporteranno il tuo codice e possono essere frustrati o spaventosi delprismatic profunctors
e questo genere di cose nel tuo modello immutabile. La stessa cosa con ReactiveCocoa
: non RACify
usare troppo il tuo codice , perché può diventare illeggibile molto velocemente, soprattutto per i neofiti. Usalo quando può davvero semplificare i tuoi obiettivi e la tua logica.
Quindi read a lot, mix, experiment, and try to pick up the best from different architectural approaches
. È il miglior consiglio che posso darti.