Perché VIPER è una cattiva scelta per la tua prossima applicazione

C’è molto clamore su VIPER nell’ultimo anno, e tutti sono così ispirati da esso. La maggior parte di questi articoli sono di parte e cercano di dimostrare quanto sia figo. Non lo è. Ha almeno la stessa quantità di problemi, se non di più, di altri pattern di architettura. In questo articolo, voglio spiegare perché VIPER non è così buono come viene pubblicizzato e non è adatto alla maggior parte delle vostre applicazioni.

Alcuni articoli sul confronto delle architetture di solito affermano che VIPER è completamente diverso da qualsiasi architettura basata su MVC. Questa affermazione non è vera: è solo una normale MVC, dove stiamo dividendo il controller in due parti: interactor e presenter. La vista rimane la stessa, ma il modello è rinominato in entità. Router merita qualche parola speciale: Sì, è vero, altre architetture non promuovono questa parte nella loro abbreviazione, ma esiste ancora, in modo implicito (sì, quando chiamate pushViewController state scrivendo un semplice router) o più esplicito (per esempio FlowCoordinators).

Ora voglio parlare dei benefici che VIPER vi offre. Userò questo libro come riferimento completo su VIPER. Cominciamo con il secondo obiettivo, conformarsi al SRP (Single Responsibility Principle). È un po’ grezzo, ma quale strana mente può chiamare questo un beneficio? Siete pagati per risolvere compiti, non per essere conformi a qualche parola d’ordine. Sì, stai ancora usando TDD, BDD, unit-test, Realm o SQLite, dependency injection, e qualsiasi cosa tu possa ricordare, ma lo stai usando per risolvere il problema del cliente, non per usarlo e basta.

Un altro obiettivo è molto più interessante. La testabilità è una preoccupazione molto importante. Merita un articolo a sé stante poiché molte persone ne parlano, ma solo pochi testano veramente le loro applicazioni e ancora meno lo fanno bene.

Una delle ragioni principali di ciò è la mancanza di buoni esempi. Ci sono molti articoli sulla creazione di un test unitario con assert 2 + 2 == 4 ma nessun esempio di vita reale (btw, Artsy sta facendo un ottimo lavoro con l’open sourcing delle sue applicazioni, dovresti dare un’occhiata ai loro progetti).

VIPER sta suggerendo di separare tutta la logica in un mucchio di piccole classi con responsabilità separate. Può rendere i test più facili, ma non sempre. Sì, scrivere test unitari per una classe semplice è facile, ma la maggior parte di questi test non stanno testando nulla. Per esempio, guardiamo la maggior parte dei metodi di un presentatore, sono solo un proxy tra una vista e altri componenti. Potete scrivere test per quel proxy, aumenterà la copertura dei test del vostro codice, ma questi test sono inutili. Avete anche un effetto collaterale: dovreste aggiornare questi test inutili dopo ogni modifica nel codice principale.

L’approccio corretto al testing dovrebbe includere il testing dell’interactor e del presenter allo stesso tempo, perché queste due parti sono profondamente integrate. Inoltre, poiché separiamo due classi, abbiamo bisogno di molti più test rispetto al caso in cui abbiamo solo una classe. È una semplice сombinatoria: la classe A ha 4 possibili stati, la classe B ha 6 possibili stati, quindi una combinazione di A e B ha 20 stati, e dovreste testarli tutti.

L’approccio giusto per semplificare i test è introdurre la purezza al codice invece di dividere semplicemente uno stato complicato in un mucchio di classi.

Può sembrare strano, ma testare le viste è più facile che testare del codice aziendale. Una vista espone lo stato come un insieme di proprietà ed eredita l’aspetto visivo da queste proprietà. Quindi è possibile utilizzare FBSnapshotTestCase per far corrispondere lo stato con l’aspetto. Ancora non gestisce alcuni casi limite come le transizioni personalizzate, ma quanto spesso lo si implementa?

Design eccessivamente ingegnerizzato

VIPER è ciò che accade quando gli ex programmatori Java aziendali invadono il mondo iOS. -n0damage, commento reddit

Onestamente, qualcuno può guardare questo e dire: “sì, queste classi e protocolli extra hanno davvero migliorato la mia capacità di capire cosa sta succedendo con la mia applicazione”.

Immaginate un compito semplice: abbiamo un pulsante che attiva un aggiornamento da un server e una vista con dati da una risposta del server. Indovina quante classi/protocolli saranno interessati da questo cambiamento? Sì, almeno 3 classi e 4 protocolli saranno cambiati per questa semplice funzione. Nessuno si ricorda come Spring ha iniziato con alcune astrazioni ed è finito con AbstractSingletonProxyFactoryBean? Voglio sempre una “comoda superclasse di proxy factory bean per proxy factory beans che creano solo singleton” nel mio codice.

Componenti ridondanti

Come ho già detto prima, un presentatore è di solito una classe molto stupida che sta solo proxando le chiamate dalla vista all’interactor (qualcosa come questo). Sì, a volte contiene una logica complicata, ma nella maggior parte dei casi è solo un componente ridondante.

Quantità di protocolli “DI-friendly”

C’è una confusione comune con questa abbreviazione: VIPER sta implementando i principi SOLID dove DI significa “inversione di dipendenza”, non “iniezione”. L’iniezione di dipendenza è un caso speciale del pattern Inversione di controllo, che è correlato, ma diverso dalla Dependency Inversion.

Dependency Inversion riguarda la separazione di moduli di diversi livelli introducendo astrazioni tra loro. Per esempio, il modulo UI non dovrebbe dipendere direttamente dal modulo di rete o di persistenza. L’inversione di controllo è diversa, è quando un modulo (di solito da una libreria che non possiamo cambiare) delega qualcosa a un altro modulo, che è tipicamente fornito al primo modulo come dipendenza. Sì, quando implementate l’origine dati per il vostro UITableView state usando il principio IoC. Usare le stesse caratteristiche del linguaggio per diversi scopi di alto livello è una fonte di confusione qui.

Torniamo a VIPER. Ci sono molti protocolli (almeno 5) tra ogni classe all’interno di un modulo. E non sono affatto necessari. Presenter e Interactor non sono moduli di livelli diversi. Applicare il principio IoC può avere senso, ma fatevi una domanda: quanto spesso implementate almeno due presentatori per una vista? Credo che la maggior parte di voi abbia risposto zero. Allora perché è necessario creare questo mucchio di protocolli che non useremo mai?

Inoltre, a causa di questi protocolli, non potete facilmente navigare nel vostro codice con un IDE, perché cmd+click vi indirizzerà ad un protocollo, invece di una vera implementazione.

Problemi di performance

E’ una cosa cruciale e ci sono molte persone che semplicemente non se ne preoccupano, o stanno solo sottovalutando un impatto da decisioni di cattiva architettura.

Non parlerò del framework Typhoon (che è molto popolare per la dependency injection nel mondo ObjC). Certo, ha un certo impatto sulle prestazioni, specialmente quando si usa l’auto-iniezione, ma VIPER non richiede di usarlo. Invece, voglio parlare del runtime e dell’avvio dell’app e di come VIPER rallenti la tua app letteralmente ovunque.

Tempo di avvio dell’app. È raramente discusso, ma è un argomento importante, se la tua app si avvia molto lentamente gli utenti eviteranno di usarla. L’anno scorso c’è stata una sessione al WWDC sull’ottimizzazione del tempo di avvio delle app. TL; DR: Il tempo di avvio della tua app dipende direttamente da quante classi hai. Se hai 100 classi va bene, nessuno lo noterà. Tuttavia, se la tua app ha solo 100 classi, hai davvero bisogno di questa architettura complicata? Ma se la vostra app è enorme, per esempio, state lavorando su un’app per Facebook (18k classi) questo impatto è enorme, qualcosa intorno a 1 secondo secondo secondo la sessione precedentemente menzionata. Sì, il lancio a freddo della vostra app richiederà 1 secondo solo per caricare tutti i metadati delle classi e nient’altro, avete letto bene.

Runtime dispatch. È più complicato, molto più difficile da profilare e per lo più applicabile solo al compilatore Swift (perché ObjC ha ricche capacità di runtime e un compilatore non può tranquillamente eseguire queste ottimizzazioni). Parliamo di quello che succede sotto la pelle quando si chiama un metodo (uso “chiamare” invece di “inviare un messaggio” perché il secondo termine non è sempre corretto per Swift). Ci sono 3 tipi di dispatch in Swift (dal più veloce al più lento): statico, dispatch di tabella e invio di un messaggio. L’ultimo è l’unico tipo che viene usato in ObjC, e viene usato in Swift quando è richiesto l’interop con il codice ObjC, o quando dichiariamo un metodo come dynamic. Naturalmente, questa parte del runtime è enormemente ottimizzata e scritta in assembler per tutte le piattaforme. Ma cosa succederebbe se potessimo evitare questo overhead perché il compilatore ha conoscenza di ciò che verrà chiamato al momento della compilazione? È esattamente ciò che il compilatore Swift sta facendo con il dispatch statico e il table dispatch. Il dispatch statico è veloce, ma il compilatore non può usarlo senza il 100% di fiducia nei tipi nell’espressione. Ma quando il tipo della nostra variabile è un protocollo, il compilatore è costretto a usare il dispatch tramite le tabelle dei testimoni di protocollo. Non è letteralmente lento, ma 1ms qui, 1ms là, e ora il tempo totale di esecuzione è più di un secondo più alto di quello che potremmo ottenere usando il puro codice Swift. Questo paragrafo è collegato al precedente sui protocolli, ma penso che sia meglio separare le preoccupazioni sulla quantità sconsiderata di protocolli inutilizzati con un vero e proprio pasticcio con il compilatore con esso.

Separazione delle astrazioni deboli

Ci dovrebbe essere un – e preferibilmente solo un – modo ovvio per farlo.

Una delle domande più popolari nella comunità VIPER è “dove dovrei mettere X?”. Così, da un lato, ci sono molte regole su come dovremmo fare le cose nel modo giusto, ma da un altro lato molte cose sono basate sull’opinione. Possono essere casi complicati, per esempio gestire CoreData con NSFetchedResultsController, o UIWebView. Ma anche casi comuni, come usare UIAlertController, è un argomento di discussione. Diamo un’occhiata, qui abbiamo un router che si occupa di allarmi, ma lì abbiamo una vista che presenta un allarme. Si può provare a battere questo con un argomento che è un semplice alert è un caso speciale per il semplice alert senza alcuna azione eccetto la chiusura.

I casi speciali non sono abbastanza speciali da infrangere le regole.

Sì, ma perché qui abbiamo una factory per creare quel tipo di alert? Quindi, abbiamo un casino anche con un caso così semplice con UIAlertController. Lo vuoi?

Generazione di codice

La leggibilità conta.

Come può essere un problema con un’architettura? È solo generare un mucchio di classi template invece di scriverlo da solo. Qual è il problema? Il problema è che la maggior parte delle volte stai leggendo del codice (a meno che tu non stia lavorando in una società di outsourcing usa e getta), non scrivendolo. Quindi state leggendo codice boilerplate incasinato con un codice reale la maggior parte del tempo. È buono? Non credo.

Conclusione

Non perseguo l’obiettivo di scoraggiare del tutto l’uso di VIPER. Ci possono essere ancora alcune app che possono beneficiare di tutto. Tuttavia, prima di iniziare a sviluppare la tua app dovresti farti alcune domande:

  1. Questa app avrà una lunga durata?
  2. Le specifiche sono abbastanza stabili? In alternativa, si può finire con enormi refactorings senza fine anche per piccole modifiche.
  3. Testate davvero le vostre app? Sii onesto con te stesso.

Solo se rispondi “sì” a tutte le domande VIPER può essere una buona scelta per la tua app.

Infine, l’ultima: dovresti usare il tuo cervello per le tue decisioni, non fidarti ciecamente di qualche ragazzo di Medium o della conferenza a cui hai partecipato che dice: “Usa X, X è figo”. Anche questi ragazzi possono sbagliarsi.