Der har været en masse hype om VIPER i det sidste år, og alle er så inspireret af det. De fleste af disse artikler er forudindtagede om det og forsøger at demonstrere, hvor cool det er. Det er det ikke. Det har mindst lige så mange problemer, hvis ikke endnu flere, som andre arkitekturmønstre. I denne artikel vil jeg forklare, hvorfor VIPER ikke er så godt som det er annonceret og ikke er egnet til de fleste af dine apps.
Nogle artikler om sammenligning af arkitekturer plejer at hævde, at VIPER er en helt anden end alle MVC-baserede arkitekturer. Denne påstand er ikke sand: Det er bare en normal MVC, hvor vi opdeler controller i to dele: interactor og presenter. View forbliver den samme, men model er omdøbt til entity. Router fortjener nogle særlige ord: Ja, det er sandt, andre arkitekturer fremmer ikke denne del i deres forkortelse, men den eksisterer stadig, på implicit (ja, når du kalder pushViewController
skriver du en simpel router) eller mere eksplicit måde (for eksempel FlowCoordinators).
Nu vil jeg tale om de fordele, som VIPER tilbyder dig. Jeg vil bruge denne bog som en komplet reference om VIPER. Lad os starte med det andet mål, som er i overensstemmelse med SRP (Single Responsibility Principle). Det er lidt groft, men hvilket mærkeligt sind kan kalde dette for en fordel? Du bliver betalt for at løse opgaver, ikke for at overholde et eller andet buzzword. Ja, du bruger stadig TDD, BDD, unit-tests, Realm eller SQLite, dependency injection, og hvad du ellers kan huske, men du bruger det til at løse kundens problem, ikke til bare at bruge det.
Et andet mål er langt mere interessant. Testbarhed er en meget vigtig bekymring. Det fortjener en selvstændig artikel, da mange mennesker taler om det, men kun få tester virkelig deres apps, og endnu færre gør det rigtigt.
En af hovedårsagerne til det er mangel på gode eksempler. Der er mange artikler om at lave en enhedstest med assert 2 + 2 == 4
, men ingen eksempler fra det virkelige liv (btw, Artsy gør et fremragende stykke arbejde ved at open sourcing deres apps, du bør tage et kig på deres projekter).
VIPER foreslår at adskille al logik i en masse små klasser med adskilte ansvarsområder. Det kan gøre det nemmere at teste, men ikke altid. Ja, det er nemt at skrive unit-test for en simpel klasse, men de fleste af disse tests tester ingenting. Lad os f.eks. se på de fleste af en præsenters metoder, de er blot en proxy mellem en visning og andre komponenter. Du kan skrive tests for denne proxy, det vil øge testdækningen af din kode, men disse tests er ubrugelige. Du har også en bivirkning: du skal opdatere disse ubrugelige tests efter hver redigering i hovedkoden.
Den rigtige tilgang til testning bør omfatte test af interactor og presenter på samme tid, fordi disse to dele er dybt integrerede. Desuden, fordi vi adskiller mellem to klasser, har vi brug for langt flere tests i sammenligning med et tilfælde, hvor vi kun har én klasse. Det er en simpel сombinatorik: klasse A har 4 mulige tilstande, klasse B har 6 mulige tilstande, så en kombination af A og B har 20 tilstande, og du bør teste det hele.
Den rigtige tilgang til at forenkle testning er at indføre renhed i koden i stedet for blot at opdele kompliceret tilstand i en masse klasser.
Det lyder måske mærkeligt, men det er lettere at teste visninger end at teste noget forretningskode. En visning eksponerer tilstand som et sæt egenskaber og arver det visuelle udseende fra disse egenskaber. Derefter kan du bruge FBSnapshotTestCase til at matche tilstand med udseende. Det håndterer stadig ikke nogle edge cases som f.eks. brugerdefinerede overgange, men hvor ofte implementerer du det?
Overengineered design
VIPER er det, der sker, når tidligere enterprise Java-programmører invaderer iOS-verdenen. -n0damage, reddit-kommentar
Honestly, can someone look at this and say: “Ja, disse ekstra klasser og protokoller har virkelig forbedret min evne til at forstå, hvad der sker med min applikation”.
Forestil dig en simpel opgave: Vi har en knap, der udløser en opdatering fra en server, og en visning med data fra et serversvar. Gæt, hvor mange klasser/protokoller der vil blive påvirket af denne ændring? Ja, mindst 3 klasser og 4 protokoller vil blive ændret for denne enkle funktion. Er der ingen, der husker, hvordan Spring startede med nogle abstraktioner og endte med AbstractSingletonProxyFactoryBean
? Jeg vil altid have en “bekvem proxy factory bean superklasse til proxy factory beans, der kun skaber singletons” i min kode.
Redundante komponenter
Som jeg allerede har nævnt det før, er en presenter normalt en meget dum klasse, der bare proxyer opkald fra view til interactor (noget i den retning). Ja, nogle gange indeholder den kompliceret logik, men i de fleste tilfælde er det bare en overflødig komponent.
“DI-venlig” mængde af protokoller
Der er en almindelig forvirring med denne forkortelse: VIPER implementerer SOLID-principper, hvor DI betyder “dependency inversion”, ikke “injection”. Dependency injection er et specialtilfælde af Inversion of Control-mønsteret, som er beslægtet, men forskelligt fra Dependency Inversion.
Dependency Inversion handler om at adskille moduler fra forskellige niveauer ved at indføre abstraktioner mellem dem. F.eks. bør UI-modulet ikke være direkte afhængigt af netværks- eller persistensmodulet. Inversion of Control er anderledes, det er, når et modul (normalt fra et bibliotek, som vi ikke kan ændre) delegerer noget til et andet modul, som typisk leveres til det første modul som en afhængighed. Ja, når du implementerer datakilde til din UITableView
, bruger du IoC-princippet. At bruge de samme sprogfunktioner til forskellige formål på højt niveau er en kilde til forvirring her.
Lad os vende tilbage til VIPER. Der er mange protokoller (mindst 5) mellem hver klasse inden for et modul. Og de er slet ikke nødvendige. Presenter og Interactor er ikke moduler fra forskellige lag. Anvendelse af IoC-princippet kan give mening, men stil dig selv et spørgsmål: Hvor ofte implementerer du mindst to presentere til en visning? Jeg tror, at de fleste af jer har svaret nul. Så hvorfor er det nødvendigt at oprette denne masse af protokoller, som vi aldrig vil bruge?
Og på grund af disse protokoller kan du heller ikke nemt navigere i din kode med en IDE, fordi cmd+klik vil navigere dig til en protokol i stedet for en rigtig implementering.
Performanceproblemer
Det er en afgørende ting, og der er mange mennesker, der bare er ligeglade med det, eller som bare undervurderer en indvirkning fra dårlige arkitekturbeslutninger.
Jeg vil ikke tale om Typhoon framework (som er meget populært til dependency injection i ObjC-verdenen). Selvfølgelig har det en vis indvirkning på ydeevnen, især når du bruger auto-injektion, men VIPER kræver ikke, at du bruger det. I stedet vil jeg tale om runtime og app-start, og hvordan VIPER bremser din app bogstaveligt talt overalt.
App-starttid. Det er sjældent diskuteret, men det er et vigtigt emne, hvis din app starter meget langsomt, vil brugerne undgå at bruge den. Der var en session på sidste WWDC om optimering af app-starttid i sidste år. TL; DR: Din app-starttid afhænger direkte af, hvor mange klasser du har. Hvis du har 100 klasser er det fint, ingen vil bemærke dette. Men hvis din app kun har 100 klasser, har du så virkelig brug for denne komplicerede arkitektur? Men hvis din app er enorm, for eksempel hvis du arbejder på Facebook-app (18k klasser) er denne indvirkning enorm, noget omkring 1 sekund i henhold til den tidligere nævnte session. Ja, kold lancering af din app vil tage 1 sekund bare for at indlæse alle klasser metadata og intet andet, du læste det rigtigt.
Runtime dispatch. Det er mere kompliceret, meget sværere at profilere og gælder for det meste kun for Swift-compiler (fordi ObjC har rige runtime-funktioner, og en compiler kan ikke sikkert udføre disse optimeringer). Lad os tale om, hvad der foregår under motorhjelmen, når du kalder en eller anden metode (jeg bruger “call” i stedet for “send a message”, fordi det andet udtryk ikke altid er korrekt for Swift). Der er 3 typer af dispatch i Swift (fra hurtigere til langsommere): statisk, table dispatch og sending a message. Den sidste er den eneste type, der bruges i ObjC, og den bruges i Swift, når interop med ObjC-kode er påkrævet, eller når vi erklærer en metode som dynamic
. Selvfølgelig er denne del af runtime enormt optimeret og skrevet i assembler til alle platforme. Men hvad nu hvis vi kan undgå dette overhead, fordi compileren har viden om, hvad der skal kaldes på kompileringstidspunktet? Det er præcis, hvad Swift-compileren gør med statisk og table dispatch. Statisk dispatch er hurtig, men compileren kan ikke bruge den uden 100 % tillid til typerne i udtrykket. Men når vores variabel type er en protokol, er compileren tvunget til at bruge dispatch via protokolvidne tabeller. Det er ikke bogstaveligt talt langsomt, men 1 ms her, 1 ms der, og nu er den samlede eksekveringstid mere end et sekund højere, end vi kunne opnå med ren Swift-kode. Dette afsnit er relateret til det foregående om protokoller, men jeg tror, det er bedre at adskille bekymringer om bare uforsvarlig mængde ubrugte protokoller med en reel rod med compileren med det.
Svag abstraktionsadskillelse
Der bør være én – og helst kun én – indlysende måde at gøre det på.
Et af de mest populære spørgsmål i VIPER-fællesskabet er “hvor skal jeg lægge X?”. Så fra den ene side er der mange regler for, hvordan vi skal gøre tingene rigtigt, men fra den anden side er en masse ting meningsbaserede. Det kan være komplicerede sager, f.eks. håndtering af CoreData med NSFetchedResultsController
, eller UIWebView
. Men selv almindelige tilfælde, som f.eks. at bruge UIAlertController
, er et emne til diskussion. Lad os tage et kig, her har vi en router, der håndterer en advarsel, men der har vi en visning, der præsenterer en advarsel. Du kan forsøge at slå dette med et argument om, at en simpel alarm er et specialtilfælde for simpel alarm uden andre handlinger end lukning.
Specialtilfælde er ikke specielle nok til at bryde reglerne.
Ja, men hvorfor har vi her en fabrik til at skabe den slags alarmer? Så vi har et rod selv med et så simpelt tilfælde med UIAlertController
. Vil du have det?
Kodegenerering
Læsbarheden tæller.
Hvordan kan det være et problem med en arkitektur? Det er bare at generere en masse skabelonklasser i stedet for at skrive det selv. Hvad er problemet med det? Problemet er, at man det meste af tiden læser kode (med mindre man arbejder i et smidt outsourcing firma), ikke skriver den. Så du læser boilerplate kode rodet med en egentlig kode det meste af din tid. Er det godt? Det tror jeg ikke.
Konklusion
Jeg forfølger ikke et mål om at afskrække dig fra at bruge VIPER overhovedet. Der er stadig kan være nogle apps, der kan drage fordel af alle. Men før du begynder at udvikle din app, bør du stille dig selv et par spørgsmål:
- Er denne app kommer til at have en lang levetid?
- Er specifikationerne stabile nok? Ellers kan du ende med endeløse store refaktoreringer, selv for små ændringer.
- Tester du virkelig dine apps? Vær ærlig over for dig selv.
Kun hvis du svarer “ja” til alle spørgsmålene kan VIPER være et godt valg til din app.
Endeligt, det sidste: Du bør bruge din egen hjerne til dine beslutninger, stol ikke bare blindt på en fyr fra Medium eller en konference, som du deltog i, der siger: “Brug X, X er cool.” Disse fyre kan også tage fejl.