V posledním roce se kolem VIPERu strhl velký humbuk a všichni se jím nechali inspirovat. Většina těchto článků je o něm zaujatá a snaží se demonstrovat, jak je skvělý. Tak to ale není. Má přinejmenším stejné množství problémů, ne-li ještě více, jako jiné vzory architektury. V tomto článku chci vysvětlit, proč VIPER není tak dobrý, jak je inzerován, a není vhodný pro většinu vašich aplikací.
Některé články o porovnávání architektur obvykle tvrdí, že VIPER je úplně jiný než všechny architektury založené na MVC. Toto tvrzení není pravdivé: Je to prostě normální MVC, kde rozdělujeme kontrolér na dvě části: interaktor a prezentér. Zobrazení zůstává stejné, ale model je přejmenován na entitu. Router si zaslouží několik zvláštních slov: Ano, je pravda, že jiné architektury tuto část ve své zkratce nepropagují, ale stále existuje, ať už implicitně (ano, když voláte pushViewController
, píšete jednoduchý router), nebo explicitněji (například FlowCoordinators).
Teď bych chtěl mluvit o výhodách, které vám VIPER nabízí. Tuto knihu použiji jako kompletní referenci o VIPERu. Začněme druhým cílem, dodržováním principu SRP (Single Responsibility Principle). Je to trochu drsné, ale který podivný rozum to může nazvat výhodou? Jste placeni za řešení úkolů, ne za to, že se podřídíte nějakému módnímu slovu. Ano, stále používáte TDD, BDD, unit-testy, Realm nebo SQLite, dependency injection a cokoliv, na co si vzpomenete, ale používáte to za řešení problému zákazníka, ne za to, že to jen používáte.
Další cíl je mnohem zajímavější. Testovatelnost je velmi důležitým zájmem. Zaslouží si samostatný článek, protože o ní mluví mnoho lidí, ale jen málo z nich své aplikace skutečně testuje a ještě méně z nich to dělá správně.
Jedním z hlavních důvodů je nedostatek dobrých příkladů. Existuje mnoho článků o vytváření jednotkových testů s assert 2 + 2 == 4
, ale žádné reálné příklady (btw, Artsy odvádí vynikající práci díky open sourcingu svých aplikací, měli byste se podívat na jejich projekty).
VIPER navrhuje oddělit veškerou logiku do hromady malých tříd s oddělenými povinnostmi. Může to usnadnit testování, ale ne vždy. Ano, napsat unit-test pro jednoduchou třídu je snadné, ale většina těchto testů nic netestuje. Podívejme se například na většinu metod prezentéru, které jsou jen prostředníkem mezi pohledem a ostatními komponentami. Pro tuto proxy můžete napsat testy, zvýšíte tím pokrytí kódu testy, ale tyto testy jsou k ničemu. Navíc máte vedlejší efekt: tyto zbytečné testy byste měli aktualizovat po každé úpravě v hlavním kódu.
Správný přístup k testování by měl zahrnovat testování interaktoru a presenteru současně, protože tyto dvě části jsou hluboce integrované. Navíc kvůli oddělení dvou tříd potřebujeme mnohem více testů ve srovnání s případem, kdy máme jen jednu třídu. Je to jednoduchá sombinatorika: třída A má 4 možné stavy, třída B má 6 možných stavů, takže kombinace A a B má 20 stavů a měli byste je všechny otestovat.
Správný přístup ke zjednodušení testování spočívá v zavedení čistoty do kódu namísto pouhého rozdělení složitého stavu do několika tříd.
Možná to zní divně, ale testování pohledů je jednodušší než testování nějakého obchodního kódu. Pohled vystavuje stav jako sadu vlastností a z těchto vlastností dědí vizuální vzhled. Pak můžete použít FBSnapshotTestCase pro porovnání stavu se vzhledem. Stále to nezvládá některé okrajové případy, jako jsou vlastní přechody, ale jak často to implementujete?“
VIPER je to, co se stane, když bývalí podnikoví programátoři Javy vtrhnou do světa iOS. -n0damage, komentář na redditu
Přímo, může se na to někdo podívat a říct: „
Představte si jednoduchou úlohu: máme tlačítko, které spustí aktualizaci ze serveru, a pohled s daty z odpovědi serveru. Hádejte, kolik tříd/protokolů bude touto změnou ovlivněno? Ano, kvůli této jednoduché funkci se změní nejméně 3 třídy a 4 protokoly. Copak si nikdo nepamatuje, jak Spring začal s nějakými abstrakcemi a skončil s AbstractSingletonProxyFactoryBean
? Vždycky chci mít v kódu „pohodlnou supertřídu proxy factory bean pro proxy factory beany, které vytvářejí jen singletony“
Zbytečné komponenty
Jak už jsem se zmínil dříve, presenter je obvykle velmi hloupá třída, která jen proxyuje volání z view na interactor (něco takového). Ano, někdy obsahuje složitou logiku, ale ve většině případů je to jen zbytečná komponenta.
„DI-friendly“ množství protokolů
S touto zkratkou dochází k častému zmatení: VIPER implementuje principy SOLID, kde DI znamená „inverzi závislostí“, nikoliv „vstřikování“. Injekce závislostí je speciální případ vzoru Inversion of Control, který je příbuzný, ale liší se od Inverze závislostí.
Inverze závislostí spočívá v oddělení modulů z různých úrovní zavedením abstrakcí mezi nimi. Například modul uživatelského rozhraní by neměl přímo záviset na modulu sítě nebo perzistence. Inverze řízení je něco jiného, jde o to, když modul (obvykle z knihovny, kterou nemůžeme změnit) deleguje něco na jiný modul, který je obvykle poskytnut prvnímu modulu jako závislost. Ano, když implementujete zdroj dat pro svůj UITableView
, používáte princip IoC. Používání stejných funkcí jazyka pro různé vysokoúrovňové účely je zde zdrojem zmatků.
Vraťme se k VIPER. Mezi jednotlivými třídami uvnitř modulu existuje mnoho protokolů (nejméně 5). A nejsou vůbec povinné. Presenter a Interactor nejsou moduly z různých vrstev. Použití principu IoC může mít smysl, ale položte si otázku: Jak často implementujete alespoň dva prezentéry pro jeden pohled? Věřím, že většina z vás odpověděla, že nula. Proč je tedy nutné vytvářet tuto hromadu protokolů, které nikdy nepoužijeme?“
Díky těmto protokolům se také nemůžete snadno orientovat v kódu pomocí IDE, protože cmd+kliknutí vás naviguje na protokol, místo na skutečnou implementaci.
Problémy s výkonem
Je to zásadní věc a je spousta lidí, kteří se o to prostě nestarají, nebo jen podceňují dopad špatných rozhodnutí o architektuře.
Nebudu mluvit o frameworku Typhoon (který je ve světě ObjC velmi populární pro dependency injection). Samozřejmě má určitý dopad na výkon, zejména když používáte automatické vstřikování, ale VIPER jeho použití nevyžaduje. Místo toho chci mluvit o době běhu a spouštění aplikace a o tom, jak VIPER zpomaluje vaši aplikaci doslova všude.
Čas spouštění aplikace. Málokdy se o něm mluví, ale je to důležité téma, pokud se vaše aplikace spouští velmi pomalu, uživatelé se jejímu používání vyhnou. Na loňské konferenci WWDC proběhlo zasedání o optimalizaci doby spouštění aplikací v loňském roce. TL; DR: Doba spouštění aplikace přímo závisí na tom, kolik máte tříd. Pokud máte 100 tříd, je to v pořádku, nikdo si toho nevšimne. Pokud má však vaše aplikace jen 100 tříd, opravdu potřebujete tak složitou architekturu? Pokud je ale vaše aplikace obrovská, například pracujete na aplikaci pro Facebook (18k tříd), je tento dopad obrovský, něco kolem 1 sekundy podle dříve zmíněné relace. Ano, studené spuštění vaší aplikace bude trvat 1 sekundu jen proto, aby se načetla metadata všech tříd a nic jiného, čtete správně.
Runtime dispatch. Je složitější, mnohem hůře se profiluje a většinou je použitelný pouze pro kompilátor Swiftu (protože ObjC má bohaté runtime možnosti a kompilátor nemůže bezpečně provádět tyto optimalizace). Pojďme se bavit o tom, co se děje pod kapotou, když voláte nějakou metodu (používám „volat“ místo „posílat zprávu“, protože druhý termín není pro Swift vždy správný). Ve Swiftu existují 3 typy dispečinku (od rychlejšího po pomalejší): statický, dispečink tabulky a odeslání zprávy. Poslední typ je jediný, který se používá v ObjC, a ve Swiftu se používá v případě, že je vyžadováno propojení s kódem ObjC, nebo když deklarujeme metodu jako dynamic
. Tato část běhového prostředí je samozřejmě obrovsky optimalizovaná a napsaná v assembleru pro všechny platformy. Ale co když se této režii můžeme vyhnout, protože kompilátor má při kompilaci znalosti o tom, co se bude volat? Přesně to dělá kompilátor Swiftu se statickým a tabulkovým dispečinkem. Statický dispatch je rychlý, ale kompilátor ho nemůže použít, aniž by měl 100% jistotu typů ve výrazu. Když je však typem naší proměnné protokol, je kompilátor nucen použít dispečink prostřednictvím tabulek svědků protokolu. Není to doslova pomalé, ale 1 ms sem, 1 ms tam, a nyní je celková doba provádění o více než jednu sekundu vyšší, než bychom mohli dosáhnout pomocí čistého kódu Swift. Tento odstavec souvisí s předchozím o protokolech, ale myslím, že je lepší oddělit obavy z pouhého bezohledného množství nepoužívaných protokolů od skutečného zaneřádění překladače tím.
Slabé oddělení abstrakcí
Měl by existovat jeden – a nejlépe jen jeden – zřejmý způsob, jak to udělat.
Jednou z nejoblíbenějších otázek v komunitě VIPER je „kam mám dát X?“. Z jedné strany tedy existuje mnoho pravidel, jak bychom to měli dělat správně, ale z druhé strany je spousta věcí založena na názoru. Může jít o komplikované případy, například zpracování CoreData pomocí NSFetchedResultsController
, nebo UIWebView
. Ale i běžné případy, jako je používání UIAlertController
, jsou tématem k diskusi. Podívejme se, zde máme router zabývající se alertem, ale tam máme pohled prezentující alert. Můžete se to pokusit přebít argumentem, že jednoduchý alert je speciální případ pro jednoduchý alert bez jakýchkoli akcí kromě zavření.
Speciální případy nejsou natolik speciální, aby porušovaly pravidla.
Jo, ale proč tady máme továrnu na vytváření takového typu alertů? Takže máme zmatek i v takovém jednoduchém případě s UIAlertController
. Chcete to?“
Generování kódu
Záleží na čitelnosti.
Jak to může být u architektury problém? Je to jen generování hromady šablonových tříd místo toho, abych si to napsal sám. V čem je problém? Problém je v tom, že většinu času kód čtete (pokud nepracujete ve vyhazovací outsourcingové firmě), nikoliv píšete. Takže většinu času čtete šablonovitý kód zaneřáděný skutečným kódem. Je to dobře? Myslím, že ne.
Závěr
Nesleduji cíl vás od používání VIPERu vůbec odradit. Stále mohou existovat aplikace, které mohou být přínosem pro všechny. Než však začnete vyvíjet svou aplikaci, měli byste si položit několik otázek:
- Má tato aplikace dlouhou životnost?
- Jsou specifikace dostatečně stabilní? V opačném případě můžete skončit u nekonečných obrovských refaktoringů i kvůli malým změnám.
- Skutečně testujete své aplikace? Buďte k sobě upřímní.
Pouze pokud na všechny otázky odpovíte „ano“, může být VIPER pro vaši aplikaci dobrou volbou.
Nakonec poslední: pro svá rozhodnutí byste měli používat vlastní mozek, ne jen slepě věřit nějakému člověku z média nebo konference, které jste se zúčastnili, a který říká: „Používejte X, X je super.“ I tito lidé se mohou mýlit.