Az építészetben köztudott, hogy mi alakítjuk az épületeinket, és utána az épületeink alakítanak minket. Amint azt végül minden programozó megtanulja, ez ugyanúgy érvényes a szoftverek építésére is.
Fontos, hogy úgy tervezzük meg a kódunkat, hogy minden egyes darab könnyen azonosítható legyen, konkrét és nyilvánvaló célja legyen, és logikusan illeszkedjen a többi darabhoz. Ezt nevezzük szoftverarchitektúrának. A jó architektúra nem teszi sikeressé a terméket, de karbantarthatóvá teszi a terméket, és segít megőrizni a termék karbantartóinak épelméjűségét!
Ebben a cikkben az iOS-alkalmazások architektúrájának egy VIPER nevű megközelítését mutatjuk be. A VIPER-t már sok nagy projekt építéséhez használták, de e cikk céljaira a VIPER-t egy teendőlistás alkalmazás építésén keresztül mutatjuk be. A példaprojektet itt követheti végig a GitHubon:
Mi a VIPER?
A tesztelés nem mindig volt fontos része az iOS-alkalmazások építésének. Amikor a Mutual Mobile-nál belevágtunk a tesztelési gyakorlatunk javításába, azt tapasztaltuk, hogy az iOS-alkalmazások tesztjeinek megírása nehézkes. Úgy döntöttünk, hogy ha javítani akarunk a szoftvertesztelésünk módján, akkor először is ki kell találnunk egy jobb módszert az alkalmazásaink architektúrájára. Ezt a módszert VIPER-nek neveztük el.
A VIPER a tiszta architektúra alkalmazása iOS-alkalmazásokra. A VIPER szó a View, Interactor, Presenter, Entity és Routing rövidítése. A Clean Architecture egy alkalmazás logikai struktúráját különálló felelősségi rétegekre osztja. Ez megkönnyíti a függőségek (pl. az adatbázis) elkülönítését és a rétegek közötti határon lévő kölcsönhatások tesztelését:
A legtöbb iOS-alkalmazást MVC (model-view-controller) architektúrával építik fel. Az MVC mint alkalmazásarchitektúra használata arra vezethet, hogy azt gondoljuk, minden osztály vagy modell, nézet vagy vezérlő. Mivel az alkalmazás logikájának nagy része nem a modellbe vagy a nézetbe való, általában a vezérlőben köt ki. Ez a Massive View Controller néven ismert problémához vezet, amikor a nézetvezérlők végül túl sokat csinálnak. Ezeknek a masszív nézetvezérlőknek a karcsúsítása nem az egyetlen kihívás, amellyel a kódjuk minőségének javítására törekvő iOS-fejlesztők szembesülnek, de ez egy remek kiindulópont.
A VIPER különálló rétegei segítenek kezelni ezt a kihívást azáltal, hogy egyértelmű helyeket biztosítanak az alkalmazáslogikának és a navigációval kapcsolatos kódnak. A VIPER alkalmazásával észreveheti, hogy a teendőlistás példánkban szereplő nézetvezérlők karcsú, átlagos nézetvezérlő gépek. Azt is meg fogja tapasztalni, hogy a nézetvezérlők és az összes többi osztály kódja könnyen érthető, könnyebben tesztelhető, és ennek eredményeként könnyebben karbantartható.
A használati eseteken alapuló alkalmazástervezés
Az alkalmazásokat gyakran használati esetek halmazaként valósítják meg. A használati eseteket elfogadási kritériumoknak vagy viselkedésnek is nevezik, és leírják, hogy egy alkalmazásnak mit kell tennie. Lehet, hogy egy listának dátum, típus vagy név szerint kell rendezhetőnek lennie. Ez egy használati eset. A használati eset az alkalmazásnak az a rétege, amely az üzleti logikáért felelős. A használati eseteknek függetlennek kell lenniük a felhasználói felület megvalósításától. Kicsiknek és jól definiáltaknak is kell lenniük. Annak eldöntése, hogy egy összetett alkalmazást hogyan bontsunk kisebb használati esetekre, kihívást jelent és gyakorlatot igényel, de hasznos módja annak, hogy behatároljuk az egyes megoldandó problémák és az egyes megírandó osztályok terjedelmét.
A VIPER segítségével történő alkalmazásépítés magában foglalja az egyes használati esetek teljesítéséhez szükséges komponensek megvalósítását. Az alkalmazási logika egy használati eset megvalósításának fontos része, de nem az egyetlen része. A használati eset a felhasználói felületet is befolyásolja. Emellett fontos figyelembe venni, hogy a használati eset hogyan illeszkedik az alkalmazás más alapvető összetevőihez, például a hálózatépítéshez és az adatmegmaradáshoz. A komponensek úgy viselkednek, mint a használati esetek pluginjai, és a VIPER egy módja annak, hogy leírjuk, mi az egyes komponensek szerepe, és hogyan léphetnek kölcsönhatásba egymással.
A teendőlistás alkalmazásunk egyik használati esete vagy követelménye az volt, hogy a teendőket a felhasználó kiválasztása alapján különböző módon csoportosítsuk. Azáltal, hogy az adatokat rendszerező logikát egy használati esetté különválasztottuk, tisztán tudjuk tartani a felhasználói felület kódját, és könnyen tesztekbe csomagolhatjuk a használati esetet, hogy megbizonyosodjunk arról, hogy továbbra is úgy működik, ahogyan elvárjuk tőle.
A VIPER fő részei
A VIPER fő részei a következők:
- Nézet: megjeleníti, amit a bemutató mond neki, és továbbítja a felhasználói bemenetet vissza a bemutatóhoz.
- Interaktor: tartalmazza a használati eset által meghatározott üzleti logikát.
- Presenter: tartalmazza a nézeti logikát a tartalom megjelenítésre való előkészítéséhez (ahogyan azt az Interactortól kapja) és a felhasználói bemenetekre való reagáláshoz (új adatok kérésével az Interactortól).
- Entity: tartalmazza az Interactor által használt alapvető modellobjektumokat.
- Routing: tartalmazza a navigációs logikát annak leírására, hogy melyik képernyő milyen sorrendben jelenik meg.
Ez a szétválasztás megfelel az Egyetlen felelősség elvének is. Az Interactor az üzleti elemzőnek, a Presenter az interakciótervezőnek, a View pedig a vizuális tervezőnek felel.
Az alábbiakban egy ábra mutatja a különböző komponenseket és azok kapcsolódását:
Míg a VIPER összetevői bármilyen sorrendben megvalósíthatók egy alkalmazásban, mi úgy döntöttünk, hogy a komponenseket az általunk ajánlott sorrendben mutatjuk be. Észre fogja venni, hogy ez a sorrend nagyjából összhangban van a teljes alkalmazás építésének folyamatával, amely azzal kezdődik, hogy megbeszéljük, mit kell tennie a terméknek, majd azt, hogy a felhasználó hogyan fog vele kapcsolatba lépni.
Interaktor
Egy interaktor egyetlen felhasználási esetet képvisel az alkalmazásban. Tartalmazza a modellobjektumok (Entities) manipulálására szolgáló üzleti logikát egy adott feladat elvégzése érdekében. Az Interactorban végzett munkának függetlennek kell lennie a felhasználói felülettől. Ugyanaz az Interactor használható egy iOS-alkalmazásban vagy egy OS X-alkalmazásban.
Mivel az Interactor egy PONSO (Plain Old NSObject
), amely elsősorban logikát tartalmaz, könnyen fejleszthető TDD-vel.
A mintaalkalmazás elsődleges használati esete az, hogy megmutassa a felhasználónak a közelgő teendőket (azaz bármit, ami a jövő hét végéig esedékes). Ennek a felhasználási esetnek az üzleti logikája a ma és a jövő hét vége között esedékes teendők megkeresése és egy relatív esedékességi dátum hozzárendelése: ma, holnap, a hét végén vagy a jövő héten.
Az alábbiakban a VTDListInteractor
megfelelő metódusa látható:
- (void)findUpcomingItems{ __weak typeof(self) welf = self; NSDate* today = ; NSDate* endOfNextWeek = dateForEndOfFollowingWeekWithDate:today]; ]; }];}
Entity
Az entitások az Interactor által kezelt modellobjektumok. Az entitásokat csak az interaktor manipulálja. Az Interactor soha nem ad át entitásokat a prezentációs rétegnek (pl. Presenter).
Az entitások általában PONSO-k is. Ha Core Data-t használ, akkor azt szeretné, ha a kezelt objektumok az adatréteg mögött maradnának. Az interaktorok nem dolgozhatnak NSManagedObjects
-vel.
Itt az Entity a tennivalónkhoz:
@interface VTDTodoItem : NSObject@property (nonatomic, strong) NSDate* dueDate;@property (nonatomic, copy) NSString* name;+ (instancetype)todoItemWithDueDate:(NSDate*)dueDate name:(NSString*)name;@end
Ne lepődj meg, ha az entitások csak adatszerkezetek. Minden alkalmazásfüggő logika valószínűleg egy Interactorban lesz.
Presenter
A Presenter egy PONSO, amely főként a felhasználói felületet vezérlő logikából áll. Tudja, hogy mikor kell bemutatni a felhasználói felületet. Gyűjti a felhasználói interakciókból származó bemenetet, hogy frissíthesse a felhasználói felületet és kéréseket küldhessen egy Interactornak.
Amikor a felhasználó megérinti a + gombot egy új teendőelem hozzáadásához, a addNewEntry
meghívásra kerül. Ehhez a művelethez a Presenter felkéri a drótvázát, hogy mutassa be az új elem hozzáadásának UI-ját:
- (void)addNewEntry{ ;}
A Presenter is kap eredményeket egy Interactortól, és az eredményeket olyan formába konvertálja, amely hatékonyan megjeleníthető egy View-ban.
Az alábbi módszer fogadja a közelgő elemeket az Interactortól. Feldolgozza az adatokat és meghatározza, hogy mit jelenítsen meg a felhasználónak:
- (void)foundUpcomingItems:(NSArray*)upcomingItems{ if ( == 0) { ; } else { ; }}
Az elemek soha nem kerülnek átadásra az Interactortól a Presenterhez. Ehelyett egyszerű, viselkedés nélküli adatszerkezetek kerülnek átadásra az Interactortól a Presenter felé. Ez megakadályozza, hogy a Presenterben bármilyen “valódi munka” történjen. A bemutató csak előkészíteni tudja az adatokat a nézetben való megjelenítésre.
Nézet
A Nézet passzív. Várja, hogy a Bemutató megadja neki a megjelenítendő tartalmat; soha nem kér adatokat a Bemutatótól. A View-hoz definiált metódusoknak (pl. LoginView egy bejelentkezési képernyőhöz) lehetővé kell tenniük a Presenter számára, hogy magasabb absztrakciós szinten kommunikáljon, a tartalomban kifejezve, és nem abban, hogy ezt a tartalmat hogyan kell megjeleníteni. A Bemutató nem tud a UILabel
, UIButton
stb. létezéséről. A Bemutató csak az általa fenntartott tartalomról tud, és arról, hogy azt mikor kell megjeleníteni. A Nézet feladata meghatározni, hogy a tartalom hogyan jelenjen meg.
A Nézet egy absztrakt interfész, amelyet Objective-C-ben egy protokollal definiálnak. Egy UIViewController
vagy valamelyik alosztálya implementálja a View protokollt. Például a példánkban szereplő ‘add’ képernyőnek a következő interfésze van:
@protocol VTDAddViewInterface <NSObject>- (void)setEntryName:(NSString *)name;- (void)setEntryDueDate:(NSDate *)date;@end
A nézetek és a nézetvezérlők kezelik a felhasználói interakciót és a bevitelt is. Könnyen érthető, hogy a nézetvezérlők miért szoktak olyan nagyra nőni, hiszen ezeken a helyeken lehet a legkönnyebben kezelni ezt a bemenetet valamilyen művelet végrehajtásához. Ahhoz, hogy a nézetvezérlőink karcsúak maradjanak, módot kell adnunk nekik arra, hogy tájékoztassák az érdekelt feleket, amikor a felhasználó bizonyos műveleteket végez. A nézetvezérlőnek nem szabad döntéseket hoznia ezen akciók alapján, de tovább kell adnia ezeket az eseményeket valaminek, ami képes rá.
Példánkban az Add View Controller rendelkezik egy eseménykezelő tulajdonsággal, amely megfelel a következő interfésznek:
@protocol VTDAddModuleInterface <NSObject>- (void)cancelAddAction;- (void)saveAddActionWithName:(NSString *)name dueDate:(NSDate *)dueDate@end
Amikor a felhasználó rákattint a cancel gombra, a nézetvezérlő közli ezzel az eseménykezelővel, hogy a felhasználó jelezte, hogy törölni kell a hozzáadási műveletet. Így az eseménykezelő gondoskodhat az add view controller elvetéséről és a listanézet frissítésének utasításáról.
A View és a Presenter közötti határvonal szintén nagyszerű hely a ReactiveCocoa számára. Ebben a példában a nézetvezérlő is biztosíthatna metódusokat a gombok műveleteit reprezentáló jelek visszaadására. Ez lehetővé tenné a Presenter számára, hogy könnyen reagáljon ezekre a jelekre anélkül, hogy a felelősségek szétválasztása megszakadna.
Routing
Az egyik képernyőről a másikra vezető útvonalakat az interakciótervező által létrehozott drótvázakban határozzuk meg. A VIPER-ben az útválasztás felelőssége két objektum között oszlik meg: a bemutató és a drótváz között. A drótváz objektum birtokolja a UIWindow
, UINavigationController
, UIViewController
stb. Felelős a View/ViewController létrehozásáért és az ablakba való telepítéséért.
Mivel a Presenter tartalmazza a felhasználói bemenetekre reagáló logikát, a Presenter az, amely tudja, hogy mikor kell másik képernyőre navigálni, és melyik képernyőre kell navigálni. Eközben a drótváz tudja, hogyan kell navigálni. Tehát a Presenter a drótváz segítségével végzi el a navigációt. Együtt írják le az egyik képernyőről a másikra vezető útvonalat.
A drótváz szintén kézenfekvő hely a navigációs átmenet animációinak kezelésére. Nézzük meg ezt a példát az add wireframe-ből:
@implementation VTDAddWireframe- (void)presentAddInterfaceFromViewController:(UIViewController *)viewController { VTDAddViewController *addViewController = ; addViewController.eventHandler = self.addPresenter; addViewController.modalPresentationStyle = UIModalPresentationCustom; addViewController.transitioningDelegate = self; ; self.presentedViewController = viewController;}#pragma mark - UIViewControllerTransitioningDelegate Methods- (id<UIViewControllerAnimatedTransitioning>)animationControllerForDismissedController:(UIViewController *)dismissed { return init];}- (id<UIViewControllerAnimatedTransitioning>)animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source { return init];}@end
Az alkalmazás egy egyéni nézetvezérlő átmenetet használ az add view controller bemutatására. Mivel a drótváz felelős az átmenet végrehajtásáért, ez lesz az add view controller átmenetet végrehajtó delegátusa, és visszaadhatja a megfelelő átmenet animációkat.
A VIPER-hez illeszkedő alkalmazáskomponensek
Egy iOS-alkalmazás architektúrájának figyelembe kell vennie, hogy az UIKit és a Cocoa Touch a fő eszközök, amelyekre az alkalmazások épülnek. Az architektúrának békésen együtt kell léteznie az alkalmazás összes komponensével, de iránymutatást is kell adnia arra vonatkozóan, hogy a keretrendszerek egyes részeit hogyan használják, és hol helyezkednek el.
Az iOS-alkalmazás munkagépe a UIViewController
. Könnyű lenne azt feltételezni, hogy az MVC felváltására pályázó versenyző visszariadna a nézetvezérlők erőteljes felhasználásától. A nézetvezérlők azonban központi szerepet játszanak a platformban: kezelik a tájolásváltást, reagálnak a felhasználó bemenetére, jól integrálódnak az olyan rendszerkomponensekkel, mint a navigációs vezérlők, és most az iOS 7-tel lehetővé teszik a képernyők közötti testreszabható átmeneteket. Rendkívül hasznosak.
A VIPER-ben a nézetvezérlő pontosan azt teszi, amire hivatott: vezérli a nézetet. A teendőlistás alkalmazásunknak két nézetvezérlője van, egy a lista képernyőjéhez, és egy a hozzáadás képernyőjéhez. A hozzáadás nézetvezérlő implementációja rendkívül egyszerű, mert csak a nézetet kell vezérelnie:
@implementation VTDAddViewController- (void)viewDidAppear:(BOOL)animated { ; UITapGestureRecognizer *gestureRecognizer = initWithTarget:self action:@selector(dismiss)]; ; self.transitioningBackgroundView.userInteractionEnabled = YES;}- (void)dismiss { ;}- (void)setEntryName:(NSString *)name { self.nameTextField.text = name;}- (void)setEntryDueDate:(NSDate *)date { ;}- (IBAction)save:(id)sender { ;}- (IBAction)cancel:(id)sender { ;}#pragma mark - UITextFieldDelegate Methods- (BOOL)textFieldShouldReturn:(UITextField *)textField { ; return YES;}@end
Az alkalmazások általában sokkal meggyőzőbbek, ha a hálózathoz kapcsolódnak. De hol történjen ez a hálózatba kapcsolódás, és mi legyen a felelős a hálózatba kapcsolódás kezdeményezéséért? Általában az Interactor feladata a hálózati művelet kezdeményezése, de nem fogja közvetlenül kezelni a hálózati kódot. Egy függőséget fog felkérni, például egy hálózatkezelőt vagy API-klienst. Előfordulhat, hogy az Interactornak több forrásból származó adatokat kell összesítenie, hogy a használati eset teljesítéséhez szükséges információkat biztosítsa. Ezután a Presenter feladata, hogy az Interactor által visszaküldött adatokat átvegye és megjelenítésre formázza.
Az adattároló felelős azért, hogy az Interactor számára entitásokat biztosítson. Ahogy az interaktor alkalmazza az üzleti logikáját, le kell kérnie az entitásokat az adattárolóból, manipulálnia kell az entitásokat, majd a frissített entitásokat vissza kell helyeznie az adattárolóba. Az adattároló kezeli az entitások megőrzését. Az entitások nem tudnak az adattárolóról, így az entitások nem tudják, hogy miként állandósítsák magukat.
Az interaktornak sem szabad tudnia, hogyan kell az entitásokat tartósítani. Előfordulhat, hogy az Interaktor egy adatkezelőnek nevezett objektumtípust akar használni, hogy megkönnyítse az adattárolóval való interakcióját. Az adatkezelő kezeli a tároló-specifikus műveletek több típusát, például a lekérdezési kérések létrehozását, a lekérdezések felépítését stb. Ez lehetővé teszi, hogy az interaktor inkább az alkalmazás logikájára koncentráljon, és ne kelljen tudnia semmit arról, hogy az entitások hogyan kerülnek összegyűjtésre vagy tárolásra. Az egyik példa arra, hogy mikor van értelme adatmenedzsert használni, az alábbiakban ismertetett Core Data használata.
Itt van a példaalkalmazás adatkezelőjének felülete:
@interface VTDListDataManager : NSObject@property (nonatomic, strong) VTDCoreDataStore *dataStore;- (void)todoItemsBetweenStartDate:(NSDate *)startDate endDate:(NSDate *)endDate completionBlock:(void (^)(NSArray *todoItems))completionBlock;@end
Az Interactor fejlesztéséhez TDD-t használva lehetőség van arra, hogy a produktív adattárolót kicseréljük egy tesztdupla/mockra. Ha nem beszélünk egy távoli szerverrel (webszolgáltatás esetén) vagy nem érintjük meg a lemezt (adatbázis esetén), akkor a tesztek gyorsabbak és megismételhetőbbek lesznek.
Az egyik ok, amiért az adattárolót egyértelmű határokkal rendelkező, különálló rétegként kell tartani, az az, hogy lehetővé teszi, hogy késleltessük egy konkrét perzisztencia-technológia kiválasztását. Ha az adattároló egyetlen osztály, akkor az alkalmazást egy alapvető perszisztencia-stratégiával kezdheti, majd később SQLite-ra vagy Core Data-ra frissítheti, ha és amikor ennek értelme van, anélkül, hogy bármi mást megváltoztatna az alkalmazás kódbázisában.
A Core Data használata egy iOS-projektben gyakran több vitát vált ki, mint maga az architektúra. A Core Data használata a VIPER-rel azonban az eddigi legjobb Core Data-tapasztalat lehet. A Core Data nagyszerű eszköz az adatok perzisztálására, miközben gyors hozzáférést és alacsony memóriaigényt biztosít. De van egy olyan szokása, hogy a NSManagedObjectContext
indái végigkígyóznak az alkalmazás implementációs fájljain, különösen ott, ahol nem kellene. A VIPER a Core Data-t ott tartja, ahol lennie kell: az adattároló rétegben.
A feladatlista példában az alkalmazásnak csak két olyan része van, amely tudja, hogy Core Data használatban van: maga az adattároló, amely beállítja a Core Data stacket, és az adatkezelő. Az adatkezelő elvégzi a lekérdezést, az adattároló által visszaküldött NSManagedObjects
adatokat szabványos PONSO modellobjektumokká alakítja, és azokat visszaadja az üzleti logikai rétegnek. Így az alkalmazás magja soha nem függ a Core Data-tól, és bónuszként soha nem kell aggódnunk amiatt, hogy az elavult vagy rosszul szálazott NSManagedObjects
gunking up the works.
Íme, hogy néz ki az adatkezelőn belül, amikor egy kérés érkezik a Core Data tároló elérésére:
@implementation VTDListDataManager- (void)todoItemsBetweenStartDate:(NSDate *)startDate endDate:(NSDate*)endDate completionBlock:(void (^)(NSArray *todoItems))completionBlock{ NSCalendar *calendar = ; NSPredicate *predicate = , ]; NSArray *sortDescriptors = @; __weak typeof(self) welf = self; ); } }];}- (NSArray*)todoItemsFromDataStoreEntries:(NSArray *)entries{ return ; }];}@end
Majdnem olyan ellentmondásosak, mint a Core Data, a UI Storyboardok. A Storyboardok számos hasznos funkcióval rendelkeznek, és hiba lenne teljesen figyelmen kívül hagyni őket. Nehéz azonban úgy elérni a VIPER összes célját, hogy közben a storyboard által kínált összes funkciót alkalmazzuk.
A kompromisszum, amit hajlamosak vagyunk kötni, az az, hogy nem használunk segues-t. Lehetnek olyan esetek, amikor a szegue használatának van értelme, de a szegue-k veszélye az, hogy nagyon megnehezítik a képernyők közötti – valamint a felhasználói felület és az alkalmazás logikája közötti – elválasztás megtartását. Ökölszabályként igyekszünk nem használni a szegue-kat, ha a prepareForSegue metódus végrehajtása szükségesnek tűnik.
Minden más esetben a storyboardok nagyszerű megoldást jelentenek a felhasználói felület elrendezésének megvalósítására, különösen az automatikus elrendezés használata során. Mi úgy döntöttünk, hogy a teendőlistás példa mindkét képernyőjét storyboard segítségével valósítjuk meg, és ilyen kódot használunk a saját navigációnk végrehajtásához:
static NSString *ListViewControllerIdentifier = @"VTDListViewController";@implementation VTDListWireframe- (void)presentListInterfaceFromWindow:(UIWindow *)window { VTDListViewController *listViewController = ; listViewController.eventHandler = self.listPresenter; self.listPresenter.userInterface = listViewController; self.listViewController = listViewController; ;}- (VTDListViewController *)listViewControllerFromStoryboard { UIStoryboard *storyboard = ; VTDListViewController *viewController = ; return viewController;}- (UIStoryboard *)mainStoryboard { UIStoryboard *storyboard = ]; return storyboard;}@end
A VIPER használata modulok építéséhez
A VIPER-rel való munka során gyakran tapasztalhatjuk, hogy egy képernyő vagy képernyőcsoport hajlamos modulként összeállni. Egy modul többféleképpen is leírható, de általában a legjobb, ha úgy gondolunk rá, mint egy funkcióra. Egy podcasting-alkalmazásban a modul lehet a hanglejátszó vagy az előfizetéses böngésző. A teendőlistás alkalmazásunkban a lista és a hozzáadás képernyője különálló modulokként épül fel.
Az alkalmazás modulok halmazaként való megtervezésének van néhány előnye. Az egyik, hogy a modulok nagyon világos és jól definiált interfésszel rendelkezhetnek, valamint függetlenek lehetnek más moduloktól. Ezáltal sokkal könnyebbé válik a funkciók hozzáadása/eltávolítása, vagy annak megváltoztatása, ahogyan a felületünk a különböző modulokat a felhasználó számára megjeleníti.
A tennivalólista példájában nagyon világossá akartuk tenni a modulok közötti elkülönítést, ezért két protokollt definiáltunk az add modul számára. Az első a modul interfész, amely meghatározza, hogy a modul mire képes. A második a moduldelegátus, amely leírja, hogy mit csinált a modul. Példa:
@protocol VTDAddModuleInterface <NSObject>- (void)cancelAddAction;- (void)saveAddActionWithName:(NSString *)name dueDate:(NSDate *)dueDate;@end@protocol VTDAddModuleDelegate <NSObject>- (void)addModuleDidCancelAddAction;- (void)addModuleDidSaveAddAction;@end
Mivel egy modult be kell mutatni ahhoz, hogy a felhasználó számára nagy értéket képviseljen, a modul bemutatója általában a modul interfészt valósítja meg. Amikor egy másik modul szeretné ezt a modult bemutatni, a Presenter implementálja a modul delegate protokollt, hogy tudja, mit csinált a modul a bemutatás során.
A modul tartalmazhat egy közös alkalmazási logikai réteget, amely entitásokból, interaktorokból és menedzserekből áll, és amely több képernyőn is használható. Ez természetesen attól függ, hogy ezek a képernyők milyen kölcsönhatásban vannak egymással, és mennyire hasonlóak. Egy modul ugyanilyen könnyen képviselhet egyetlen képernyőt is, ahogyan azt a teendőlistás példa mutatja. Ebben az esetben az alkalmazás logikai rétege nagyon specifikus lehet az adott modul viselkedésére.
A modulok csak egy jó egyszerű módját jelentik a kód szervezésének. Ha egy modul összes kódját saját mappában és csoportban tartjuk az Xcode-ban, akkor könnyen megtaláljuk, ha valamit meg kell változtatnunk. Nagyszerű érzés, amikor egy osztályt pontosan ott találunk, ahol azt vártuk, hogy keressük.
A modulok VIPER-rel történő építésének további előnye, hogy könnyebbé válik a modulok kiterjesztése többféle formátumra. Ha az összes felhasználási eset alkalmazási logikáját az Interactor rétegben izoláljuk, akkor az alkalmazási réteg újrafelhasználása mellett az új felhasználói felület táblagépre, telefonra vagy Macre történő építésére koncentrálhatunk.
Egy lépéssel továbblépve, az iPad alkalmazások felhasználói felülete újra felhasználhatja az iPhone alkalmazás egyes nézeteit, nézetvezérlőit és bemutatóit. Ebben az esetben az iPad képernyőjét “szuper” prezenterek és drótvázak képviselnék, amelyek az iPhone-ra írt meglévő prezenterek és drótvázak felhasználásával állítanák össze a képernyőt. Egy alkalmazás létrehozása és karbantartása több platformon is nagy kihívást jelenthet, de a jó architektúra, amely elősegíti az újrafelhasználást a modell- és az alkalmazási rétegben, nagyban megkönnyíti ezt.
Tesztelés a VIPER-rel
A VIPER követése ösztönzi a gondok elkülönítését, ami megkönnyíti a TDD elfogadását. Az interaktor tiszta logikát tartalmaz, amely független bármilyen felhasználói felülettől, ami megkönnyíti a tesztekkel való vezetést. A Presenter logikát tartalmaz az adatok megjelenítésre való előkészítésére, és független az UIKit widgetektől. Ennek a logikának a fejlesztése szintén könnyen vezethető tesztekkel.
Az általunk preferált módszer az, hogy az Interactorral kezdjük. Az UI-ban minden azért van, hogy a használati eset igényeit szolgálja. Ha TDD-vel teszteljük az Interactor API-ját, jobban megértjük a felhasználói felület és a használati eset közötti kapcsolatot.
Példaként a következő teendők listájáért felelős Interactort fogjuk megvizsgálni. A közelgő elemek keresésére szolgáló irányelv a következő hét végéig esedékes összes teendőelem keresése, és minden egyes teendőelem besorolása úgy, hogy ma, holnap, a hét végén vagy a jövő héten esedékes.
Az első teszt, amit írunk, annak biztosítása, hogy az interaktor megtalálja az összes jövő hét végéig esedékes tennivalót:
- (void)testFindingUpcomingItemsRequestsAllToDoItemsFromTodayThroughEndOfNextWeek{ todoItemsBetweenStartDate:self.today endDate:self.endOfNextWeek completionBlock:OCMOCK_ANY]; ;}
Amikor már tudjuk, hogy az interaktor a megfelelő tennivalókat kéri, több tesztet írunk annak megerősítésére, hogy a tennivalókat a megfelelő relatív dátumcsoportba sorolja (pl.pl. ma, holnap stb.):
- (void)testFindingUpcomingItemsWithOneItemDueTodayReturnsOneUpcomingItemsForToday{ NSArray *todoItems = @]; ; NSArray *upcomingItems = @]; ; ;}
Most, hogy már tudjuk, hogyan néz ki az Interactor API-ja, fejleszthetjük a Presentert. Amikor a Presenter megkapja a közelgő tennivalókat az Interactortól, tesztelni akarjuk, hogy megfelelően formázzuk az adatokat és megjelenítjük a felhasználói felületen:
- (void)testFoundZeroUpcomingItemsDisplaysNoContentMessage{ showNoContentMessage]; ];}- (void)testFoundUpcomingItemForTodayDisplaysUpcomingDataWithNoDay{ VTDUpcomingDisplayData *displayData = ; showUpcomingDisplayData:displayData]; NSCalendar *calendar = ; NSDate *dueDate = ; VTDUpcomingItem *haircut = ; ];}- (void)testFoundUpcomingItemForTomorrowDisplaysUpcomingDataWithDay{ VTDUpcomingDisplayData *displayData = ; showUpcomingDisplayData:displayData]; NSCalendar *calendar = ; NSDate *dueDate = ; VTDUpcomingItem *groceries = ; ];}
Tesztelni akarjuk azt is, hogy az alkalmazás elindítja a megfelelő műveletet, amikor a felhasználó új tennivalót akar hozzáadni:
- (void)testAddNewToDoItemActionPresentsAddToDoUI{ presentAddInterface]; ;}
Most már fejleszthetjük a View-t. Ha nincsenek közelgő teendők, akkor egy speciális üzenetet akarunk megjeleníteni:
- (void)testShowingNoContentMessageShowsNoContentView{ ; XCTAssertEqualObjects(self.view.view, self.view.noContentView, @"the no content view should be the view");}
Ha vannak megjelenítendő közelgő teendők, akkor meg akarjuk győződni arról, hogy a táblázat megjelenik:
- (void)testShowingUpcomingItemsShowsTableView{ ; XCTAssertEqualObjects(self.view.view, self.view.tableView, @"the table view should be the view");}
Az interaktort először megépíteni természetes a TDD-hez. Ha először az Interactort, majd a Presentert fejlesztjük, akkor először ezek köré a rétegek köré építhetünk ki egy tesztcsomagot, és megalapozhatjuk a használati esetek megvalósítását. Gyorsan iterálhatsz ezeken az osztályokon, mivel a tesztelésükhöz nem kell interakcióba lépned a felhasználói felülettel. Ezután, amikor a nézetet fejleszteni kezded, már lesz egy működő és tesztelt logikai és megjelenítési réteged, amelyhez csatlakozhatsz. Mire befejezi a Nézet fejlesztését, lehet, hogy az alkalmazás első futtatásakor minden egyszerűen működik, mert az összes sikeres tesztje azt mondja, hogy működni fog.
Következtetés
Reméljük, hogy tetszett ez a bevezetés a VIPER-be. Sokan most talán azon tűnődnek, hogy merre tovább. Ha a következő alkalmazásukat a VIPER segítségével akarnák megtervezni, hol kezdenék?
Ez a cikk és a VIPER-t használó alkalmazásunk példaimplementációja olyan konkrét és jól körülhatárolt, amennyire csak lehetett. A teendőlistás alkalmazásunk meglehetősen egyszerű, de ennek is pontosan el kell magyaráznia, hogyan lehet egy alkalmazást a VIPER használatával létrehozni. Egy valós projektben az, hogy mennyire követi ezt a példát, a saját kihívásaitól és korlátaitól függ. Tapasztalataink szerint minden egyes projektünkben némileg változott a VIPER használatának megközelítése, de mindegyiküknek nagy hasznára vált, ha a VIPER-t használták a megközelítéseik irányításához.
Elképzelhető, hogy különböző okokból el kíván térni a VIPER által kijelölt útvonaltól. Talán “nyuszi” objektumok warrenjébe futott bele, vagy az alkalmazásának előnyére válna a Storyboardsban az átmenetek használata. Ez rendben van. Ezekben az esetekben a döntés meghozatalakor vegye figyelembe annak szellemét, amit a VIPER képvisel. Alapjában véve a VIPER egy olyan architektúra, amely az Egyetlen felelősség elvén alapul. Ha gondjai vannak, gondoljon erre az elvre, amikor eldönti, hogyan lépjen tovább.
Az is felmerülhet önben, hogy lehetséges-e a VIPER használata a meglévő alkalmazásában. Ebben a forgatókönyvben fontolja meg egy új funkció létrehozását a VIPER segítségével. Számos meglévő projektünk ezt az utat választotta. Ez lehetővé teszi, hogy egy modult a VIPER használatával építsen, és segít észrevenni a meglévő problémákat is, amelyek megnehezíthetik az Egyetlen felelősség elvén alapuló architektúra elfogadását.
A szoftverfejlesztés egyik nagyszerű tulajdonsága, hogy minden alkalmazás más és más, és minden alkalmazás architektúrájának is különböző módjai vannak. Számunkra ez azt jelenti, hogy minden alkalmazás egy új lehetőség a tanulásra és új dolgok kipróbálására. Ha úgy döntesz, hogy kipróbálod a VIPER-t, úgy gondoljuk, hogy te is tanulni fogsz néhány új dolgot. Köszönjük, hogy elolvastad.
Swift kiegészítés
A múlt héten a WWDC-n az Apple bemutatta a Swift programozási nyelvet, mint a Cocoa és a Cocoa Touch fejlesztés jövőjét. Még túl korai lenne komplex véleményt alkotni a Swift nyelvről, de azt tudjuk, hogy a nyelvek nagy hatással vannak arra, hogyan tervezzük és építjük a szoftvereket. Úgy döntöttünk, hogy újraírjuk a VIPER TODO példaalkalmazásunkat Swift használatával, hogy megtudjuk, mit jelent ez a VIPER számára. Eddig tetszik, amit látunk. Íme a Swift néhány olyan jellemzője, amelyről úgy érezzük, hogy javítani fogja az alkalmazások VIPER használatával történő építésének élményét.
Structs
A VIPER-ben kis, könnyű modellosztályokat használunk a rétegek közötti adatátadásra, például a Presenterről a View-ra. Ezek a PONSO-k általában csak kis mennyiségű adat szállítására szolgálnak, és általában nem alosztályozhatók. A Swift struktúrák tökéletesen megfelelnek ezekre a helyzetekre. Íme egy példa a VIPER Swift példában használt struktúrára. Vegyük észre, hogy ennek a structnak egyenértékűnek kell lennie, ezért az == operátort túlterheltük a típusának két példányának összehasonlítására:
struct UpcomingDisplayItem : Equatable, Printable { let title : String = "" let dueDate : String = "" var description : String { get { return "\(title) -- \(dueDate)" }} init(title: String, dueDate: String) { self.title = title self.dueDate = dueDate }}func == (leftSide: UpcomingDisplayItem, rightSide: UpcomingDisplayItem) -> Bool { var hasEqualSections = false hasEqualSections = rightSide.title == leftSide.title if hasEqualSections == false { return false } hasEqualSections = rightSide.dueDate == rightSide.dueDate return hasEqualSections}
Típusbiztonság
Talán a legnagyobb különbség az Objective-C és a Swift között az, ahogy a kettő a típusokat kezeli. Az Objective-C dinamikusan tipizált, a Swift pedig szándékosan nagyon szigorú azzal, ahogyan a típusellenőrzést fordítási időben megvalósítja. Egy olyan architektúra esetében, mint a VIPER, ahol egy alkalmazás több különböző rétegből áll, a típusbiztonság hatalmas nyereség lehet a programozók hatékonysága és az architektúra struktúrája szempontjából. A fordító segít meggyőződni arról, hogy a tárolók és objektumok a megfelelő típusúak, amikor a réteghatárok között átadjuk őket. Ez egy nagyszerű hely a fentiekben bemutatott structok használatára. Ha egy struktúrának két réteg közötti határon kell élnie, akkor a típusbiztonságnak köszönhetően garantálhatjuk, hogy soha nem fog tudni kiszabadulni a rétegek között.
További olvasmányok
- VIPER TODO, cikk példalehetőség
- VIPER SWIFT, cikk Swift segítségével épített példalehetőség
- Counter, egy másik példa alkalmazás
- Mutual Mobile Bevezetés a VIPER-be
- Clean Architecture
- Lighter View Controllers
- Testing View Controllers
- Bunies