Waarom VIPER een slechte keuze is voor je volgende toepassing

Er is het laatste jaar veel hype over VIPER, en iedereen is er zo door geïnspireerd. De meeste van deze artikelen zijn er bevooroordeeld over en proberen aan te tonen hoe cool het is. Dat is het niet. Het heeft minstens evenveel problemen, zo niet nog meer, als andere architectuurpatronen. In dit artikel wil ik uitleggen waarom VIPER niet zo goed is als wordt geadverteerd en niet geschikt is voor de meeste van je apps.

In sommige artikelen over het vergelijken van architecturen wordt meestal beweerd dat VIPER een heel andere is dan welke MVC-gebaseerde architectuur dan ook. Deze bewering is niet waar: Het is gewoon een normale MVC, waar we controller splitsen in twee delen: interactor en presenter. View blijft hetzelfde, maar model wordt hernoemd naar entity. Router verdient wat speciale woorden: Ja, het is waar, andere architecturen promoten dit deel niet in hun afkorting, maar het bestaat nog steeds, in impliciete (ja, als je pushViewController aanroept, schrijf je een eenvoudige router) of meer expliciete manier (bijvoorbeeld FlowCoordinators).

Nu wil ik het hebben over de voordelen die VIPER je biedt. Ik zal dit boek gebruiken als een complete referentie over VIPER. Laten we beginnen met het tweede doel, het voldoen aan SRP (Single Responsibility Principle). Het is een beetje ruw, maar welke vreemde geest kan dit een voordeel noemen? Je wordt betaald voor het oplossen van taken, niet voor het conformeren aan een of ander modewoord. Ja, je gebruikt nog steeds TDD, BDD, unit-tests, Realm of SQLite, dependency injection, en wat je je maar kunt herinneren, maar je gebruikt het om het probleem van de klant op te lossen, niet om het alleen maar te gebruiken.

Een ander doel is veel interessanter. Testbaarheid is een zeer belangrijk punt. Het verdient een apart artikel, omdat veel mensen het erover hebben, maar slechts weinigen testen hun apps echt en nog minder doen het goed.

Een van de belangrijkste redenen daarvoor is een gebrek aan goede voorbeelden. Er zijn veel artikelen over het maken van een unit test met assert 2 + 2 == 4, maar geen real-life voorbeelden (trouwens, Artsy doet uitstekend werk door het open sourcen van hun apps, moet je eens een kijkje nemen op hun projecten).

VIPER stelt voor om alle logica te scheiden in een bos van kleine klassen met gescheiden verantwoordelijkheden. Het kan het testen gemakkelijker maken, maar niet altijd. Ja, het schrijven van unit-tests voor een eenvoudige klasse is gemakkelijk, maar de meeste van deze tests testen niets. Bijvoorbeeld, laten we eens kijken naar de meeste methodes van een presenter, zij zijn slechts een proxy tussen een view en andere componenten. Je kan tests schrijven voor die proxy, het zal de testdekking van je code verhogen, maar deze tests zijn nutteloos. Je hebt ook een neveneffect: je moet deze nutteloze tests bijwerken na elke wijziging in de hoofdcode.

De juiste aanpak van het testen zou het testen van interactor en presenter tegelijk moeten omvatten, omdat deze twee onderdelen diep geïntegreerd zijn. Bovendien, omdat we scheiden tussen twee klassen die we nodig hebben veel meer tests in vergelijking met een geval waarin we hebben slechts een klasse. Het is een simpele сombinatoriek: klasse A heeft 4 mogelijke toestanden, klasse B heeft 6 mogelijke toestanden, dus een combinatie van A en B heeft 20 toestanden, en die zou je allemaal moeten testen.

De juiste aanpak om testen te vereenvoudigen is het introduceren van zuiverheid in de code in plaats van ingewikkelde toestanden op te splitsen in een aantal klassen.

Het klinkt misschien vreemd, maar het testen van views is eenvoudiger dan het testen van sommige bedrijfscode. Een view geeft de status weer als een set eigenschappen en erft het visuele uiterlijk van deze eigenschappen. Dan kun je FBSnapshotTestCase gebruiken voor het matchen van de status met het uiterlijk. Het kan nog steeds niet overweg met sommige randgevallen zoals aangepaste overgangen, maar hoe vaak implementeer je het?

Overengineered design

VIPER is wat er gebeurt als voormalige enterprise Java-programmeurs de iOS-wereld binnendringen. -n0damage, reddit comment

Eerlijk gezegd, kan iemand hiernaar kijken en zeggen: “ja, deze extra klassen en protocollen hebben mijn vermogen om te begrijpen wat er met mijn applicatie gebeurt, echt verbeterd”.

Stel je een eenvoudige taak voor: we hebben een knop die een update van een server activeert en een view met gegevens van een serverrespons. Raad eens hoeveel klassen/protocollen door deze verandering zullen worden beïnvloed? Ja, ten minste 3 klassen en 4 protocollen zullen worden gewijzigd voor deze eenvoudige functie. Herinnert niemand zich nog hoe Spring begon met enkele abstracties en eindigde met AbstractSingletonProxyFactoryBean? Ik wil altijd een “handige superclass voor proxy factory beans die alleen singletons maken” in mijn code.

Redundante componenten

Zoals ik al eerder heb gezegd, is een presenter meestal een hele domme klasse die alleen maar proxying calls van view naar interactor (zoiets als dit). Ja, soms bevat het ingewikkelde logica, maar in de meeste gevallen is het gewoon een overbodige component.

“DI-vriendelijke” hoeveelheid protocollen

Er is een veel voorkomende verwarring met deze afkorting: VIPER implementeert SOLID-principes waarbij DI “dependency inversion” betekent, niet “injection”. Dependency injection is een speciaal geval van het Inversion of Control patroon, dat verwant is, maar verschillend van Dependency Inversion.

Dependency Inversion gaat over het scheiden van modules van verschillende niveaus door abstracties tussen hen te introduceren. Bijvoorbeeld, UI-module mag niet direct afhankelijk van netwerk of persistentie module. Inversion of Control is anders, het is wanneer een module (meestal van een bibliotheek die we niet kunnen veranderen) iets delegeert aan een andere module, die typisch wordt verstrekt aan de eerste module als een dependency. Ja, wanneer je data source voor je UITableView implementeert, gebruik je het IoC principe. Het gebruik van dezelfde taal functies voor verschillende high-level doeleinden is een bron van verwarring hier.

Laten we teruggaan naar VIPER. Er zijn veel protocollen (ten minste 5) tussen elke klasse in een module. En ze zijn helemaal niet nodig. Presenter en Interactor zijn geen modules uit verschillende lagen. Het toepassen van het IoC principe kan zinvol zijn, maar stel jezelf de vraag: hoe vaak implementeer je tenminste twee presenters voor één view? Ik denk dat de meesten van jullie nul hebben geantwoord. Dus waarom is het nodig om deze hoop protocollen te maken die we nooit zullen gebruiken?

Ook, vanwege deze protocollen, kun je niet gemakkelijk door je code navigeren met een IDE, omdat cmd+klik je naar een protocol zal navigeren, in plaats van een echte implementatie.

Prestatie problemen

Het is een cruciaal ding en er zijn een heleboel mensen die er gewoon niet om geven, of die gewoon de impact van slechte architectuur beslissingen onderschatten.

Ik zal het niet hebben over Typhoon framework (dat erg populair is voor dependency injection in de ObjC wereld). Natuurlijk heeft het enige invloed op de prestaties, vooral als je auto-injectie gebruikt, maar VIPER vereist niet dat je het gebruikt. In plaats daarvan wil ik het hebben over runtime en app startup en hoe VIPER je app letterlijk overal vertraagt.

App startup time. Het wordt zelden besproken, maar het is een belangrijk onderwerp, als uw app lanceert erg traag gebruikers zullen vermijden het te gebruiken. Er was een sessie op de laatste WWDC over het optimaliseren van app opstarttijd in vorig jaar. TL; DR: De opstarttijd van je app is direct afhankelijk van het aantal classes dat je hebt. Als je 100 classes hebt is het prima, niemand zal dit opmerken. Maar als je app maar 100 classes heeft, heb je dan echt zo’n ingewikkelde architectuur nodig? Maar als je app enorm is, bijvoorbeeld, je werkt aan een Facebook app (18k classes) dan is de impact enorm, iets rond de 1 seconde volgens de eerder genoemde sessie. Ja, koude lancering van uw app duurt 1 seconde alleen maar om alle klassen metadata te laden en niets anders, je leest het goed.

Runtime dispatch. Het is ingewikkelder, veel moeilijker te profileren en meestal alleen van toepassing op Swift compilers (omdat ObjC rijke runtime mogelijkheden heeft en een compiler deze optimalisaties niet veilig kan uitvoeren). Laten we het hebben over wat er onderhuids gebeurt als je een methode aanroept (ik gebruik “call” in plaats van “send a message” omdat de tweede term niet altijd correct is voor Swift). Er zijn 3 soorten dispatch in Swift (van sneller naar langzamer): static, table dispatch en het versturen van een bericht. De laatste is het enige type dat in ObjC wordt gebruikt, en het wordt in Swift gebruikt wanneer interop met ObjC code is vereist, of wanneer we een methode declareren als dynamic. Natuurlijk is dit deel van de runtime enorm geoptimaliseerd en geschreven in assembler voor alle platforms. Maar wat als we deze overhead kunnen vermijden omdat de compiler kennis heeft over wat er aangeroepen gaat worden tijdens het compileren? Dat is precies wat de Swift compiler doet met static en table dispatch. Static dispatch is snel, maar de compiler kan het niet gebruiken zonder 100% vertrouwen in de types in de expressie. Maar als het type van onze variabele een protocol is, is de compiler gedwongen om dispatch te gebruiken via protocol-getuigen tabellen. Het is niet letterlijk traag, maar 1ms hier, 1ms daar, en nu is de totale uitvoeringstijd meer dan een seconde hoger dan we zouden kunnen bereiken met pure Swift code. Deze paragraaf is gerelateerd aan de vorige over protocollen, maar ik denk dat het beter is om zorgen over alleen maar roekeloze hoeveelheid ongebruikte protocollen te scheiden van een echte knoeiboel met de compiler hiermee.

Zwakke abstractiescheiding

Er zou één – en bij voorkeur slechts één – voor de hand liggende manier moeten zijn om dit te doen.

Eén van de populairste vragen in de VIPER-gemeenschap is “waar moet ik X plaatsen?”. Aan de ene kant zijn er dus veel regels voor hoe we dingen goed moeten doen, maar aan de andere kant zijn veel dingen gebaseerd op meningen. Het kan ingewikkelde gevallen betreffen, bijvoorbeeld het omgaan met CoreData met NSFetchedResultsController, of UIWebView. Maar zelfs gewone gevallen, zoals het gebruik van UIAlertController, is onderwerp van discussie. Laten we eens kijken, hier hebben we een router die met een alert omgaat, maar daar hebben we een view die een alert presenteert. Je kunt proberen om dit te verslaan met een argument dat is een eenvoudige waarschuwing is een speciaal geval voor eenvoudige waarschuwing zonder enige acties, behalve sluiten.

Speciale gevallen zijn niet speciaal genoeg om de regels te breken.

Ja, maar waarom hier hebben we een fabriek voor het maken van dat soort waarschuwingen? Dus, we hebben een puinhoop zelfs met zo’n eenvoudig geval met UIAlertController. Wil je het?

Code generatie

Leesbaarheid telt.

Hoe kan dit een probleem zijn met een architectuur? Het is gewoon het genereren van een stel template classes in plaats van het zelf te schrijven. Wat is daar het probleem mee? Het probleem is dat je meestal code leest (tenzij je in een wegwerp outsourcing bedrijf werkt), niet schrijft. Dus het grootste deel van de tijd lees je boilerplate code die geknoeid is met echte code. Is dat goed? Ik denk het niet.

Conclusie

Ik streef er niet naar om je te ontmoedigen VIPER überhaupt te gebruiken. Er is nog steeds kunnen sommige apps die kunnen profiteren van alle. Echter, voordat u begint met het ontwikkelen van uw app moet je jezelf een paar vragen stellen:

  1. Gaat deze app een lange levensduur hebben?
  2. Zijn de specificaties stabiel genoeg? Anders kunt u eindigen met eindeloze enorme refactorings, zelfs voor kleine wijzigingen.
  3. Doet u echt testen van uw apps? Wees eerlijk tegen jezelf.

Alleen als je op alle vragen “ja” antwoordt, kan VIPER een goede keuze zijn voor je app.

Ten slotte, de laatste: je moet je eigen hersens gebruiken voor je beslissingen, vertrouw niet blindelings op een of andere vent van Medium of conferentie die je hebt bijgewoond en die zegt: “Gebruik X, X is cool.” Deze jongens kunnen het ook mis hebben.