Atlas: un approccio unificato al ciclo di sviluppo mobile in IQUII

Atlas: un approccio unificato al ciclo di sviluppo mobile in IQUII

Pubblicato da Daniele Margutti il 29/11/2017 in Thinking

Come sviluppatori passiamo una buona parte del nostro tempo scrivendo, riscrivendo o facendo refactoring di codice già esistente; per qualcuno di noi si tratta di un lungo percorso di crescita professionale che ha come obiettivo un po’ utopico quello di ottenere il vero clean code.

Altri invece non hanno ancora colto uno dei principi cardine dell’ingegneria del software: evitare quanto più possibile di reinventare la ruota, promuovendo il riutilizzo del codice, sopratutto quando si ha a che fare con più prodotti.

Far crescere i propri prodotti su solide basi non solo è un buon modo per evitare rework in corsa, ma consente un ciclo di sviluppo per gli aggiornamenti più agile e meno complesso.

Con un ciclo più snello gli aggiornamenti sono più veloci, possono comprendere più funzioni e hanno un impatto sui tempi di sviluppo più limitato (un bel vantaggio sia per chi sviluppa sia per chi ha a che fare con i tempi).

Cos’è Atlas

La stragrande maggioranza delle applicazioni odierne comunicano in qualche modo con dei server ricevendo o inviando dati per poi presentarli all’utente.
Pur trattandosi solo di un tassello all’interno di ogni progetto è chiaro come esso sia però di fondamentale importanza; è proprio per questo motivo che il nostro progetto di standardizzazione dei processi di sviluppo mobile in IQUII è iniziato da questa componente.

Il progetto Atlas nasce con l’ambizioso obiettivo di creare un layer intermedio tra quello che è il codice applicativo, il sistema operativo e le varie SDK sottostanti; questo include un layer di rete, uno di persistenza, una metodologia comune per l’architettura, una gestione semplificata per le push, le impostazioni e tutto quello che solitamente è parte di una app mobile.

I benefit dietro un progetto del genere sono molteplici:

  • Concentrarsi sul prodotto. Con delle solide basi su cui basare i progetti possiamo concentrarsi su quello che davvero conta: l’esperienza utente e le funzionalità, in una parola il prodotto;
  • Stabilità. Con una codebase unificata tra i progetti (e cicli di Unit Testing) il codice risulta più testato, gli aggiornamenti e fix risolvono bug e aggiungono funzionalità che vengono riportate automaticamente su tutti i programmi che ne fanno uso.
  • Standardizzare il ciclo di sviluppo. Con un team allineato su una metodologia comune, lo sviluppo risulta più snello e la capacità di switch tra progetti – tipica in ambienti di consulenza – non è più motivo di frustrazione per i membri del team.

 

Abbiamo chiamato questo progetto Atlas come il titano che fu costretto da Zeus a tenere sulle spalle l’intera volta celeste: nella stessa maniera anche il nostro framework vuole essere lo strumento sul quale poggiare i prodotti presenti e futuri realizzati in IQUII.

In questa prima parte vedremo nel dettaglio come è stata realizzata la parte networking di Atlas; successivamente avremo modo di raccontare nel dettaglio le altre parti.

Come NON realizzare un layer di rete

Alzi la mano chi, per realizzare un layer di rete, non si è mai affidato ad un singleton dove aggiungere tutte le chiamate di rete utilizzate nella propria applicazione.

Purtroppo si tratta di uno scenario abbastanza comune, anche tra gli sviluppatori più senior: in effetti si tratta della maniera più veloce con cui approcciare questo genere di problemi.

Non fraintendetemi: non sono tra chi crede che i singleton siano il male: il fatto è che questo genere di approccio va volontariamente contro uno dei principi cardine del buon software: il Single Responsibility Principle.

A prescindere dalla definizione che possiamo quando parliamo di buon codice, una caratteristica risulta però imprescindibile: il buon codice deve essere mantenibile nel tempo, adattandosi a cambiamenti che, si vogliano o no, fanno parte di un normale ciclo di vita.

Codice che risulta scarsamente adattabile, dove ogni modifica è motivo di frustrazione per il team, richiede un tempo largamente superiore alle stime e rischia di generare regressioni è un codice che diventa velocemente ingestibile e quindi obsoleto (richiedendo di conseguenza continui rework non previsti).

In qualunque codice di scarsa qualità si può sempre trovare una class con più di una responsabilità.

In un layer di rete di questo tipo il singleton è a conoscenza di ogni cosa; dalla connessione, ai path di ogni richiesta, all’utente attivo e ad altri elementi dell’app fintanto – nei casi peggiori – perfino view controller dell’interfaccia.
In pratica tutto vive all’interno di una grossa grassa classe che rende impossibile un sano testing e meno che mai operazioni dependency injection o unit testing.

La giusta via

Uno dei goal di Atlas è quello di fornire un layer di gestione di rete che possa interfacciarsi con la nostra controparte server, Athena.
Athena è un moderno backend creato dal nostro team server e basato su NodeJS/ MongoDB in grado di servire API RESTful.

In effetti, come Atlas rappresenta il fondamento di ogni app mobile in IQUII, anche Athena fa un lavoro simile lato backend: ogni nuovo progetto o prodotto viene costruito attraverso questa base comune.

Da questo punto di vista ci sono un set di funzionalità minime richieste:

  • Persistenza della sessione: consente all’applicazione host di eseguire facilmente login/ logout mantenendo le informazioni di sessione aggiornate e presenti in maniera del tutto trasparente allo sviluppatore;
  • Un buon grado di astrazione: il framework deve prevedere un singolo punto di estensione dove aggiungere e modificare le chiamate di rete gestendo in maniera trasparente l’aggiunta di nuovi endpoint, timeout, gestione errori di rete, mancata presenza di connessione e sopratutto la trasformazione dei dati grezzi in entità gestiti dal backend.
  • Gestire l’autenticazione del server: in Athena le credenziali sono gestite tramite token JTW .
    Questo, in buona sostanza, vuol dire che a cascata della login l’app riceve due token, un access e un refresh token.
    Il primo rappresenta la sessione utente, ha una durata limitata nel tempo e va allegato ad ogni richiesta verso il server; il secondo, dalla durata più lunga, va salvato in maniera sicura all’interno dell’app e va utilizzato nel momento in cui si ha la necessità di fare un refresh del primo (per i più curiosi questo articolo descrive la procedura nel dettaglio)

Il compito di Atlas è gestire in maniera automatica e del tutto trasparente tutte le situazioni derivanti da questo approccio.

Una volta definiti i requisiti è necessario scegliere i tool giusti per realizzarli.
Nella versione iOS del framework, di cui mi sono occupato direttamente, abbiamo scelto:

  • Hydra: una implementazione del concetto di FuturePromise derivante da JS ES6. Grazie all’utilizzo di promise (come rimpiazzo alle classiche callbacks o datasourcedelegate) abbiamo semplificato molto la gestione degli errori, dei recovery e la necessità di eseguire a cascata e in maniera asincrona le operazioni. Hydra è a tutti gli effetti il componente chiave dietro l’intera architettura (per i più curiosi ho scritto un articolo su come implementare una architettura di questo tipo qui);
  • SwiftyJSON: anche se con l’avvento di Swift 4 il parsing dei JSON si è enormemente semplificato, SwiftyJSON rimane comunque un prezioso tool in grado di gestire dati JSON grezzi senza nascondersi troppo dietro la “magia” dei reflection;
  • JWTDecode: consente di gestire i JSON Web Token;
  • SwiftyUserDefaults: utilizzato per la gestione strong-typed delle preferenze.

Architettura

Il seguente grafico mostra l’architettura alla base di Atlas:

IQUII Atlas

 

ServiceConfig

Il SC agisce come storage della configurazione di rete; Atlas può istanziare questo oggetto in maniera programmatica oppure generarne uno a partire dalla configurazione esplicitata all’interno del file Info.plist dell’app host.
Al fine di automatizzare e semplificare la gestione degli ambienti (tipicamente Staging/ Production) creiamo per ogni chiave di configurazione (url,environment,headers…) una User Defined Settings legata all’ambiente. in questo modo sarà compito di XCode impostare i valori corretti a seconda dello schema scelto.
Chiaramente dalla lista delle chiavi va esclusa la APIKey che invece sarà definita a runtime dal programmatore e assegnata prima dell’utilizzo del layer.

Service

Service è il cuore del layer di rete; si tratta di una classe che risponde ad un protocollo il cui unico metodo richiesto è quello di execute() di una Request.

L’utilizzo del protocollo consente di implementare più versioni di un Service che possono utilizzare diversi engine di rete; in un buona sostanza consente di isolare completamente l’implementazione di rete da quella che è l’app: in questa prima versione siamo ricorsi ad Alamofire ma stiamo già lavorando per portare tutto sotto il classico NSURLSession semplificando e alleggerendo il nostro layer.

Con questo obiettivo in mente modificare lo strato più basso non comporterà alcuna modifica alle applicazioni che utilizzano Atlas portando contemporaneamente però i benefici di una modifica che in altre occasioni sarebbe stata quantomeno complessa.

L’implementazione che segue è quella fatta in Alamofire:

IQUII Atlas

 

Request & Response

Request incapsula tutte le informazioni che riguardano una request:

  • Endpoint della chiamata (ex. “authlogin”);
  • HTTP Method (POST,GET,PUT…);
  • Timeout;
  • Parametri di input e come sono forniti (url encoded, multipart, JSON, XML, GraphQL etc.);
  • Output atteso (raw data, automatic entity conversion, JSON o XML)

Una singola richiesta è completamente indipendente dal servizio sulla quale verrà eseguita, ed è proprio questo l’obiettivo!

Ecco un esempio di Request:

let service: Service = ...
let rq = Request(.post, "save/article/{art_cat}/id/{art_id}", ["art_id" : 55, "art_cat": "scifi"])
rq.timeout = 5 // if not specified Service's global timeout is used
rq.body = RequestBody.json(["text" : "bla bla bla", "author": "Me"])
 
service.execute(rq, retry: 3).then( { response in
 // response ok, read inside
}).catch({ err in 
 // request fail with error
})

La Response è ciò che viene ritornato dall’esecuzione di una Request all’interno di un Service; essa comprenderà:

  • Request originale (utilizzabile per debug ma rimossa automaticamente in produzione per alleggerire l’ambiente);
  • Response HTTP (headers/body);
  • Dati di benchmark (latenza di rete, tempi di start/end della richiesta etc; tutte informazioni utili al debug).

Inoltre Response espone due funzioni importanti: toJSON() e toString() che possono essere utilizzate per eseguire il parsing di dati grezzi o effettuare debug delle response.

JSONOperation & DataOperation

Collocati appena sopra lo strato inferiore di rete, composto da Service, Request e Response, si trovano due altre classi: DataOperation e JSONOperation (sottoclasse della prima).

Entrambe sono concrete implementation del protocollo Operation, il cui scopo è quello di definire un ulteriore livello di astrazione sopra le operazioni di rete: in effetti una Operation raggruppa una serie di operazioni di business logic atte a preparare il dato grezzo trasformandolo in entità utilizzabile dall’applicazione host.

Un esempio tipico è l’operazione di Login con la quale viene si effettuata la login al backend ma a cui seguono una serie di altre operazioni quali l’estrapolazione del profilo utente e la gestione dei token.

Quando uno sviluppatore al lavoro su un’app mobile IQUII ha bisogno di aggiungere una nuova chiamata di rete, il suo unico compito sarà quello di creare una nuova Operation, tipicamente una JSONOperation.

JSONOperation mette a disposizione diverse funzionalità quali:

  • Conversione da dati a modello automatica: i dati grezzi JSON sono convertiti automaticamente in entità gestibili dall’applicazione host. Un classico feed reader potrebbe ricevere un array di dati grezzi json per trasformarli in oggetti Article: nessun parsing o gestione particolare dei dati asincroni è richiesta; tutta la complessità è gestita in maniera automatica da Atlas;
  • Gestione degli errori automatica: la gestione degli errori del server (siano essi di input validation o di altra natura) è gestita automaticamente dalla classe che provvederà all’eventuale recovery o alla segnalazione della tipologia di errore direttamente all’app di hosting. Sono gestiti automaticamente anche problematiche generiche come la gestione di versioni obsolete o situazioni di indisponibilità dei servizi;
  • Incapsulamento della business logic: ogni operazione contiene al suo interno una Request che comprende, in una singola entità tutte le informazioni necessarie a gestire correttamente la comunicazione cn il server.

 

Un esempio

Nell’esempio sotto viene riportata (in maniera semplificata) la creazione di una operation per gestire la login di un utente verso Athena.

Atlas mette a disposizione un singleton Application nel quale viene esposto un primo Service (nulla toglie che il programmatore possa crearne un secondo a sua discrezione).

Application è l’oggetto fondamentale a cui accedere ai servizi di Atlas; al momento del bootstrap dell’applicazione l’ambiente viene configurato automaticamente secondo le impostazioni fornite.
Il sistema gestisce automaticamente il profilo dell’utente eventualmente loggato, la setup del device e tutte le informazioni riguardanti la sessione.

Ecco come appare la login:

public class LoginOp<T: UserModelProtocol>: AtlasOperation<T> {

  public init(email: String, password: String) {
    super.init()
    self.request = Request(method: .post, endpoint: type.endpoint)
    self.tokenRefreshAllowed = false
    self.request?.headers = [Constants.Headers.Authorization.rawValue : nil] // remove auth header
    self.request?.body = RequestBody.json(["email": email, "pwd": password])
    self.request?.cachePolicy = URLRequest.CachePolicy.reloadIgnoringCacheData
    self.onGenerateOutputModel = { json in
      let newUser = T(json["data"])
      return newUser
    }
  }
}

Un punto interessante di questa classe è composto dalla funzione onGenerateOutputModel ; essa agisce come punto di ingresso per consentire allo sviluppatore di generare (manualmente se necessario) un modello dati a partire da dati grezzi.
Una secondo punto di ancoraggio, chiamato onValidateJSONResponse, consentirà l’eventuale verifica dei dati ottenuti prima di passare allo step successivo.

La presenza della proprietà tokenRefreshAllowed infine consente di disabilitare, qualora necessaria, la gestione automatica del token (nel caso di login si tratta di una operazione non rilevante).

Con la nostra operazione già pronta possiamo infine estendere il Service di Atlas fornendo una funzione asincrona per il login:

public extension AtlasService {

  public func login<T: UserModelProtocol>(email: String, pwd: String) -> Promise<T> {
    return Promise<T>({ r, rj, st in
      LoginOp<T>(email: email, password: pwd).in(self.app).exec().then({ loggedUser in
        self.app.setLoggedUser(loggedUser)
        r(loggedUser)
      }).catch({ err in
        rj(err)
      })
    })
  }

}

Come si può notare si tratta semplicemente di una shortcut per l’esecuzione di LoginOp all’interno di un service esistente.

Giunti a questo punto chiamare la funzione è un gioco da ragazzi:

App.network.login(email: "...", pwd:: "...").then { user in
 // asynchronously return logged user
}.catch({ err in
 // something went wrong
})

Appena una linea di codice: gestione asincrona del codice, degli errori e conversione automatica dei dati grezzi al model. “Pura magia“!

Come anticipato ad inizio articolo Atlas è molto di più di un layer di rete; nelle successive occasioni avremo modo di approfondire l’oggetto Application, vero cuore centrale per accedere ad Atlas.

public let App: Application = Application.main

public class Application {
  /// Main singleton reference.
  public static var main: Application = Application()

  /// Configuration
  internal var configuration: AppConfiguration!

  /// Main Networking service
  public lazy var network: AtlasService

  /// Currently logged user
  public private(set) var loggedUser: UserModelProtocol? = nil

  ... // and much more...
}

Conclusioni

Come abbiamo visto da questo veloce viaggio all’interno di Atlas, la creazione di uno strato unificato rappresenta un vero passo in avanti nella gestione del codice e nella realizzazione dei prodotti.

La necessità di una metodologia unificata all’interno del ciclo di vita del software è un requisito imprescindibile, la cui forza risiede sopratutto nella facilità con cui consente ai prodotti di evolvere nel tempo limitando rework e problematiche tipiche di approcci singoli.

Nei prossimi articoli continueremo questo viaggio esplorando altre componenti del nostro framework.


Daniele Margutti

"Don't code today what you can't debug tomorrow" Code craftsman, UX/UI lover, Swift addicted.


Non perderti i prossimi post! Ricevili via mail per leggerli quando vuoi.

Dai un'occhiata all'archivio


Nel caso di commenti i dati verranno trattati come da informativa

ARTICOLI CORRELATI

di IQUII

5G: la quinta generazione di tecnologie mobile alla base della società connessa

Smart City, Self-Driving Car e Smart Home: il 5G sarà l’elemento disruptive di molti settori e l’asset principale di una società sempre più connessa.

in: Thinking

di IQUII

Mobile Advertising e tracciamento campagne: Apple mette a disposizione il nuovo framework “SKAdNetwork”

Con l’ultima release iOS 11.3, Apple mette a disposizione un nuovo set di API, “SKAdNetwork”, per il tracciamento pubblicitario all’interno dell’App Store: un cambiamento che secondo alcuni esperti potrebbe ridefinire la posizione della casa di Cupertino nell’ecosistema del Mobile Advertising.

in: Thinking

CONTATTACI SUBITO

IQUII S.r.l.Part of Be Group

Sede Legale: Viale dell'Esperanto 71 – 00144 Roma

P.iva 11289201003 - Cap.Soc. 10.000 €
Reg. Imprese di Roma REA n.1293642

Email. info@iquii.com
Tel. +39 06 72.15.125

Sede Operativa Roma
Via Vincenzo Lamaro 13/15 Ed.U, 00173

Sede Operativa Milano
Piazza degli Affari 3, 20123

Mobile Analytics

NEWSLETTER

Ricevi una volta al mese il nostro #ForwardThinking / Dai un'occhiata all'archivio