Viime vuonna VIPERistä on ollut paljon kohua, ja kaikki ovat innostuneet siitä. Useimmat näistä artikkeleista suhtautuvat siihen puolueellisesti ja yrittävät osoittaa kuinka siisti se on. Eihän se sitä ole. Siinä on vähintään yhtä paljon ongelmia, ellei jopa enemmän, kuin muissakin arkkitehtuurimalleissa. Tässä artikkelissa haluan selittää, miksi VIPER ei ole niin hyvä kuin sitä mainostetaan, eikä se sovellu useimpiin sovelluksiisi.
Joissain arkkitehtuurien vertailua käsittelevissä artikkeleissa väitetään yleensä, että VIPER on täysin erilainen kuin mikään MVC-pohjainen arkkitehtuuri. Tämä väite ei pidä paikkaansa: se on vain normaali MVC, jossa jaamme kontrollerin kahteen osaan: interaktori ja presenteri. Näkymä pysyy samana, mutta malli nimetään uudelleen entiteetiksi. Reititin ansaitsee muutamat erityiset sanat: Kyllä, se on totta, muut arkkitehtuurit eivät mainosta tätä osaa lyhenteissään, mutta se on silti olemassa, implisiittisesti (kyllä, kun kutsut pushViewController
, kirjoitat yksinkertaisen reitittimen) tai eksplisiittisemmin (esimerkiksi FlowCoordinators).
Nyt haluan puhua eduista, joita VIPER tarjoaa sinulle. Käytän tätä kirjaa täydellisenä viitteenä VIPERistä. Aloitetaan toisesta tavoitteesta, SRP:n (Single Responsibility Principle) mukaisuudesta. Se on hieman karkea, mutta mikä outo mieli voi kutsua tätä eduksi? Sinulle maksetaan tehtävien ratkaisemisesta, ei siitä, että noudatat jotain muotisanaa. Kyllä, käytät edelleen TDD:tä, BDD:tä, yksikkötestejä, Realmia tai SQLiteä, riippuvuusinjektiota ja mitä ikinä muistatkaan, mutta käytät sitä asiakkaan ongelman ratkaisemiseen, et vain sen käyttämiseen.
Toinen tavoite on paljon mielenkiintoisempi. Testattavuus on erittäin tärkeä huolenaihe. Se ansaitsee erillisen artikkelin, koska monet puhuvat siitä, mutta vain harvat todella testaavat sovelluksiaan ja vielä harvemmat tekevät sen oikein.
Yksi tärkeimmistä syistä tähän on hyvien esimerkkien puute. On olemassa monia artikkeleita yksikkötestin luomisesta assert 2 + 2 == 4
, mutta ei yhtään todellista esimerkkiä (btw, Artsy tekee erinomaista työtä avohankkimalla sovelluksiaan, kannattaa vilkaista heidän projektejaan).
VIPER ehdottaa kaiken logiikan erottamista joukoksi pieniä luokkia, joilla on erilliset vastuut. Se saattaa helpottaa testausta, mutta ei aina. Kyllä, yksikkötestien kirjoittaminen yksinkertaiselle luokalle on helppoa, mutta suurin osa näistä testeistä ei testaa mitään. Tarkastellaan esimerkiksi useimpia esittelijän metodeja, jotka ovat vain välittäjä näkymän ja muiden komponenttien välillä. Voit kirjoittaa testejä tuolle välittäjälle, mikä lisää koodin testikattavuutta, mutta nämä testit ovat hyödyttömiä. Sinulla on myös sivuvaikutus: sinun pitäisi päivittää nämä hyödyttömät testit jokaisen pääkoodin muokkauksen jälkeen.
Oikea lähestymistapa testaukseen pitäisi sisältää interaktori- ja presenter-testien testaamisen samanaikaisesti, koska nämä kaksi osaa ovat syvästi integroituneita. Lisäksi, koska erotamme kaksi luokkaa toisistaan, tarvitsemme paljon enemmän testejä verrattuna tapaukseen, jossa meillä on vain yksi luokka. Se on yksinkertaista сombinatoriikkaa: luokassa A on 4 mahdollista tilaa, luokassa B on 6 mahdollista tilaa, joten A:n ja B:n yhdistelmällä on 20 tilaa, ja sinun pitäisi testata ne kaikki.
Oikea lähestymistapa testauksen yksinkertaistamiseksi on puhtauden tuominen koodiin sen sijaan, että monimutkaiset tilat vain jaetaan useisiin luokkiin.
Saattaa kuulostaa oudolta, mutta näkymien testaaminen on helpompaa kuin jonkun liiketoimintakoodin testaus. Näkymä paljastaa tilan joukkona ominaisuuksia ja perii visuaalisen ulkoasun näistä ominaisuuksista. Sitten voit käyttää FBSnapshotTestCasea tilan ja ulkonäön yhteensovittamiseen. Se ei edelleenkään käsittele joitakin ääritapauksia, kuten mukautettuja siirtymiä, mutta kuinka usein toteutat sen?
Overengineered design
VIPER on se, mitä tapahtuu, kun entiset Enterprise Java -ohjelmoijat tunkeutuvat iOS-maailmaan. -n0damage, reddit-kommentti
Honestly, can someone look at this and say: ”kyllä, nämä ylimääräiset luokat ja protokollat todella paransivat kykyäni ymmärtää, mitä sovelluksessani tapahtuu.”
Kuvittele yksinkertainen tehtävä: meillä on painike, joka laukaisee päivityksen palvelimelta ja näkymä, jossa on tietoja palvelimen vastauksesta. Arvaa kuinka paljon luokkia/protokollia tämä muutos vaikuttaa? Kyllä, ainakin 3 luokkaa ja 4 protokollaa muuttuu tämän yksinkertaisen ominaisuuden takia. Eikö kukaan muista, miten Spring alkoi joillakin abstraktioilla ja päättyi AbstractSingletonProxyFactoryBean
? Haluan aina koodiini ”kätevän proxy factory bean yläluokan proxy factory beaneille, jotka luovat vain singletoneja.”
Redundantit komponentit
Kuten jo aiemmin mainitsin, presenter on yleensä hyvin typerä luokka, joka vain välittää kutsuja näkymästä vuorovaikuttajalle (jotain tällaista). Kyllä, joskus se sisältää monimutkaista logiikkaa, mutta useimmissa tapauksissa se on vain turha komponentti.
”DI-ystävällinen” määrä protokollia
Tämän lyhenteen kanssa on yleinen sekaannus: VIPER toteuttaa SOLID-periaatteita, joissa DI tarkoittaa ”dependency inversion”, ei ”injection”. Riippuvuusinjektio on erikoistapaus Inversion of Control -mallista, joka liittyy siihen, mutta eroaa Dependency Inversionista.
Dependency Inversionissa on kyse eri tasojen moduulien erottamisesta toisistaan ottamalla käyttöön abstraktioita niiden välille. Esimerkiksi käyttöliittymämoduuli ei saisi olla suoraan riippuvainen verkko- tai pysyvyysmoduulista. Inversion of Control on erilainen, se on kun moduuli (yleensä kirjastosta, jota emme voi muuttaa) delegoi jotain toiselle moduulille, joka tyypillisesti annetaan ensimmäiselle moduulille riippuvuutena. Kyllä, kun toteutat tietolähteen UITableView
:lle, käytät IoC-periaatetta. Samojen kielen ominaisuuksien käyttäminen eri korkean tason tarkoituksiin aiheuttaa tässä sekaannusta.
Palataan takaisin VIPERiin. Jokaisen luokan välillä moduulin sisällä on monia protokollia (ainakin 5). Eikä niitä tarvita lainkaan. Presenter ja Interactor eivät ole moduuleja eri kerroksista. IoC-periaatteen soveltaminen voi olla järkevää, mutta kysy itseltäsi kysymys: kuinka usein toteutat vähintään kaksi esittäjää yhteen näkymään? Uskon, että useimmat teistä vastasivat nollaan. Miksi on siis luotava joukko protokollia, joita emme koskaan tule käyttämään?
Näiden protokollien takia et myöskään voi helposti navigoida koodissasi IDE:llä, koska cmd+klikkaus ohjaa sinut protokollan luo todellisen toteutuksen sijasta.
Suorituskykyongelmat
Se on ratkaiseva asia ja on paljon ihmisiä, jotka eivät vain välitä siitä, tai vain aliarvioivat huonojen arkkitehtuuripäätösten vaikutuksen.
En puhu Typhoon-kehyksestä (joka on hyvin suosittu riippuvuusinjektioon ObjC-maailmassa). Sillä on tietysti jonkin verran suorituskykyvaikutuksia, varsinkin kun käytät autoinjektiota, mutta VIPER ei vaadi sen käyttöä. Sen sijaan haluan puhua suoritusajasta ja sovelluksen käynnistymisestä ja siitä, miten VIPER hidastaa sovellustasi kirjaimellisesti kaikkialla.
Sovelluksen käynnistymisaika. Siitä keskustellaan harvoin, mutta se on tärkeä aihe, jos sovelluksesi käynnistyy hyvin hitaasti, käyttäjät välttävät sen käyttöä. Viime WWDC:llä oli viime vuonna sessio sovelluksen käynnistysajan optimoinnista. TL; DR: Sovelluksesi käynnistymisaika riippuu suoraan siitä, kuinka monta luokkaa sinulla on. Jos sinulla on 100 luokkaa, se on hyvä, kukaan ei huomaa tätä. Jos sovelluksessasi on kuitenkin vain 100 luokkaa, tarvitsetko todella näin monimutkaista arkkitehtuuria? Mutta jos sovelluksesi on valtava, esimerkiksi työskentelet Facebook-sovelluksen parissa (18k luokkaa), tämä vaikutus on valtava, jotain noin 1 sekunti aiemmin mainitun istunnon mukaan. Kyllä, sovelluksen kylmä käynnistäminen vie 1 sekunnin vain kaikkien luokkien metatietojen lataamiseen eikä mihinkään muuhun, luit oikein.
Runtime dispatch. Se on monimutkaisempi, paljon vaikeampi profiloida ja lähinnä sovellettavissa vain Swift-kääntäjälle (koska ObjC:llä on rikkaat runtime-ominaisuudet ja kääntäjä ei voi turvallisesti suorittaa näitä optimointeja). Puhutaan siitä, mitä tapahtuu konepellin alla, kun kutsut jotakin metodia (käytän ”kutsua” ”lähettää viestin” sijasta, koska jälkimmäinen termi ei ole aina oikea Swiftissä). Swiftissä on kolme erilaista lähetystapaa (nopeammasta hitaampaan): staattinen, taulukkolähetys ja viestin lähettäminen. Jälkimmäinen on ainoa tyyppi, jota käytetään ObjC:ssä, ja sitä käytetään Swiftissä silloin, kun tarvitaan yhteentoimivuutta ObjC-koodin kanssa tai kun ilmoitamme metodin dynamic
:ksi. Tietenkin tämä osa runtimea on valtavasti optimoitu ja kirjoitettu assemblerilla kaikille alustoille. Mutta entä jos voisimme välttää tämän yleiskustannuksen, koska kääntäjällä on tietoa siitä, mitä tullaan kutsumaan kääntämisaikana? Juuri näin Swift-kääntäjä tekee staattisen ja taulukkodispatchin avulla. Staattinen dispatch on nopea, mutta kääntäjä ei voi käyttää sitä ilman 100-prosenttista luottamusta lausekkeen tyyppeihin. Mutta kun muuttujamme tyyppi on protokolla, kääntäjän on pakko käyttää dispatchia protokollan todistustaulukoiden kautta. Se ei ole kirjaimellisesti hidasta, mutta 1 ms täällä, 1 ms siellä, ja nyt kokonaissuoritusaika on yli sekunnin suurempi kuin mitä voisimme saavuttaa käyttämällä puhdasta Swift-koodia. Tämä kappale liittyy edelliseen protokollia koskevaan kappaleeseen, mutta mielestäni on parempi erottaa huolet vain holtittomasta määrästä käyttämättömiä protokollia ja todellisesta sotkemisesta kääntäjän kanssa sen avulla.
Heikko abstraktioiden erottelu
Pitäisi olla yksi – ja mieluiten vain yksi – ilmiselvä tapa tehdä se.
Yksi suosituimmista kysymyksistä VIPER-yhteisön keskuudessa on: ”Mihin minun pitäisi laittaa X?”. Toiselta puolelta on siis olemassa monia sääntöjä, miten asiat pitäisi tehdä oikein, mutta toiselta puolelta moni asia on mielipidepohjaista. Kyse voi olla monimutkaisista tapauksista, esimerkiksi CoreDatan käsittelystä NSFetchedResultsController
:llä tai UIWebView
:llä. Mutta myös yleiset tapaukset, kuten UIAlertController
:n käyttö, ovat keskustelun aihe. Katsotaanpa, tässä meillä on reititin, joka käsittelee hälytystä, mutta tuossa meillä on näkymä, joka esittää hälytyksen. Voit yrittää voittaa tämän väitteellä, että yksinkertainen hälytys on erikoistapaus yksinkertaiselle hälytykselle, jolla ei ole muita toimintoja kuin sulkeminen.
Erikoistapaukset eivät ole tarpeeksi erikoisia rikkoakseen sääntöjä.
Joo, mutta miksi meillä on tässä tehdas, jolla luodaan tuollaisia hälytyksiä? Meillä on siis sotku jopa näin yksinkertaisessa tapauksessa UIAlertController
. Haluatko sitä?
Koodin luominen
Lukukelpoisuus ratkaisee.
Miten tämä voi olla ongelma arkkitehtuurin kanssa? Se vain generoi kasan template-luokkia sen sijaan, että kirjoittaisin sen itse. Mikä siinä on ongelmana? Ongelma on se, että suurimman osan ajasta luet koodia (ellet työskentele jossain heitteillejättö-ulkoistusfirmassa), etkä kirjoita sitä. Luet siis suurimman osan ajasta boilerplate-koodia, joka on sekoitettu todelliseen koodiin. Onko se hyvä? En usko.
Johtopäätös
En tavoittele päämääränä lannistaa sinua käyttämästä VIPERiä lainkaan. On vielä voi olla joitakin sovelluksia, jotka voivat hyötyä kaikista. Ennen kuin aloitat sovelluksesi kehittämisen, sinun tulisi kuitenkin kysyä itseltäsi muutama kysymys:
- Onko tämän sovelluksen käyttöikä pitkä?
- Ovatko määrittelyt tarpeeksi vakaita? Vaihtoehtoisesti voit päätyä loputtomiin valtaviin refaktorointeihin jopa pienistä muutoksista.
- Testaatko todella sovelluksesi? Ole rehellinen itsellesi.
Vain jos vastaat kaikkiin kysymyksiin ”kyllä” VIPER voi olla hyvä valinta sovelluksellesi.
Viimeiseksi viimeinen: käytä omia aivojasi päätöksiäsi varten, älä vain luota sokeasti johonkin kaveriin Mediumissa tai konferenssissa johon osallistuit ja joka sanoo: ”Käytä X:ää, X on siisti.” Nämäkin tyypit voivat olla väärässä.