MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Objective-C应用架构设计之VIPER模式探索

2022-01-305.6k 阅读

VIPER模式简介

VIPER模式是一种基于MVC(Model - View - Controller)架构模式演变而来的设计模式,它在iOS开发中为应用架构设计提供了一种更为清晰、可测试性更强的方式。VIPER分别代表View、Interactor、Presenter、Entity和Router,每个组件都有明确的职责。

VIPER各组件职责

  1. View:负责展示用户界面,它只关心如何将数据呈现给用户。在Objective - C中,通常是继承自UIViewUIViewController的类。View接收来自Presenter的指令,更新界面上的元素,但不处理任何业务逻辑。例如,在一个登录界面中,View负责显示用户名和密码的输入框、登录按钮等UI元素。
#import <UIKit/UIKit.h>

@protocol LoginViewProtocol <NSObject>
- (void)showError:(NSString *)errorMessage;
- (void)navigateToHome;
@end

@interface LoginViewController : UIViewController <LoginViewProtocol>
// 用户名输入框
@property (nonatomic, strong) UITextField *usernameTextField;
// 密码输入框
@property (nonatomic, strong) UITextField *passwordTextField;
// 登录按钮
@property (nonatomic, strong) UIButton *loginButton;

@end

在上述代码中,LoginViewController作为View,遵循LoginViewProtocol协议,协议中定义了展示错误信息和跳转到首页的方法。同时,它拥有用于用户交互的UI元素。

  1. Interactor:主要处理业务逻辑,它从Entity获取数据,并与外部数据源(如网络服务、本地数据库)进行交互。Interactor不依赖于View和Presenter,这使得它的测试非常容易。比如在登录功能中,Interactor负责验证用户名和密码是否正确,可能会调用网络API来进行验证。
#import <Foundation/Foundation.h>

@protocol LoginInteractorOutputProtocol <NSObject>
- (void)loginSuccess;
- (void)loginFailure:(NSString *)errorMessage;
@end

@interface LoginInteractor : NSObject
@property (nonatomic, weak) id<LoginInteractorOutputProtocol> output;

- (void)loginWithUsername:(NSString *)username password:(NSString *)password;
@end

LoginInteractor类中有一个output属性,它遵循LoginInteractorOutputProtocol协议,通过这个协议将登录结果反馈给Presenter。loginWithUsername:password:方法用于执行登录的业务逻辑。

  1. Presenter:作为View和Interactor之间的桥梁,Presenter接收来自View的用户操作事件,调用Interactor的方法处理业务逻辑,并将Interactor返回的结果处理后传递给View。在登录场景中,Presenter接收用户点击登录按钮的事件,调用Interactor的登录方法,并根据返回结果决定是显示错误信息还是跳转到首页。
#import <Foundation/Foundation.h>
#import "LoginViewProtocol.h"
#import "LoginInteractor.h"

@interface LoginPresenter : NSObject <LoginViewProtocol, LoginInteractorOutputProtocol>
@property (nonatomic, strong) LoginViewProtocol *view;
@property (nonatomic, strong) LoginInteractor *interactor;

- (instancetype)initWithView:(LoginViewProtocol *)view interactor:(LoginInteractor *)interactor;
@end

LoginPresenter类遵循LoginViewProtocolLoginInteractorOutputProtocol协议,通过初始化方法接收View和Interactor的实例,从而实现两者之间的交互。

  1. Entity:代表应用中的数据模型,它是业务数据的载体,与具体的业务逻辑和界面展示无关。例如,用户信息可能会被封装在一个User类中,这个类就是一个Entity。
#import <Foundation/Foundation.h>

@interface User : NSObject
@property (nonatomic, strong) NSString *username;
@property (nonatomic, strong) NSString *password;

- (instancetype)initWithUsername:(NSString *)username password:(NSString *)password;
@end

User类简单地封装了用户名和密码两个属性,用于在应用中传递用户相关的数据。

  1. Router:负责处理应用内的导航逻辑,它知道如何创建和切换不同的View。在登录成功后,Router负责将用户导航到首页。
#import <UIKit/UIKit.h>

@interface LoginRouter : NSObject
+ (instancetype)routerWithViewController:(UIViewController *)viewController;
- (void)navigateToHome;
@end

LoginRouter类通过类方法创建实例,并提供navigateToHome方法用于执行导航到首页的操作。

VIPER模式的优势

  1. 高可测试性:由于各组件职责明确且相互独立,Interactor可以很容易地进行单元测试,不依赖于View和Presenter。例如,我们可以单独测试LoginInteractor的登录逻辑,模拟不同的用户名和密码输入,验证其返回结果是否正确。
#import <XCTest/XCTest.h>
#import "LoginInteractor.h"

@interface LoginInteractorTests : XCTestCase
@property (nonatomic, strong) LoginInteractor *interactor;
@end

@implementation LoginInteractorTests

- (void)setUp {
    [super setUp];
    self.interactor = [[LoginInteractor alloc] init];
}

- (void)tearDown {
    self.interactor = nil;
    [super tearDown];
}

- (void)testLoginSuccess {
    XCTestExpectation *expectation = [self expectationWithDescription:@"Login success"];
    self.interactor.output = self;
    [self.interactor loginWithUsername:@"validUser" password:@"validPassword"];
    [self waitForExpectationsWithTimeout:1 handler:nil];
}

- (void)loginSuccess {
    XCTFail(@"Login should not succeed with test data");
}

- (void)loginFailure:(NSString *)errorMessage {
    XCTAssertEqualObjects(errorMessage, @"Invalid username or password", @"Error message should be correct");
    [[self expectationForDescription:@"Login failure"] fulfill];
}

@end

在上述测试代码中,我们创建了LoginInteractorTests测试类,测试LoginInteractor的登录逻辑。通过模拟登录操作,验证错误信息是否正确。

  1. 职责清晰:每个组件只负责自己特定的任务,View专注于UI展示,Interactor专注于业务逻辑,Presenter负责协调两者,Entity承载数据,Router处理导航。这使得代码结构更加清晰,易于理解和维护。比如在一个复杂的电商应用中,商品展示、购物车操作等功能模块可以按照VIPER模式进行清晰的划分,每个模块的代码都不会相互混淆。

  2. 可扩展性:当应用需要添加新功能或修改现有功能时,由于VIPER模式的组件化特性,可以很方便地在相应的组件中进行修改或扩展,而不会影响到其他组件。例如,如果要添加第三方登录功能,只需要在Interactor中添加相应的业务逻辑,Presenter和View根据新的逻辑进行少量调整即可。

VIPER模式在实际项目中的应用

项目架构搭建

以一个简单的待办事项应用为例,我们来看看如何基于VIPER模式搭建项目架构。

  1. 创建Entity:待办事项可以用一个TodoItem类来表示,它包含标题、描述、是否完成等属性。
#import <Foundation/Foundation.h>

@interface TodoItem : NSObject
@property (nonatomic, strong) NSString *title;
@property (nonatomic, strong) NSString *description;
@property (nonatomic, assign) BOOL isCompleted;

- (instancetype)initWithTitle:(NSString *)title description:(NSString *)description;
@end
  1. 创建View:待办事项列表界面可以由TodoListView类来实现,它遵循TodoListViewProtocol协议,负责展示待办事项列表,并接收Presenter的指令进行界面更新。
#import <UIKit/UIKit.h>

@protocol TodoListViewProtocol <NSObject>
- (void)reloadData;
- (void)showError:(NSString *)errorMessage;
@end

@interface TodoListView : UITableView <TodoListViewProtocol>
@property (nonatomic, strong) NSMutableArray<TodoItem *> *todoItems;
@end
  1. 创建InteractorTodoListInteractor负责与数据存储(如本地数据库)进行交互,获取和保存待办事项数据。
#import <Foundation/Foundation.h>

@protocol TodoListInteractorOutputProtocol <NSObject>
- (void)fetchTodoItemsSuccess:(NSArray<TodoItem *> *)todoItems;
- (void)fetchTodoItemsFailure:(NSString *)errorMessage;
@end

@interface TodoListInteractor : NSObject
@property (nonatomic, weak) id<TodoListInteractorOutputProtocol> output;

- (void)fetchTodoItems;
- (void)saveTodoItem:(TodoItem *)todoItem;
@end
  1. 创建PresenterTodoListPresenter协调View和Interactor,处理用户在列表界面的操作,如加载数据、添加新的待办事项等。
#import <Foundation/Foundation.h>
#import "TodoListViewProtocol.h"
#import "TodoListInteractor.h"

@interface TodoListPresenter : NSObject <TodoListViewProtocol, TodoListInteractorOutputProtocol>
@property (nonatomic, strong) TodoListViewProtocol *view;
@property (nonatomic, strong) TodoListInteractor *interactor;

- (instancetype)initWithView:(TodoListViewProtocol *)view interactor:(TodoListInteractor *)interactor;
- (void)viewDidLoad;
- (void)addTodoItemWithTitle:(NSString *)title description:(NSString *)description;
@end
  1. 创建RouterTodoListRouter负责处理待办事项列表界面与其他界面(如添加待办事项详情界面)之间的导航。
#import <UIKit/UIKit.h>

@interface TodoListRouter : NSObject
+ (instancetype)routerWithViewController:(UIViewController *)viewController;
- (void)navigateToAddTodoItem;
@end

具体功能实现

  1. 加载待办事项列表
    • TodoListPresenterviewDidLoad方法中,调用interactorfetchTodoItems方法。
- (void)viewDidLoad {
    [self.interactor fetchTodoItems];
}
  • TodoListInteractor从数据存储中获取待办事项数据,成功后通过output回调fetchTodoItemsSuccess:方法给Presenter。
- (void)fetchTodoItems {
    // 模拟从本地数据库获取数据
    NSArray<TodoItem *> *todoItems = [self fetchTodoItemsFromDatabase];
    if (todoItems) {
        [self.output fetchTodoItemsSuccess:todoItems];
    } else {
        [self.output fetchTodoItemsFailure:@"Failed to fetch todo items"];
    }
}

- (NSArray<TodoItem *> *)fetchTodoItemsFromDatabase {
    // 实际实现从数据库获取数据逻辑,这里简单返回nil
    return nil;
}
  • TodoListPresenterfetchTodoItemsSuccess:方法中,更新viewtodoItems属性,并调用viewreloadData方法刷新界面。
- (void)fetchTodoItemsSuccess:(NSArray<TodoItem *> *)todoItems {
    self.view.todoItems = [NSMutableArray arrayWithArray:todoItems];
    [self.view reloadData];
}
  1. 添加新的待办事项
    • 用户在TodoListView中触发添加新待办事项的操作,TodoListPresenteraddTodoItemWithTitle:description:方法被调用。
- (void)addTodoItemWithTitle:(NSString *)title description:(NSString *)description {
    TodoItem *newTodoItem = [[TodoItem alloc] initWithTitle:title description:description];
    [self.interactor saveTodoItem:newTodoItem];
}
  • TodoListInteractorsaveTodoItem:方法将新的待办事项保存到数据存储中,成功后回调Presenter。
- (void)saveTodoItem:(TodoItem *)todoItem {
    // 模拟保存到本地数据库
    BOOL success = [self saveTodoItemToDatabase:todoItem];
    if (success) {
        [self.output fetchTodoItemsSuccess:@[todoItem]];
    } else {
        [self.output fetchTodoItemsFailure:@"Failed to save todo item"];
    }
}

- (BOOL)saveTodoItemToDatabase:(TodoItem *)todoItem {
    // 实际实现保存到数据库逻辑,这里简单返回NO
    return NO;
}
  • Presenter再次调用fetchTodoItemsSuccess:方法,更新界面显示新添加的待办事项。

VIPER模式与其他架构模式的比较

  1. 与MVC的比较
    • 职责划分:MVC中Controller的职责相对较为模糊,它既处理业务逻辑又负责协调View和Model。而VIPER模式将业务逻辑从Controller中分离出来,由Interactor专门处理,使得职责更加明确。例如,在一个图片浏览应用中,MVC模式下Controller可能既要处理图片加载的业务逻辑,又要负责图片在View上的展示逻辑。而在VIPER模式中,Interactor负责图片加载逻辑,Presenter负责将加载后的图片传递给View进行展示。
    • 可测试性:MVC模式下,由于Controller耦合了业务逻辑和View相关的操作,对其进行单元测试相对困难,需要模拟较多的依赖。而VIPER模式中Interactor独立于View和Presenter,很容易进行单元测试,提高了代码的可测试性。
  2. 与MVVM的比较
    • 数据绑定方式:MVVM模式通过数据绑定机制(如在iOS开发中可以使用KVO等方式),使得View和ViewModel之间的数据同步更加自动化。而VIPER模式中,View和Presenter之间通过协议方法进行交互,相对来说没有MVVM的数据绑定那么自动化,但这种显式的交互方式使得代码的执行流程更加清晰。例如,在一个文本输入框实时更新数据的场景中,MVVM可以通过数据绑定自动更新ViewModel中的数据,而VIPER需要通过Presenter来协调View的输入事件和数据更新。
    • 架构复杂度:MVVM模式在处理简单界面时,由于数据绑定的便利性,代码量可能相对较少。但在处理复杂业务逻辑和导航时,可能需要更多的代码来管理ViewModel之间的关系。VIPER模式由于组件职责明确,在复杂项目中更易于维护和扩展,但在简单项目中可能会显得架构过于复杂,增加了一定的代码量。

VIPER模式应用中的注意事项

  1. 组件间通信:虽然VIPER模式各组件职责明确,但组件间的通信需要仔细设计。例如,Presenter与View之间通过协议进行通信,协议方法的定义要准确反映View的需求和Presenter能提供的功能。同时,Presenter与Interactor之间的通信也要确保数据的准确传递和处理。在实际项目中,可能会出现由于协议方法定义错误或通信数据格式不一致导致的问题。
  2. 项目规模适用性:VIPER模式在大型项目中优势明显,能够有效管理复杂的业务逻辑和界面交互。但对于小型项目,由于其架构相对复杂,可能会增加开发成本。在选择使用VIPER模式时,需要根据项目的规模、复杂度和开发周期等因素综合考虑。如果是一个简单的工具类应用,采用MVC模式可能更为合适,开发效率更高。
  3. 开发团队熟悉度:如果开发团队对VIPER模式不熟悉,可能需要一定的学习成本来掌握其设计理念和开发方式。在引入VIPER模式之前,需要对团队成员进行培训,确保大家能够理解和遵循这种架构模式进行开发,否则可能会导致代码结构混乱,无法发挥VIPER模式的优势。

通过以上对VIPER模式的详细介绍,包括其组件职责、优势、实际应用、与其他模式的比较以及注意事项,希望能帮助开发者在Objective - C应用架构设计中更好地运用VIPER模式,开发出结构清晰、可维护性强、可测试性高的iOS应用。