De ce VIPER este o alegere proastă pentru următoarea dvs. aplicație

Există o mulțime de hype despre VIPER în ultimul an, și toată lumea este atât de inspirată de el. Cele mai multe dintre aceste articole sunt părtinitoare despre el și încearcă să demonstreze cât de tare este. Nu este așa. Are cel puțin aceeași cantitate de probleme, dacă nu chiar mai multe, ca și alte modele de arhitectură. În acest articol, vreau să explic de ce VIPER nu este atât de bun pe cât este promovat și nu este potrivit pentru majoritatea aplicațiilor dumneavoastră.

Câteva articole despre compararea arhitecturilor susțin de obicei că VIPER este o arhitectură complet diferită de orice arhitectură bazată pe MVC. Această afirmație nu este adevărată: este doar o MVC normală, în care împărțim controlerul în două părți: interactor și prezentator. Vizualizarea rămâne aceeași, dar modelul este redenumit în entitate. Router-ul merită câteva cuvinte speciale: Da, este adevărat, alte arhitecturi nu promovează această parte în abrevierea lor, dar ea există în continuare, în mod implicit (da, atunci când apelați pushViewController scrieți un router simplu) sau mai explicit (de exemplu FlowCoordinators).

Acum vreau să vorbesc despre beneficiile pe care vi le oferă VIPER. Voi folosi această carte ca o referință completă despre VIPER. Să începem cu cel de-al doilea obiectiv, conformarea la SRP (Single Responsibility Principle). Este un pic cam dur, dar ce minte ciudată poate numi asta un beneficiu? Ești plătit pentru rezolvarea sarcinilor, nu pentru a te conforma unor cuvinte la modă. Da, folosești în continuare TDD, BDD, teste unitare, Realm sau SQLite, injecție de dependență și orice îți amintești, dar le folosești pentru a rezolva problema clientului, nu doar pentru a le folosi.

Un alt obiectiv este mult mai interesant. Testabilitatea este o preocupare foarte importantă. Merită un articol de sine stătător, deoarece multă lume vorbește despre ea, dar doar câțiva își testează cu adevărat aplicațiile și chiar mai puțini o fac corect.

Unul dintre principalele motive este lipsa exemplelor bune. Există multe articole despre crearea unui test unitar cu assert 2 + 2 == 4, dar nu există exemple din viața reală (btw, Artsy face o treabă excelentă prin open sourcing-ul aplicațiilor lor, ar trebui să aruncați o privire la proiectele lor).

VIPER sugerează să separați toată logica într-o grămadă de clase mici cu responsabilități separate. Acest lucru poate face testarea mai ușoară, dar nu întotdeauna. Da, scrierea de teste unitare pentru o clasă simplă este ușoară, dar majoritatea acestor teste nu testează nimic. De exemplu, să ne uităm la majoritatea metodelor unui prezentator, acestea sunt doar un proxy între o vizualizare și alte componente. Puteți scrie teste pentru acest proxy, va crește acoperirea de testare a codului dumneavoastră, dar aceste teste sunt inutile. Aveți, de asemenea, un efect secundar: ar trebui să actualizați aceste teste inutile după fiecare modificare în codul principal.

Abordarea corectă a testării ar trebui să includă testarea interactorului și a prezentatorului în același timp, deoarece aceste două părți sunt profund integrate. În plus, deoarece separăm între două clase, avem nevoie de mult mai multe teste în comparație cu un caz în care avem o singură clasă. Este o simplă сombinatorică: clasa A are 4 stări posibile, clasa B are 6 stări posibile, deci o combinație între A și B are 20 de stări și ar trebui să le testați pe toate.

Abordarea corectă pentru a simplifica testarea este introducerea purității în cod în loc de a împărți pur și simplu o stare complicată într-o grămadă de clase.

Poate suna ciudat, dar testarea vizualizărilor este mai ușoară decât testarea unor coduri de afaceri. O vizualizare expune starea ca un set de proprietăți și moștenește aspectul vizual din aceste proprietăți. Apoi, puteți utiliza FBSnapshotTestCase pentru potrivirea stării cu aspectul. Tot nu gestionează unele cazuri limită, cum ar fi tranzițiile personalizate, dar cât de des îl implementați?

Proiectare suprainginerită

VIPER este ceea ce se întâmplă când foști programatori Java de întreprindere invadează lumea iOS. -n0damage, comentariu pe reddit

Honestly, poate cineva să se uite la asta și să spună: „da, aceste clase și protocoale suplimentare mi-au îmbunătățit cu adevărat capacitatea de a înțelege ce se întâmplă cu aplicația mea”.

Imaginați-vă o sarcină simplă: avem un buton care declanșează o actualizare de la un server și o vizualizare cu date dintr-un răspuns al serverului. Ghiciți câte clase/protocoale vor fi afectate de această schimbare? Da, cel puțin 3 clase și 4 protocoale vor fi modificate pentru această funcție simplă. Nu-și amintește nimeni cum Spring a început cu niște abstracțiuni și s-a terminat cu ? Întotdeauna îmi doresc în codul meu o „superclasă convenabilă de proxy factory bean pentru proxy factory beans care creează doar singletons”.

Componente redundante

Așa cum am menționat deja înainte, un prezentator este, de obicei, o clasă foarte proastă care nu face decât să proxenetizeze apelurile de la view la interactor (ceva de genul acesta). Da, uneori conține o logică complicată, dar în cele mai multe cazuri este doar o componentă redundantă.

Cantitatea de protocoale „DI-friendly”

Există o confuzie comună cu această abreviere: VIPER implementează principiile SOLID, unde DI înseamnă „inversare a dependenței”, nu „injecție”. Injectarea dependențelor este un caz special al modelului Inversion of Control, care este înrudit, dar diferit de Dependency Inversion.

Dependency Inversion se referă la separarea modulelor de la niveluri diferite prin introducerea de abstracțiuni între ele. De exemplu, modulul UI nu ar trebui să depindă direct de modulul de rețea sau de persistență. Inversiunea de control este diferită, este atunci când un modul (de obicei dintr-o bibliotecă pe care nu o putem modifica) deleagă ceva unui alt modul, care este de obicei furnizat primului modul ca dependență. Da, atunci când implementați sursa de date pentru UITableView, utilizați principiul IoC. Folosirea acelorași caracteristici ale limbajului pentru diferite scopuri de nivel înalt este o sursă de confuzie aici.

Să ne întoarcem la VIPER. Există multe protocoale (cel puțin 5) între fiecare clasă din interiorul unui modul. Și ele nu sunt deloc necesare. Prezentatorul și Interactor nu sunt module din straturi diferite. Aplicarea principiului IoC poate avea sens, dar puneți-vă o întrebare: cât de des implementați cel puțin doi prezentatori pentru o vizualizare? Cred că cei mai mulți dintre voi au răspuns zero. Deci, de ce este necesar să creăm această grămadă de protocoale pe care nu le vom folosi niciodată?

De asemenea, din cauza acestor protocoale, nu puteți naviga cu ușurință prin codul dvs. cu un IDE, deoarece cmd+click vă va conduce la un protocol, în loc de o implementare reală.

Probleme de performanță

Este un lucru crucial și există o mulțime de oameni cărora pur și simplu nu le pasă de acest lucru, sau pur și simplu subestimează un impact al unor decizii proaste de arhitectură.

Nu voi vorbi despre cadrul Typhoon (care este foarte popular pentru injecția de dependență în lumea ObjC). Desigur, are un anumit impact asupra performanței, în special atunci când folosiți auto-injecția, dar VIPER nu vă cere să îl folosiți. În schimb, vreau să vorbesc despre timpul de execuție și de pornire a aplicației și despre modul în care VIPER vă încetinește aplicația literalmente peste tot.

Timp de pornire a aplicației. Se discută rar, dar este un subiect important, dacă aplicația dvs. se lansează foarte încet, utilizatorii vor evita să o folosească. A existat o sesiune pe ultimul WWDC despre optimizarea timpului de pornire a aplicațiilor în ultimul an. TL; DR: Timpul de pornire al aplicației dvs. depinde în mod direct de câte clase aveți. Dacă aveți 100 de clase este în regulă, nimeni nu va observa acest lucru. Totuși, dacă aplicația dvs. are doar 100 de clase, chiar aveți nevoie de această arhitectură complicată? Dar dacă aplicația ta este uriașă, de exemplu, lucrezi la aplicația Facebook (18k clase) acest impact este enorm, ceva în jur de 1 secundă conform sesiunii menționate anterior. Da, lansarea la rece a aplicației dvs. va dura 1 secundă doar pentru a încărca toate metadatele claselor și nimic altceva, ați citit bine.

Runtime dispatch. Este mai complicat, mult mai greu de profilat și aplicabil în principal numai pentru compilatorul Swift (deoarece ObjC are capacități bogate de execuție și un compilator nu poate efectua în siguranță aceste optimizări). Să vorbim despre ceea ce se întâmplă sub capotă atunci când apelați o anumită metodă (folosesc „apel” în loc de „trimite un mesaj”, deoarece cel de-al doilea termen nu este întotdeauna corect pentru Swift). Există 3 tipuri de expediere în Swift (de la mai rapidă la mai lentă): statică, expediere de tabel și trimiterea unui mesaj. Ultimul este singurul tip care este utilizat în ObjC și este folosit în Swift atunci când este necesară o interopulare cu codul ObjC sau când declarăm o metodă ca dynamic. Bineînțeles, această parte a runtime-ului este extrem de optimizată și scrisă în asamblor pentru toate platformele. Dar ce s-ar întâmpla dacă am putea evita această suprasolicitare deoarece compilatorul are cunoștințe despre ceea ce va fi apelat în momentul compilării? Este exact ceea ce face compilatorul Swift cu dispecerizarea statică și de tabel. Dispecerizarea statică este rapidă, dar compilatorul nu o poate utiliza fără o încredere de 100% în tipurile din expresie. Dar atunci când tipul variabilei noastre este un protocol, compilatorul este forțat să utilizeze dispecerizarea prin intermediul tabelelor martor de protocol. Nu este literalmente lent, dar 1ms aici, 1ms acolo, iar acum timpul total de execuție este cu mai mult de o secundă mai mare decât am putea obține folosind cod Swift pur. Acest paragraf este legat de cel anterior despre protocoale, dar cred că este mai bine să separăm preocupările legate doar de cantitatea nesăbuită de protocoale nefolosite de o adevărată încurcătură a compilatorului cu acesta.

Separare slabă a abstracțiilor

Ar trebui să existe o singură – și, de preferință, numai una – modalitate evidentă de a face acest lucru.

Una dintre cele mai populare întrebări din comunitatea VIPER este „unde ar trebui să pun X?”. Deci, pe de o parte, există multe reguli despre cum ar trebui să facem lucrurile corect, dar pe de altă parte, o mulțime de lucruri se bazează pe opinii. Poate fi vorba de cazuri complicate, de exemplu manipularea CoreData cu NSFetchedResultsController, sau UIWebView. Dar chiar și cazurile comune, cum ar fi utilizarea UIAlertController, reprezintă un subiect de discuție. Să aruncăm o privire, aici avem un router care se ocupă de alertă, dar acolo avem o vedere care prezintă o alertă. Puteți încerca să învingeți acest lucru cu un argument conform căruia o alertă simplă este un caz special pentru alertă simplă fără alte acțiuni în afară de închidere.

Cazurile speciale nu sunt suficient de speciale pentru a încălca regulile.

Da, dar de ce avem aici o fabrică pentru crearea acestui tip de alerte? Deci, avem o încurcătură chiar și cu un caz atât de simplu cu UIAlertController. Vreți asta?

Generarea de coduri

Lecturabilitatea contează.

Cum poate fi aceasta o problemă cu o arhitectură? Este doar generarea unei grămezi de clase de șabloane în loc să le scriu eu însumi. Care este problema cu asta? Problema este că de cele mai multe ori citești codul (cu excepția cazului în care lucrezi într-o companie de outsourcing de aruncat), nu îl scrii. Așadar, de cele mai multe ori citiți cod boilerplate amestecat cu un cod real. Este un lucru bun? Nu cred că da.

Concluzie

Nu urmăresc un scop de a vă descuraja să folosiți VIPER deloc. Încă mai pot fi unele aplicații care pot beneficia de toate. Cu toate acestea, înainte de a începe să vă dezvoltați aplicația, ar trebui să vă puneți câteva întrebări:

  1. Această aplicație va avea o durată de viață lungă?
  2. Sunt specificațiile suficient de stabile? În mod alternativ, puteți ajunge la nesfârșite refactorizări uriașe chiar și pentru schimbări mici.
  3. Îți testezi cu adevărat aplicațiile? Fii sincer cu tine însuți.

Doar dacă răspunzi „da” la toate întrebările VIPER poate fi o alegere bună pentru aplicația ta.

În cele din urmă, ultima: ar trebui să-ți folosești propriul creier pentru deciziile tale, nu te încrede orbește în vreun tip de pe Medium sau de la conferința la care ai participat și care spune: „Folosiți X, X este cool”. Și tipii ăștia se pot înșela.