Varför VIPER är ett dåligt val för din nästa ansökan

Det har varit mycket hype kring VIPER det senaste året, och alla är så inspirerade av det. De flesta av dessa artiklar är partiska om det och försöker visa hur coolt det är. Det är det inte. Det har minst lika många problem, om inte ännu fler, som andra arkitekturmönster. I den här artikeln vill jag förklara varför VIPER inte är så bra som det marknadsförs och inte är lämpligt för de flesta av dina appar.

En del artiklar om att jämföra arkitekturer brukar hävda att VIPER är en helt annan än alla MVC-baserade arkitekturer. Detta påstående är inte sant: Det är bara en vanlig MVC, där vi delar upp controller i två delar: interactor och presenter. Vyn förblir densamma, men modellen byter namn till entitet. Router förtjänar några särskilda ord: Ja, det är sant, andra arkitekturer främjar inte denna del i sin förkortning, men den finns fortfarande, implicit (ja, när du kallar pushViewController skriver du en enkel router) eller på ett mer explicit sätt (till exempel FlowCoordinators).

Nu vill jag tala om de fördelar som VIPER erbjuder dig. Jag kommer att använda den här boken som en fullständig referens om VIPER. Låt oss börja med det andra målet, att följa SRP (Single Responsibility Principle). Det är lite grovt, men vilket konstigt sinne kan kalla detta för en fördel? Du får betalt för att lösa uppgifter, inte för att anpassa dig till något modeord. Ja, du använder fortfarande TDD, BDD, enhetstester, Realm eller SQLite, beroendeinjektion och vad du nu kan komma ihåg, men du använder det för att lösa kundens problem, inte för att bara använda det.

Ett annat mål är mycket mer intressant. Testbarhet är en mycket viktig fråga. Det förtjänar en fristående artikel eftersom många pratar om det, men bara ett fåtal testar verkligen sina appar och ännu färre gör det på rätt sätt.

En av de viktigaste orsakerna till det är bristen på bra exempel. Det finns många artiklar om att skapa ett enhetstest med assert 2 + 2 == 4 men inga exempel från verkligheten (för övrigt gör Artsy ett utmärkt jobb genom att open sourcing sina appar, du borde ta en titt på deras projekt).

VIPER föreslår att man ska separera all logik i ett gäng små klasser med separata ansvarsområden. Det kan göra det lättare att testa, men inte alltid. Ja, det är lätt att skriva enhetstest för en enkel klass, men de flesta av dessa tester testar ingenting. Låt oss till exempel titta på de flesta av presentatörens metoder, de är bara en proxy mellan en vy och andra komponenter. Du kan skriva tester för denna proxy, det kommer att öka testtäckningen av din kod, men dessa tester är värdelösa. Du har också en bieffekt: du bör uppdatera dessa värdelösa tester efter varje ändring i huvudkoden.

Den korrekta metoden för testning bör inkludera testning av interactor och presenter samtidigt eftersom dessa två delar är djupt integrerade. Eftersom vi separerar mellan två klasser behöver vi dessutom mycket fler tester i jämförelse med ett fall där vi bara har en klass. Det är en enkel сombinatorik: klass A har 4 möjliga tillstånd, klass B har 6 möjliga tillstånd, så en kombination av A och B har 20 tillstånd, och du bör testa allt.

Det rätta tillvägagångssättet för att förenkla testningen är att införa renhet i koden i stället för att bara dela upp komplicerade tillstånd i en massa klasser.

Det kan låta konstigt, men det är lättare att testa vyer än att testa en del företagskod. En vy exponerar tillstånd som en uppsättning egenskaper och ärver visuellt utseende från dessa egenskaper. Sedan kan du använda FBSnapshotTestCase för att matcha tillstånd med utseende. Det hanterar fortfarande inte vissa kantfall som anpassade övergångar, men hur ofta implementerar du det?

Overengineered design

VIPER är vad som händer när tidigare Java-programmerare från företag invaderar iOS-världen. -n0damage, reddit-kommentar

Ärligt talat, kan någon titta på det här och säga: ”

Föreställ dig en enkel uppgift: Vi har en knapp som utlöser en uppdatering från en server och en vy med data från ett serversvar. Gissa hur många klasser/protokoll som kommer att påverkas av denna förändring? Ja, minst 3 klasser och 4 protokoll kommer att ändras för denna enkla funktion. Kommer ingen ihåg hur Spring började med några abstraktioner och slutade med AbstractSingletonProxyFactoryBean? Jag vill alltid ha en ”bekväm proxy factory bean superklass för proxy factory beans som endast skapar singletons” i min kod.

Redundanta komponenter

Som jag redan har nämnt tidigare är en presentatör vanligtvis en mycket dum klass som bara proxyser anrop från view till interactor (ungefär så här). Ja, ibland innehåller den komplicerad logik, men i de flesta fall är det bara en överflödig komponent.

”DI-vänlig” mängd protokoll

Det finns en vanlig förvirring med denna förkortning: VIPER tillämpar SOLID-principer där DI betyder ”dependency inversion”, inte ”injection”. Dependency injection är ett specialfall av Inversion of Control-mönstret, som är besläktat, men skiljer sig från Dependency Inversion.

Dependency Inversion handlar om att separera moduler från olika nivåer genom att införa abstraktioner mellan dem. Till exempel bör UI-modulen inte vara direkt beroende av nätverks- eller persistensmodulen. Inversion of Control är annorlunda, det är när en modul (vanligtvis från ett bibliotek som vi inte kan ändra) delegerar något till en annan modul, som vanligtvis tillhandahålls till den första modulen som ett beroende. Ja, när du implementerar datakällan för din UITableView använder du IoC-principen. Att använda samma språkfunktioner för olika ändamål på hög nivå är en källa till förvirring här.

Vi återgår till VIPER. Det finns många protokoll (minst fem) mellan varje klass i en modul. Och de är inte alls nödvändiga. Presenter och Interactor är inte moduler från olika lager. Att tillämpa IoC-principen kan vara vettigt, men ställ dig själv en fråga: hur ofta implementerar du minst två presentatörer för en vy? Jag tror att de flesta av er svarade noll. Så varför är det nödvändigt att skapa en massa protokoll som vi aldrig kommer att använda?

På grund av dessa protokoll kan du inte heller enkelt navigera i din kod med en IDE, eftersom cmd+klick kommer att navigera dig till ett protokoll, istället för till en riktig implementering.

Prestandaproblem

Det är en avgörande sak och det finns många människor som bara inte bryr sig om det, eller bara underskattar en påverkan från dåliga arkitekturbeslut.

Jag kommer inte att prata om Typhoon-ramverket (som är mycket populärt för injektion av beroenden i ObjC-världen). Naturligtvis har det en viss prestandapåverkan, särskilt när du använder auto-injection, men VIPER kräver inte att du använder det. Istället vill jag prata om körtid och appstart och hur VIPER saktar ner din app bokstavligen överallt.

Appstarttid. Det diskuteras sällan, men det är ett viktigt ämne, om din app startar väldigt långsamt kommer användarna att undvika att använda den. Det fanns en session på förra WWDC om optimering av appstarttiden förra året. TL; DR: Din appstarttid beror direkt på hur många klasser du har. Om du har 100 klasser går det bra, ingen kommer att märka detta. Men om din app bara har 100 klasser, behöver du verkligen denna komplicerade arkitektur? Men om din app är enorm, till exempel om du arbetar med en Facebook-app (18 000 klasser) är denna påverkan enorm, något runt 1 sekund enligt den tidigare nämnda sessionen. Ja, om du startar din app kallt kommer det att ta 1 sekund bara för att ladda alla metadata om klasserna och inget annat, du läste rätt.

Runtime dispatch. Det är mer komplicerat, mycket svårare att profilera och mestadels tillämpligt endast för Swift-kompilatorn (eftersom ObjC har rika runtime-funktioner och en kompilator inte säkert kan utföra dessa optimeringar). Låt oss prata om vad som händer under huven när du anropar någon metod (jag använder ”anropa” istället för ”skicka ett meddelande” eftersom den andra termen inte alltid är korrekt för Swift). Det finns 3 typer av dispatchning i Swift (från snabbare till långsammare): statisk, table dispatch och skicka ett meddelande. Den sista är den enda typen som används i ObjC, och den används i Swift när interop med ObjC-kod krävs, eller när vi deklarerar en metod som dynamic. Naturligtvis är den här delen av körtiden enormt optimerad och skriven i assembler för alla plattformar. Men tänk om vi kan undvika denna overhead eftersom kompilatorn har kunskap om vad som kommer att anropas vid kompileringstiden? Det är precis vad Swift-kompilatorn gör med statisk och tabellbaserad dispatchning. Statisk dispatch är snabb, men kompilatorn kan inte använda den utan att vara 100 % säker på typerna i uttrycket. Men när variabelns typ är ett protokoll är kompilatorn tvungen att använda sig av expediering via protokollvittnestabeller. Det är inte bokstavligen långsamt, men 1ms här, 1ms där, och nu är den totala exekveringstiden mer än en sekund högre än vad vi skulle kunna uppnå med ren Swift-kod. Detta stycke är relaterat till föregående om protokoll, men jag tror att det är bättre att separera bekymmer om bara hänsynslös mängd oanvända protokoll med en verklig röra med kompilatorn med det.

Svaga abstraktioner separation

Det borde finnas ett – och helst bara ett – uppenbart sätt att göra det på.

En av de populäraste frågorna i VIPER-communityn är ”var ska jag lägga X?”. Så, från ena sidan finns det många regler för hur vi ska göra saker och ting rätt, men från den andra sidan är en hel del saker åsiktsbaserade. Det kan vara komplicerade fall, till exempel hantering av CoreData med NSFetchedResultsController eller UIWebView. Men även vanliga fall, som att använda UIAlertController, är ett diskussionsämne. Låt oss ta en titt, här har vi en router som hanterar varningar, men där har vi en vy som presenterar en varning. Du kan försöka besegra detta med argumentet att en enkel varning är ett specialfall för enkel varning utan andra åtgärder än att stänga.

Specialfall är inte tillräckligt speciella för att bryta mot reglerna.

Ja, men varför har vi här en fabrik för att skapa den typen av varningar? Vi har alltså en röra även med ett så enkelt fall som UIAlertController. Vill du ha det?

Kodgenerering

Läsbarheten räknas.

Hur kan detta vara ett problem med en arkitektur? Det är bara att generera en massa mallklasser istället för att skriva det själv. Vad är problemet med det? Problemet är att man för det mesta läser kod (om man inte jobbar i ett slöseri med outsourcingföretag), inte skriver den. Så du läser boilerplate-kod som är mixad med en riktig kod för det mesta. Är det bra? Jag tror inte det.

Slutsats

Jag strävar inte efter att avskräcka dig från att använda VIPER överhuvudtaget. Det finns fortfarande kan vara vissa appar som kan dra nytta av alla. Innan du börjar utveckla din app bör du dock ställa dig själv några frågor:

  1. Är den här appen tänkt att ha en lång livslängd?
  2. Är specifikationerna tillräckligt stabila? Alternativt kan du hamna i oändliga enorma refaktoriseringar även för små ändringar.
  3. Testar du verkligen dina appar? Var ärlig mot dig själv.

Endast om du svarar ”ja” på alla frågor kan VIPER vara ett bra val för din app.

Det sista: du bör använda din egen hjärna för dina beslut, lita inte bara blint på någon kille från Medium eller en konferens som du deltog i som säger: ”Använd X, X är coolt.” Dessa killar kan också ha fel.