Quale architettura / design adottare quando si utilizza un'API RESTful? (App per iOS)

4

TL; DR

Parte 1)

Voglio refactoring la parte più grande dell'applicazione su cui lavoro - ed è praticamente spaghetti. È una singola classe che effettua le richieste al server, analizza il JSON e salva i dati nel database (ha anche conoscenza di alcune UI che devono essere modificate quando vengono soddisfatte specifiche regole aziendali e sicuramente non dovrebbe sapere tutto questo ). Ha un valore inferiore a 5000 LOC: (.

Un modo per rompere la classe in parti più piccole sarebbe separare le richieste dalla semantica (ad esempio richieste degli utenti, richieste di ricerca ecc.). Sarebbe utile o no?

Inoltre, penso che il codice di rete debba essere disaccoppiato dal codice di analisi e anche dal codice DB. Il problema è che non so davvero quale approccio (architettura / design pattern) dovrei usare per fare questo nel modo migliore. (ad esempio, la nostra libreria di analisi JSON è vecchia e non viene più mantenuta, volevamo cambiarla perché ha prodotto alcuni problemi e siamo rimasti bloccati a causa dell'accoppiamento stretto).

Parte 2)

Vorrei anche ricevere alcuni suggerimenti per il seguente problema: Usiamo il DB principalmente per la modalità offline e amp; caching. L'utente può vedere i dati in modalità offline, modificarli (e le richieste vengono serializzate) e quando torna in linea, le richieste verranno inviate al server. Lo usiamo anche come cache per mostrare qualcosa sull'interfaccia utente mentre facciamo una richiesta in background per vedere se ci sono dati nuovi / modificati (usiamo un meccanismo simile come ETag per verificare con il server).

Quindi questo è il flusso quando un utente entra in uno schermo:

enter_screen -> 
check_cache + background_request -> 
show_UI with cache data -> 
update_UI when the background request finishes and new/modified data is available.

Questa roba può essere sottratta in qualche modo? Stavo pensando ad una sorta di pattern delegato in cui ottengo una richiamata immediata quando colpisco la cache e poi ottengo una callback successiva al termine della richiesta (con flag facoltativi per distinguere tra dati cache e nuovi dati).

Un po 'di storia di sottofondo

Attualmente lavoro per un'app iOS che è aprox. 4 anni. Mi sono unito al team solo un anno fa. Secondo me il codice è brutto, pieno di hack e ha un'architettura molto traballante. Siamo 2 sviluppatori iOS che ci lavorano. Il cliente non ha esperienza tecnica e potrebbe non comprendere tutte le implicazioni di alcuni problemi che abbiamo. Fare in modo che un cliente capisca perché è necessario "refactoring" è un compito difficile. Faccio fatica a refactoring quanto posso, ma abbiamo ancora molto da fare.

L'app ha anche molte funzionalità che aumentano la complessità dell'app: deeplink, notifiche push, modalità offline, UI non bloccante, ecc. combinate con un modello di dati piuttosto ampio e peloso.

Poiché il client richiede funzionalità in modo costante, ritengo che il debito tecnico sia elevato.

Il mondo mobile sta cambiando molto velocemente e appaiono nuove tecnologie (classi di dimensioni, layout automatico, Swift, ecc.). Questo è vero per tutte le tecnologie, ma in qualche modo sento che la gara è un po 'più intensa nel mondo mobile. Attualmente sogno solo di usare Swift al lavoro (anche se gioco a casa nel mio tempo libero).

    
posta enum 02.03.2015 - 16:49
fonte

3 risposte

2

Come ha detto Frank, sembra che tu abbia già capito molto di tutto da solo, quindi ho solo un consiglio in merito alla domanda principale.

Esegui il refactoring gradualmente, in modo incrementale, insieme a qualsiasi modifica apportata per aggiungere valore aziendale. Non cercare di rompere il mostro 5000 LOC in cinque creature 1000 LOC tutto in una volta. Fintanto che tieni a mente tutti gli obiettivi a lungo termine che hai elencato, il codice di rete, l'analisi JSON, la gestione del database e tutto il resto passeranno costantemente in angoli separati autonomi del codice base.

    
risposta data 04.03.2015 - 21:26
fonte
2

Naturalmente, non ho mai visto il tuo codice reale, ma da quello che hai detto, proporrei qualcosa di simile alla seguente architettura. Mi dispiace per lo pseudo-codice, volevo solo che fosse veloce & chiaro;)

  1. Livello interfaccia utente:

    dataService = new DataService();
    
    dataService->makeDataRequest(requestName, requestParams[], cachedDataArrivedCallback, freshDataArrivedCallback);
    
    showWaitMessageToUser();
    
  2. Livello servizio (classe fittizia DataService):

    processor = RequestProcessorFactory.getProcessor(requestName);
    
    if(processor.cachedDataAvailable(requestParams)) {
        cachedDataCallback(processor.getCachedData());
    }
    
    freshData = processor.getFreshDataAsync(requestParams, freshDataArrivedCallback);
    
  3. RequestProcessor :: getFreshDataAsync:

    networkTransport = NetworkRequestFactory.getTransport(requestName);
    networkTransport.send(requestParams[]);
    rawResult = networkTransport.result();
    
    deserializer = DeserializerFactory.getDeserializer(requestName); //JSON deserializers
    result = deserializer.deserialize(rawResult);
    
    storageHandler = StorageHandlerFactory.getStorageHandler(requestName); //Database layer
    storageHandler.storeData(requestName, result);
    
    cacheHandler = CacheHandlerFactory.getCacheHandler(requestName);
    cacheHandler.store(requestParams, result);
    
    freshDataArrivedCallback(result);
    

Non mi piacciono i diagrammi, ma penso che questo dovrebbe essere abbastanza chiaro.

L'interfaccia utente chiama il tuo livello di servizio con una richiesta di fornire dati. Passa lì i callback da eseguire quando i dati saranno disponibili. E poi mostra il messaggio all'utente e attende le interazioni dell'utente.

Il livello di servizio ottiene dapprima RequestProcessor, che è responsabile della gestione di questa particolare richiesta (richieste degli utenti, richieste di ricerca, ecc.). Controlla con il processore se ha dati memorizzati nella cache per questo particolare parametro di richiesta. Se è così, lo ottiene e attiva uno dei callback dell'interfaccia utente. Quindi chiede a requestProcessor di cercare nuovi dati.

Richiedi il processore chiama il livello di rete per inoltrare la richiesta al server. Deserializza la risposta usando un deserializzatore, che al momento è la tua vecchia e brutta libreria JSON, ma sarai in grado di passare a un'altra piuttosto facile con quello che propongo.

I risultati deserializzati vanno al gestore del database e al gestore della cache (ma potrebbero essere, sono uguali nella tua app, non sono sicuro). Bene, i risultati puliti possono andare a qualsiasi gestore che desideri ora;)

Bene, all'ultima richiesta il processore chiama freshDataArrivedCallback per annunciare che sono disponibili nuovi dati per l'interfaccia utente.

Questo è solo un esempio di come questo può essere diviso. Potrebbero esserci vari miglioramenti allo schema. Ad esempio, è meglio non passare freshDataArrivedCallback in giro, tenerlo nel DataService e fare in modo che DataService esponga il proprio callback per ridurre l'accoppiamento.

Questo esempio usa le fabbriche ogni tanto, ma puoi saltarle se hai un solo gestore per evitare complicazioni inutili.

Ho solo cercato di rendere il mio pensiero breve di 5 minuti per darti un'idea;) Spero che tu trovi utile questo.

È stata una domanda interessante a cui rispondere e se questa risposta mi aiuta, mi immergerò volentieri più in profondità;) A proposito, non ho mai provato a fare lo sviluppo iOS;))

    
risposta data 05.03.2015 - 11:06
fonte
2

Posso già vedere alcune risposte sensate a questa domanda, ma ho pensato che avrei ampliato e offerto la mia opinione.

Hai descritto un sistema che sembra accoppiare la logica in classi grandi e che deve essere strettamente collegato a molte funzioni diverse. Il primo modo per provare e rifattorizzare questo è assicurarsi in primo luogo di capire che aspetto avrebbe una 'buona' architettura / base di codice.

Spero che tu abbia familiarità con i principi di sviluppo di SOLID, ma se non lo farai, applicherei davvero qui. Prenderò un paio di principi che alla fine si applicano a te, ma dovresti assicurarti di comprenderli tutti.

Principio di responsabilità singola (SRP - o S in SOLID) consiglia di assicurare che crei classi che gestiscono solo una cosa e una sola cosa. Usando questo principio, dovresti vedere il codice che si concentra su singoli compiti e ignora qualsiasi cosa al di fuori del loro scopo. Ad esempio, il tuo codice di rete non dovrebbe avere nulla a che fare con l'analisi di JSON, poiché questa è una responsabilità completamente separata.

Inversione di dipendenza (DI - della D in SOLID) afferma che le classi dovrebbero "dipendere dalle astrazioni. Non dipendere dalle concrezioni '. Ciò va di pari passo con l'ISP, in quanto una classe consumante dovrebbe utilizzare una dipendenza su un'interfaccia, lasciando un contenitore IOC per iniettare la classe concreta appropriata come richiesto. Ciò rimuove le dipendenze dal codice e aiuta a far rispettare SRP, oltre a rendere il test molto più semplice.

Speriamo che questo ti dia un'idea di cosa sia una buona qualità del codice. Se SOLID viene seguito nel codebase, dovresti vedere le classi che codificano rispetto alle interfacce, con le dipendenze inserite. Le classi dovrebbero essere focalizzate singolarmente sul loro dominio e non svolgere altre attività.

Quindi, per quanto riguarda l'architettura, è possibile iniziare a separare la logica in livelli distinti, con la logica tra i livelli gestiti tramite chiamate di servizio. Un approccio molto comune a questo è l'architettura Model-View-Controller, che separa l'interfaccia utente in una vista, i dati in un modello e la logica di cablaggio in un controller. Questo è in genere supportato con un livello di "servizio" inferiore, che fornisce funzionalità specializzate, come l'analisi di JSON, l'accesso ai dati, ecc. Il livello di servizio dovrebbe essere separato da quello precedente ed esporre un'interfaccia, più comunemente attraverso un'API RESTful. p>

Con quanto sopra riportato, il tuo esempio di sostituzione del parser JSON ora richiede semplicemente di scrivere una nuova classe concreta che implementa l'interfaccia IJsonParser (o qualsiasi altra cosa possa essere) ed è cablata tramite il tuo IOC (Dependency Injection) contenitore. Finché la nuova classe implementa tutta la logica nell'interfaccia, allora dovrebbe essere l'unica modifica richiesta.

Penso che la seconda domanda meriti una domanda separata su questo thread, in quanto potrebbe portare a qualche confusione in questa risposta, e sicuramente ha molte opzioni.

    
risposta data 05.03.2015 - 16:51
fonte

Leggi altre domande sui tag