Este bine cunoscut în domeniul arhitecturii faptul că noi ne modelăm clădirile, iar apoi clădirile ne modelează pe noi. Așa cum toți programatorii învață în cele din urmă, acest lucru se aplică la fel de bine la construirea de software.
Este important să ne proiectăm codul astfel încât fiecare piesă să fie ușor de identificat, să aibă un scop specific și evident și să se potrivească împreună cu alte piese într-un mod logic. Aceasta este ceea ce numim arhitectură software. O arhitectură bună nu este ceea ce face ca un produs să aibă succes, dar face ca un produs să poată fi întreținut și ajută la păstrarea sănătății mintale a persoanelor care îl întrețin!
În acest articol, vom prezenta o abordare a arhitecturii aplicațiilor iOS numită VIPER. VIPER a fost folosit pentru a construi multe proiecte mari, dar în scopul acestui articol vă vom arăta VIPER prin construirea unei aplicații de tip to-do list. Puteți urmări împreună cu proiectul de exemplu aici pe GitHub:
- Ce este VIPER?
- Proiectarea aplicației pe baza cazurilor de utilizare
- Părți principale ale VIPER
- Interactor
- Entity
- Prezentator
- View
- Routing
- Componente de aplicații care se potrivesc cu VIPER
- Utilizarea VIPER pentru a construi module
- Testing with VIPER
- Concluzie
- Addenda la Swift
- Structuri
- Siguranța tipurilor
- Lecturi suplimentare
Ce este VIPER?
Testarea nu a fost întotdeauna o parte importantă a construirii aplicațiilor iOS. Pe măsură ce am pornit în încercarea de a ne îmbunătăți practicile de testare la Mutual Mobile, am constatat că scrierea de teste pentru aplicațiile iOS era dificilă. Am decis că, dacă aveam de gând să îmbunătățim modul în care ne testăm software-ul, va trebui mai întâi să găsim un mod mai bun de a ne arhitectura aplicațiile. Noi numim această metodă VIPER.
VIPER este o aplicație a arhitecturii curate la aplicațiile iOS. Cuvântul VIPER este un backronim pentru View, Interactor, Presenter, Entity și Routing. Clean Architecture împarte structura logică a unei aplicații în straturi distincte de responsabilitate. Acest lucru facilitează izolarea dependențelor (de exemplu, baza de date) și testarea interacțiunilor la granițele dintre straturi:
Majoritatea aplicațiilor iOS sunt arhitecturate folosind MVC (model-view-controller). Utilizarea MVC ca arhitectură a aplicației vă poate ghida să credeți că fiecare clasă este fie un model, fie o vizualizare, fie un controler. Având în vedere că o mare parte din logica aplicației nu aparține unui model sau unei vizualizări, aceasta ajunge de obicei în controler. Acest lucru duce la o problemă cunoscută sub numele de Massive View Controller, în care controlorii de vizualizare ajung să facă prea multe. Reducerea acestor controllere de vizualizare masive nu este singura provocare cu care se confruntă dezvoltatorii iOS care caută să îmbunătățească calitatea codului lor, dar este un punct de plecare excelent.
Capacele distincte ale lui VIPER ajută la rezolvarea acestei provocări, oferind locații clare pentru logica aplicației și codul legat de navigare. Cu VIPER aplicat, veți observa că controlorii de vizualizare din exemplul nostru cu lista de lucruri de făcut sunt mașini de control al vizualizărilor, slabe și eficiente. Veți observa, de asemenea, că codul din controlorii de vizualizare și din toate celelalte clase este ușor de înțeles, mai ușor de testat și, ca urmare, și mai ușor de întreținut.
Proiectarea aplicației pe baza cazurilor de utilizare
Aplicațiile sunt adesea implementate ca un set de cazuri de utilizare. Cazurile de utilizare sunt, de asemenea, cunoscute sub numele de criterii de acceptare sau comportamente și descriu ceea ce o aplicație este menită să facă. Poate că o listă trebuie să poată fi sortată după dată, tip sau nume. Acesta este un caz de utilizare. Un caz de utilizare este stratul unei aplicații care este responsabil pentru logica de afaceri. Cazurile de utilizare ar trebui să fie independente de implementarea interfeței cu utilizatorul a acestora. De asemenea, acestea ar trebui să fie mici și bine definite. Să decideți cum să împărțiți o aplicație complexă în cazuri de utilizare mai mici este o provocare și necesită practică, dar este o modalitate utilă de a limita domeniul de aplicare a fiecărei probleme pe care o rezolvați și a fiecărei clase pe care o scrieți.
Construirea unei aplicații cu VIPER implică implementarea unui set de componente pentru a îndeplini fiecare caz de utilizare. Logica aplicației este o parte importantă a implementării unui caz de utilizare, dar nu este singura parte. Cazul de utilizare afectează, de asemenea, interfața cu utilizatorul. În plus, este important să se ia în considerare modul în care cazul de utilizare se potrivește cu alte componente de bază ale unei aplicații, cum ar fi rețeaua și persistența datelor. Componentele acționează ca niște plugin-uri pentru cazurile de utilizare, iar VIPER este o modalitate de a descrie care este rolul fiecăreia dintre aceste componente și cum pot interacționa unele cu altele.
Unul dintre cazurile de utilizare sau cerințele pentru aplicația noastră de listă de sarcini a fost să grupeze sarcinile în moduri diferite în funcție de selecția unui utilizator. Prin separarea logicii care organizează aceste date într-un caz de utilizare, suntem capabili să păstrăm codul interfeței cu utilizatorul curat și să înglobăm cu ușurință cazul de utilizare în teste pentru a ne asigura că acesta continuă să funcționeze așa cum ne așteptăm.
Părți principale ale VIPER
Părțile principale ale VIPER sunt:
- Vizualizare: afișează ceea ce i se spune de către prezentator și retransmite datele de intrare ale utilizatorului către prezentator.
- Interactor: conține logica de afaceri așa cum este specificată de un caz de utilizare.
- Prezentator: conține logica de vizualizare pentru a pregăti conținutul pentru afișare (așa cum este primit de la Interactor) și pentru a reacționa la intrările utilizatorului (solicitând noi date de la Interactor).
- Entitate: conține obiecte de model de bază utilizate de Interactor.
- Rutare: conține logica de navigare pentru a descrie ce ecrane sunt afișate în ce ordine.
Această separare este, de asemenea, conformă cu principiul responsabilității unice. Interactorul este responsabil față de analistul de afaceri, prezentatorul reprezintă proiectantul de interacțiune, iar vizualizarea este responsabilă față de proiectantul vizual.
Mai jos este o diagramă a diferitelor componente și a modului în care acestea sunt conectate:
În timp ce componentele VIPER pot fi implementate într-o aplicație în orice ordine, am ales să prezentăm componentele în ordinea în care recomandăm implementarea lor. Veți observa că această ordine este aproximativ în concordanță cu procesul de construire a unei întregi aplicații, care începe cu discutarea a ceea ce trebuie să facă produsul, urmată de modul în care un utilizator va interacționa cu acesta.
Interactor
Un interactor reprezintă un singur caz de utilizare în aplicație. Acesta conține logica de afaceri pentru a manipula obiectele modelului (Entități) pentru a îndeplini o sarcină specifică. Activitatea desfășurată într-un Interactor ar trebui să fie independentă de orice interfață de utilizator. Același Interactor ar putea fi utilizat într-o aplicație iOS sau într-o aplicație OS X.
Pentru că Interactorul este un PONSO (Plain Old NSObject
) care conține în primul rând logică, este ușor de dezvoltat utilizând TDD.
Cazul de utilizare principal pentru aplicația de exemplu este de a arăta utilizatorului orice elemente viitoare de făcut (de exemplu, orice lucru care trebuie făcut până la sfârșitul săptămânii viitoare). Logica de afaceri pentru acest caz de utilizare este de a găsi orice lucru de făcut care trebuie făcut între astăzi și sfârșitul săptămânii viitoare și de a atribui o dată de scadență relativă: astăzi, mâine, mai târziu în această săptămână sau săptămâna viitoare.
Mai jos este metoda corespunzătoare din VTDListInteractor
:
- (void)findUpcomingItems{ __weak typeof(self) welf = self; NSDate* today = ; NSDate* endOfNextWeek = dateForEndOfFollowingWeekWithDate:today]; ]; }];}
Entity
Entitățile sunt obiectele de model manipulate de un Interactor. Entitățile sunt manipulate numai de către Interactor. Interactorul nu transmite niciodată entitățile către stratul de prezentare (adică către prezentator).
Entitățile tind, de asemenea, să fie PONSO. Dacă utilizați Core Data, veți dori ca obiectele gestionate să rămână în spatele stratului de date. Interactorii nu ar trebui să lucreze cu NSManagedObjects
.
Iată Entitatea pentru elementul nostru de făcut:
@interface VTDTodoItem : NSObject@property (nonatomic, strong) NSDate* dueDate;@property (nonatomic, copy) NSString* name;+ (instancetype)todoItemWithDueDate:(NSDate*)dueDate name:(NSString*)name;@end
Nu fiți surprins dacă entitățile dvs. sunt doar structuri de date. Orice logică dependentă de aplicație se va afla, cel mai probabil, într-un Interactor.
Prezentator
Prezentatorul este un PONSO care constă în principal din logica de acționare a interfeței cu utilizatorul. Acesta știe când să prezinte interfața cu utilizatorul. Colectează datele de intrare de la interacțiunile utilizatorului, astfel încât să poată actualiza interfața de interfață și să trimită cereri către un interactor.
Când utilizatorul atinge butonul + pentru a adăuga un nou element de făcut, este apelat addNewEntry
. Pentru această acțiune, prezentatorul cere wireframe-ului să prezinte interfața de utilizare pentru adăugarea unui nou element:
- (void)addNewEntry{ ;}
Prezentatorul primește, de asemenea, rezultate de la un Interactor și convertește rezultatele într-un formular care este eficient pentru a fi afișat într-o vizualizare.
Mai jos este metoda care primește elementele viitoare de la Interactor. Aceasta va procesa datele și va determina ce să arate utilizatorului:
- (void)foundUpcomingItems:(NSArray*)upcomingItems{ if ( == 0) { ; } else { ; }}
Entitățile nu sunt niciodată transmise de la Interactor la Prezentator. În schimb, structurile de date simple care nu au niciun comportament sunt transmise de la Interactor la Prezentator. Acest lucru împiedică orice „muncă reală” să fie efectuată în prezentator. Prezentatorul poate doar să pregătească datele pentru afișare în View.
View
Visualizarea este pasivă. Așteaptă ca prezentatorul să îi ofere conținut pentru a-l afișa; nu cere niciodată date prezentatorului. Metodele definite pentru un View (de exemplu, LoginView pentru un ecran de autentificare) ar trebui să permită unui Presenter să comunice la un nivel mai ridicat de abstractizare, exprimat în termeni de conținut, și nu de modul în care acest conținut trebuie afișat. Prezentatorul nu știe despre existența lui UILabel
, UIButton
, etc. Prezentatorul știe doar despre conținutul pe care îl păstrează și despre momentul în care acesta trebuie afișat. Depinde de View să determine modul în care este afișat conținutul.
View este o interfață abstractă, definită în Objective-C cu un protocol. O UIViewController
sau una dintre subclasele sale va implementa protocolul View. De exemplu, ecranul „add” din exemplul nostru are următoarea interfață:
@protocol VTDAddViewInterface <NSObject>- (void)setEntryName:(NSString *)name;- (void)setEntryDueDate:(NSDate *)date;@end
Vizualizările și controlorii de vizualizare gestionează, de asemenea, interacțiunea și intrările utilizatorului. Este ușor de înțeles de ce controlorii de vizualizare devin, de obicei, atât de mari, deoarece sunt locul cel mai ușor de a gestiona această intrare pentru a efectua o anumită acțiune. Pentru a ne menține controlorii de vizualizare slabi, trebuie să le oferim o modalitate de a informa părțile interesate atunci când un utilizator efectuează anumite acțiuni. Controlerul de vizualizare nu ar trebui să ia decizii pe baza acestor acțiuni, dar ar trebui să transmită aceste evenimente către ceva care poate face acest lucru.
În exemplul nostru, controlerul de vizualizare Add are o proprietate event handler care se conformează următoarei interfețe:
@protocol VTDAddModuleInterface <NSObject>- (void)cancelAddAction;- (void)saveAddActionWithName:(NSString *)name dueDate:(NSDate *)dueDate@end
Când utilizatorul apasă pe butonul de anulare, controlerul de vizualizare îi spune acestui event handler că utilizatorul a indicat că ar trebui să anuleze acțiunea de adăugare. În acest fel, gestionarul de evenimente se poate ocupa de respingerea controlerului de vizualizare a adăugării și de a-i spune vizualizării listei să se actualizeze.
Limita dintre View și Presenter este, de asemenea, un loc excelent pentru ReactiveCocoa. În acest exemplu, controlerul de vizualizare ar putea, de asemenea, să furnizeze metode pentru a returna semnale care să reprezinte acțiunile butoanelor. Acest lucru ar permite prezentatorului să răspundă cu ușurință la aceste semnale fără a rupe separarea responsabilităților.
Routing
Rutele de la un ecran la altul sunt definite în wireframe-urile create de un designer de interacțiune. În VIPER, responsabilitatea pentru rutare este împărțită între două obiecte: prezentatorul și wireframe-ul. Un obiect de tip wireframe deține UIWindow
, UINavigationController
, UIViewController
, UIViewController
, etc. Acesta este responsabil de crearea unui View/ViewController și de instalarea acestuia în fereastră.
Din moment ce prezentatorul conține logica pentru a reacționa la intrările utilizatorului, prezentatorul este cel care știe când să navigheze către un alt ecran și către care ecran să navigheze. Între timp, wireframe-ul știe cum să navigheze. Așadar, prezentatorul va folosi wireframe-ul pentru a efectua navigarea. Împreună, acestea descriu un traseu de la un ecran la altul.
Cadrul de sârmă este, de asemenea, un loc evident pentru a gestiona animațiile de tranziție de navigare. Aruncați o privire la acest exemplu din wireframe-ul de adăugare:
@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
Aplicația utilizează o tranziție de controler de vizualizare personalizată pentru a prezenta controlerul de vizualizare de adăugare. Deoarece wireframe-ul este responsabil pentru efectuarea tranziției, acesta devine delegatul de tranziție pentru controlerul de vizualizare add și poate returna animațiile de tranziție corespunzătoare.
Componente de aplicații care se potrivesc cu VIPER
Arhitectura unei aplicații iOS trebuie să țină cont de faptul că UIKit și Cocoa Touch sunt principalele instrumente pe care sunt construite aplicațiile. Arhitectura trebuie să coexiste în mod pașnic cu toate componentele aplicației, dar trebuie, de asemenea, să ofere linii directoare pentru modul în care sunt utilizate unele părți ale cadrelor și unde locuiesc acestea.
Cetatea de bază a unei aplicații iOS este UIViewController
. Ar fi ușor să presupunem că un concurent care să înlocuiască MVC s-ar feri să folosească intensiv controlorii de vizualizare. Dar controlorii de vizualizare sunt esențiali pentru platformă: se ocupă de schimbarea orientării, răspund la intrările din partea utilizatorului, se integrează bine cu componentele de sistem, cum ar fi controlorii de navigare, iar acum, odată cu iOS 7, permit tranziții personalizabile între ecrane. Ele sunt extrem de utile.
Cu VIPER, un controler de vizualizare face exact ceea ce a fost menit să facă: controlează vizualizarea. Aplicația noastră cu lista de sarcini are doi controlori de vizualizare, unul pentru ecranul de listă și unul pentru ecranul de adăugare. Implementarea controlerului de vizualizare de adăugare este extrem de simplă, deoarece tot ce trebuie să facă este să controleze vizualizarea:
@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
Aplicațiile sunt, de obicei, mult mai convingătoare atunci când sunt conectate la rețea. Dar unde ar trebui să aibă loc această conectare în rețea și ce ar trebui să fie responsabil pentru inițierea ei? De obicei, este de competența interactorului să inițieze o operațiune de rețea, dar nu se va ocupa direct de codul de rețea. Acesta va solicita o dependență, cum ar fi un manager de rețea sau un client API. Este posibil ca Interactor să fie nevoit să adune date din mai multe surse pentru a furniza informațiile necesare pentru a îndeplini un caz de utilizare. Apoi depinde de prezentator să preia datele returnate de către interactor și să le formateze pentru prezentare.
Un magazin de date este responsabil pentru furnizarea de entități unui interactor. Pe măsură ce un Interactor își aplică logica de afaceri, acesta va trebui să recupereze entități din stocul de date, să manipuleze entitățile și apoi să pună entitățile actualizate înapoi în stocul de date. Depozitul de date gestionează persistența entităților. Entitățile nu știu despre stocul de date, astfel încât entitățile nu știu cum să se mențină pe ele însele.
Nici Interactorul nu ar trebui să știe cum să persiste entitățile. Uneori, interactorul poate dori să utilizeze un tip de obiect numit manager de date pentru a facilita interacțiunea sa cu depozitul de date. Managerul de date gestionează mai multe tipuri de operațiuni specifice magazinului, cum ar fi crearea de cereri de preluare, construirea de interogări etc. Acest lucru îi permite interactorului să se concentreze mai mult pe logica aplicației și să nu fie nevoit să știe nimic despre modul în care sunt colectate sau păstrate entitățile. Un exemplu de situație în care are sens să folosiți un manager de date este atunci când utilizați Core Data, care este descris mai jos.
Iată interfața pentru managerul de date al aplicației de exemplu:
@interface VTDListDataManager : NSObject@property (nonatomic, strong) VTDCoreDataStore *dataStore;- (void)todoItemsBetweenStartDate:(NSDate *)startDate endDate:(NSDate *)endDate completionBlock:(void (^)(NSArray *todoItems))completionBlock;@end
Când folosiți TDD pentru a dezvolta un Interactor, este posibil să schimbați depozitul de date de producție cu un dublu/mock de testare. Faptul că nu vorbiți cu un server de la distanță (pentru un serviciu web) sau că nu atingeți discul (pentru o bază de date) permite ca testele dvs. să fie mai rapide și mai ușor de repetat.
Un motiv pentru a păstra stocul de date ca un strat distinct cu limite clare este că vă permite să amânați alegerea unei tehnologii specifice de persistență. Dacă magazinul de date este o singură clasă, puteți să vă începeți aplicația cu o strategie de persistență de bază și apoi să treceți la SQLite sau Core Data mai târziu, dacă și când are sens să faceți acest lucru, totul fără să schimbați nimic altceva în baza de cod a aplicației.
Utilizarea Core Data într-un proiect iOS poate declanșa adesea mai multe dezbateri decât arhitectura însăși. Cu toate acestea, utilizarea Core Data cu VIPER poate fi cea mai bună experiență Core Data pe care ați avut-o vreodată. Core Data este un instrument excelent pentru persistența datelor, menținând în același timp un acces rapid și o amprentă de memorie redusă. Dar are obiceiul de a-și strecura tentaculele NSManagedObjectContext
prin toate fișierele de implementare ale unei aplicații, în special acolo unde nu ar trebui să fie. VIPER păstrează Core Data acolo unde ar trebui să fie: la nivelul de stocare a datelor.
În exemplul listei de sarcini, singurele două părți ale aplicației care știu că se utilizează Core Data sunt magazinul de date în sine, care stabilește stiva Core Data, și managerul de date. Managerul de date efectuează o cerere de preluare, convertește NSManagedObjects
returnate de către depozitul de date în obiecte standard ale modelului PONSO și le transmite înapoi la stratul de logică de afaceri. În acest fel, nucleul aplicației nu depinde niciodată de Core Data și, ca bonus, nu trebuie să vă faceți griji că NSManagedObjects
învechite sau cu fire de execuție necorespunzătoare vor da peste cap lucrările.
Iată cum arată în interiorul managerului de date atunci când se face o cerere de accesare a magazinului Core Data:
@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
La fel de controversate ca și Core Data sunt UI Storyboards. Storyboards au multe caracteristici utile, iar ignorarea lor în totalitate ar fi o greșeală. Cu toate acestea, este dificil să se realizeze toate obiectivele VIPER în timp ce se folosesc toate caracteristicile pe care le oferă un storyboard.
Compromisul pe care tindem să-l facem este să alegem să nu folosim secvențe. Pot exista unele cazuri în care utilizarea secvențelor are sens, dar pericolul cu secvențele este că acestea fac foarte dificilă păstrarea intactă a separării între ecrane – precum și între interfața utilizator și logica aplicației. Ca regulă generală, încercăm să nu folosim secvențe dacă implementarea metodei prepareForSegue pare a fi necesară.
În rest, storyboard-urile sunt o modalitate excelentă de a implementa aspectul pentru interfața cu utilizatorul, în special atunci când se utilizează Auto Layout. Am ales să implementăm ambele ecrane pentru exemplul listei de sarcini folosind un storyboard și să folosim un cod ca acesta pentru a realiza propria noastră navigare:
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
Utilizarea VIPER pentru a construi module
De multe ori, atunci când lucrați cu VIPER, veți constata că un ecran sau un set de ecrane tinde să se unească sub forma unui modul. Un modul poate fi descris în mai multe moduri, dar de obicei cel mai bine este gândit ca o caracteristică. Într-o aplicație de podcasting, un modul ar putea fi playerul audio sau browserul de abonamente. În aplicația noastră de listă de sarcini, ecranele de listă și de adăugare sunt construite fiecare ca module separate.
Există câteva avantaje ale proiectării aplicației dvs. ca un set de module. Unul este că modulele pot avea interfețe foarte clare și bine definite, precum și că pot fi independente de alte module. Astfel, este mult mai ușor să adăugați/eliminați caracteristici sau să schimbați modul în care interfața dvs. prezintă diverse module utilizatorului.
Am vrut ca separarea dintre module să fie foarte clară în exemplul listei de sarcini, așa că am definit două protocoale pentru modulul de adăugare. Primul este interfața modulului, care definește ceea ce poate face modulul. Al doilea este delegatul modulului, care descrie ceea ce a făcut modulul. Exemplu:
@protocol VTDAddModuleInterface <NSObject>- (void)cancelAddAction;- (void)saveAddActionWithName:(NSString *)name dueDate:(NSDate *)dueDate;@end@protocol VTDAddModuleDelegate <NSObject>- (void)addModuleDidCancelAddAction;- (void)addModuleDidSaveAddAction;@end
Din moment ce un modul trebuie să fie prezentat pentru a fi de mare valoare pentru utilizator, prezentatorul modulului implementează, de obicei, interfața modulului. Atunci când un alt modul dorește să îl prezinte pe acesta, prezentatorul său va implementa protocolul delegat al modulului, astfel încât să știe ce a făcut modulul în timp ce a fost prezentat.
Un modul poate include un strat logic comun al aplicației format din entități, interactivi și manageri care pot fi utilizați pentru mai multe ecrane. Acest lucru depinde, desigur, de interacțiunea dintre aceste ecrane și de cât de similare sunt acestea. Un modul ar putea la fel de bine să reprezinte doar un singur ecran, așa cum se arată în exemplul listei de sarcini. În acest caz, stratul logic al aplicației poate fi foarte specific comportamentului modulului său particular.
Modulii sunt, de asemenea, doar o modalitate bună și simplă de organizare a codului. Păstrarea întregului cod pentru un modul ascuns în propriul său folder și grup în Xcode face ca acesta să fie ușor de găsit atunci când trebuie să modificați ceva. Este un sentiment grozav atunci când găsiți o clasă exact acolo unde vă așteptați să o căutați.
Un alt beneficiu al construirii modulelor cu VIPER este că acestea devin mai ușor de extins la mai mulți factori de formă. Faptul că logica aplicației pentru toate cazurile de utilizare este izolată la nivelul Interactor vă permite să vă concentrați asupra construirii noii interfețe utilizator pentru tabletă, telefon sau Mac, reutilizând în același timp stratul de aplicații.
Făcând acest lucru cu un pas mai departe, interfața utilizator pentru aplicațiile iPad poate fi capabilă să reutilizeze unele dintre vizualizările, controlorii de vizualizare și prezentatorii aplicației iPhone. În acest caz, un ecran de iPad ar fi reprezentat de „super” prezentatoare și wireframe-uri, care ar compune ecranul folosind prezentatoare și wireframe-uri existente care au fost scrise pentru iPhone. Construirea și întreținerea unei aplicații pe mai multe platforme poate fi destul de dificilă, dar o bună arhitectură care promovează reutilizarea în cadrul modelului și al stratului de aplicație ajută la facilitarea acestui lucru.
Testing with VIPER
Să urmezi VIPER încurajează o separare a preocupărilor care facilitează adoptarea TDD. Interactorul conține o logică pură care este independentă de orice interfață de utilizator, ceea ce face ca acesta să fie ușor de condus cu teste. Prezentatorul conține logica de pregătire a datelor pentru afișare și este independent de orice widget UIKit. Dezvoltarea acestei logici este, de asemenea, ușor de condus cu teste.
Metoda noastră preferată este de a începe cu Interactor. Totul în UI este acolo pentru a servi nevoilor cazului de utilizare. Utilizând TDD pentru a testa API-ul pentru Interactor, veți avea o mai bună înțelegere a relației dintre UI și cazul de utilizare.
Ca exemplu, vom analiza Interactorul responsabil pentru lista de articole viitoare de făcut. Politica de căutare a elementelor viitoare este de a găsi toate elementele de făcut care trebuie îndeplinite până la sfârșitul săptămânii viitoare și de a clasifica fiecare element de făcut ca fiind datorat astăzi, mâine, în cursul acestei săptămâni sau săptămâna viitoare.
Primul test pe care îl scriem este acela de a ne asigura că Interactorul găsește toate articolele de făcut care trebuie să fie predate până la sfârșitul săptămânii viitoare:
- (void)testFindingUpcomingItemsRequestsAllToDoItemsFromTodayThroughEndOfNextWeek{ todoItemsBetweenStartDate:self.today endDate:self.endOfNextWeek completionBlock:OCMOCK_ANY]; ;}
După ce știm că Interactorul cere articolele de făcut corespunzătoare, vom scrie mai multe teste pentru a confirma că alocă articolele de făcut în grupul corect de date relative (de ex.ex. astăzi, mâine, etc.):
- (void)testFindingUpcomingItemsWithOneItemDueTodayReturnsOneUpcomingItemsForToday{ NSArray *todoItems = @]; ; NSArray *upcomingItems = @]; ; ;}
Acum că știm cum arată API-ul pentru Interactor, putem dezvolta Prezentatorul. Atunci când prezentatorul primește de la Interactor viitoarele sarcini de îndeplinit de la Interactor, vom dori să testăm dacă formatam corect datele și le afișăm în UI:
- (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 = ; ];}
De asemenea, dorim să testăm dacă aplicația va porni acțiunea corespunzătoare atunci când utilizatorul dorește să adauge o nouă sarcină de îndeplinit:
- (void)testAddNewToDoItemActionPresentsAddToDoUI{ presentAddInterface]; ;}
Acum putem dezvolta View. Atunci când nu există elemente viitoare de făcut, vrem să afișăm un mesaj special:
- (void)testShowingNoContentMessageShowsNoContentView{ ; XCTAssertEqualObjects(self.view.view, self.view.noContentView, @"the no content view should be the view");}
Când există elemente viitoare de făcut care trebuie afișate, vrem să ne asigurăm că se afișează tabelul:
- (void)testShowingUpcomingItemsShowsTableView{ ; XCTAssertEqualObjects(self.view.view, self.view.tableView, @"the table view should be the view");}
Construirea Interactorului mai întâi este o potrivire naturală cu TDD. Dacă dezvoltați mai întâi Interactorul, urmat de Prezentator, ajungeți să construiți mai întâi o suită de teste în jurul acestor straturi și să puneți bazele pentru implementarea acelor cazuri de utilizare. Puteți itera rapid pe aceste clase, deoarece nu va trebui să interacționați cu interfața de utilizator pentru a le testa. Apoi, când veți dezvolta vizualizarea, veți avea o logică funcțională și testată și un strat de prezentare care să se conecteze la ea. În momentul în care terminați de dezvoltat View, s-ar putea să descoperiți că la prima rulare a aplicației totul funcționează pur și simplu, pentru că toate testele care trec vă spun că va funcționa.
Concluzie
Sperăm că v-a plăcut această introducere în VIPER. Mulți dintre dumneavoastră s-ar putea să vă întrebați acum unde să mergeți în continuare. Dacă ați dori să vă arhitecturați următoarea aplicație folosind VIPER, de unde ați începe?
Acest articol și exemplul nostru de implementare a unei aplicații folosind VIPER sunt atât de specifice și bine definite pe cât am putut să le facem. Aplicația noastră cu lista de sarcini este destul de simplă, dar ar trebui să explice, de asemenea, cu exactitate cum să construiți o aplicație folosind VIPER. Într-un proiect din lumea reală, cât de mult veți urma acest exemplu va depinde de propriul set de provocări și constrângeri. Din experiența noastră, fiecare dintre proiectele noastre a variat ușor abordarea adoptată pentru utilizarea VIPER, dar toate au beneficiat foarte mult de utilizarea acestuia pentru a-și ghida abordările.
Pot exista cazuri în care doriți să vă abateți de la calea trasată de VIPER din diverse motive. Poate că v-ați lovit de un lanț de obiecte „iepuraș”, sau aplicația dvs. ar beneficia de utilizarea secvențelor în Storyboards. Acest lucru este în regulă. În aceste cazuri, luați în considerare spiritul a ceea ce reprezintă VIPER atunci când luați o decizie. În esența sa, VIPER este o arhitectură bazată pe principiul responsabilității unice. Dacă aveți probleme, gândiți-vă la acest principiu atunci când decideți cum să mergeți mai departe.
S-ar putea să vă întrebați, de asemenea, dacă este posibil să utilizați VIPER în aplicația dvs. existentă. În acest scenariu, luați în considerare construirea unei noi caracteristici cu VIPER. Multe dintre proiectele noastre existente au urmat această cale. Acest lucru vă permite să construiți un modul folosind VIPER și, de asemenea, vă ajută să depistați orice problemă existentă care ar putea îngreuna adoptarea unei arhitecturi bazate pe principiul responsabilității unice.
Unul dintre lucrurile minunate legate de dezvoltarea de software este că fiecare aplicație este diferită și există, de asemenea, moduri diferite de a arhitectura orice aplicație. Pentru noi, acest lucru înseamnă că fiecare aplicație este o nouă oportunitate de a învăța și de a încerca lucruri noi. Dacă vă decideți să încercați VIPER, credem că veți învăța și dumneavoastră câteva lucruri noi. Vă mulțumim că ați citit.
Addenda la Swift
Săptămâna trecută, la WWDC, Apple a prezentat limbajul de programare Swift ca fiind viitorul dezvoltării Cocoa și Cocoa Touch. Este prea devreme pentru a ne fi format opinii complexe despre limbajul Swift, dar știm că limbajele au o influență majoră asupra modului în care proiectăm și construim software. Am decis să rescriem aplicația noastră de exemplu VIPER TODO folosind Swift pentru a ne ajuta să învățăm ce înseamnă acest lucru pentru VIPER. Până acum, ne place ceea ce vedem. Iată câteva caracteristici ale Swift care, după părerea noastră, vor îmbunătăți experiența de construire a aplicațiilor care utilizează VIPER.
Structuri
În VIPER folosim clase model mici, ușoare, pentru a trece date între straturi, cum ar fi de la Presenter la View. Aceste PONSO-uri sunt, de obicei, destinate pur și simplu să transporte cantități mici de date și, de obicei, nu sunt destinate să fie subclasate. Structurile Swift se potrivesc perfect pentru aceste situații. Iată un exemplu de struct utilizat în exemplul VIPER Swift. Observați că acest struct trebuie să fie echivocabil și, prin urmare, am supraîncărcat operatorul == pentru a compara două instanțe ale tipului său:
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}
Siguranța tipurilor
Poate cea mai mare diferență între Objective-C și Swift este modul în care cele două gestionează tipurile. Objective-C este tipizat dinamic, iar Swift este foarte intenționat strict în ceea ce privește modul în care implementează verificarea tipurilor în momentul compilării. Pentru o arhitectură precum VIPER, în care o aplicație este compusă din mai multe straturi distincte, siguranța tipurilor poate fi un mare câștig pentru eficiența programatorului și pentru structura arhitecturală. Compilatorul vă ajută să vă asigurați că containerele și obiectele sunt de tipul corect atunci când sunt transmise între limitele straturilor. Acesta este un loc excelent pentru a utiliza structurile, așa cum se arată mai sus. Dacă un struct este menit să trăiască la granița dintre două straturi, atunci puteți garanta că nu va putea niciodată să evadeze dintre aceste straturi datorită siguranței tipului.
Lecturi suplimentare
- VIPER TODO, articol exemplu de aplicație
- VIPER SWIFT, articol exemplu de aplicație construită folosind Swift
- Contoare, o altă aplicație exemplu
- Mutual Mobile Introducere în VIPER
- Arhitectură curată
- Controlere de vizualizare mai ușoare
- Testarea controlorilor de vizualizare
- Bunnies
.