找回密码
 立即注册
首页 业界区 安全 MVC 架构解析

MVC 架构解析

蓬森莉 4 小时前
认真对待每时、每刻每一件事,把握当下、立即去做。
MVC 模式的目的是实现一种动态的程序设计,使后续对程序的修改和扩展简化,并且使程序某一部分的重复利用成为可能。除此之外,此模式通过对复杂度的简化,使程序结构更加直观。下面主要对 MVC 架构下的优化方案以及其项目结构解析。
1.jpg

一. MVC 相应层应该做什么?

1. 控制器(Controller)业务层

控制器(Controller)-->业务层, Model 与 View 层的中介,负责转发请求,对请求进行处理,把 Model 数据在 View 上展示出来。
主要职责:

  • 管理 View Container 的生命周期;
  • 负责生成所有的 View 实例,并放入 View Container;
  • 监听来自 View 与业务有关的事件,通过与 Model 的合作,来完成对应事件的业务;
2. 视图(View)展现层

视图(View) -->展现层,承载 UI 展示和事件响应(交互)。
主要职责:

  • 响应与业务无关的事件,并因此引发动画效果,点击反馈(如果合适的话,尽量还是放在 View 去做)等。
  • 界面元素表达;
3. 模型(Model)数据层

模型(Model) -->数据层,数据处理层,包括网络请求,数据加工,算法实现等。
主要职责:

  • 给 ViewController 提供数据;
  • 给 ViewController 存储数据提供接口;
  • 提供经过抽象的业务基本组件,供 Controller 调度;
2.png

4. 示例解析

在 iOS 中的 Controlller 是  UIViewController,所以导致很多人会把视图写在 Controller 中,如下图:
  1. @implementation DemoViewController
  2. - (void)viewDidLoad {
  3.     [super viewDidLoad];
  4.     //setupUI
  5.     //1.createView
  6.     UIView *view = [[UIView alloc]init];
  7.     view.frame = CGRectMake(100, 100, 100, 100);
  8.     view.backgroundColor = [UIColor orangeColor];
  9.     [self.view addSubview:view];
  10.    
  11.     //2.createButton
  12.     UIButton *btn = [UIButton buttonWithType:UIButtonTypeInfoDark];
  13.     btn.center = self.view.center;
  14.     [self.view addSubview:btn];
  15.    
  16.     //3...
  17. }
复制代码
这种写法在我刚学习编程的时候也这样写过,先说这样写的好处,以及初学者为什么会这么写:

  • 比如按钮,可以在当前控制器直接 add target: 添加点击事件,在当前控制器内就能调用到点击方法,不需要设置代理之类的;
  • 比如要找某个界面,直接切到这个界面对应的 controller 就行,因为View 写在 Controller 里面,不用去别的地方找就这里有;
  • 比如一个 View,里面有一张图片,图片依赖于网络资源,这样写的好处,可以直接让 View 在 Controller 中就能拿到资源,不需要传值;
缺点:

  • 导致 Controller 特别臃肿,里面代码特别多,视图一复杂起来,代码量可能过1000行,不好维护;
  • 写在 Controller 里无法复用,除非你在 VC2 里面 copy 当前 VC 中的  View 的代码;
  • 特别low!!会被懂架构的人瞧不起,喷你根本不是 MVC,是 MC 架构;
如何告别 MC 模式,真正走到 MVC?
先给自己洗脑,iOS 的 Controller 不是 UIViewController,而是普通的 Controller,没有 View。(很关键的一步)。
模块化划分,每个模块对应自己的一个 View,例如 Demo 模块,View 层里面有个 DemoView,将界面元素写到 View 中。
二. MVC 相应层之间如何通信?

3.jpg

1. View 层和 Controller 层双向通信

1.1 Controller 如何将数据传递到 View 层


  • 创建 View 的时候通过 View 的函数作为外部参数传进去。
1.2 View 层(用户事件)如何传递到 Controller 层

1.2.1 代理(delegate)

通过代理(delegate),代理委托模式通过定义协议方法实现解耦, View 只关心事件触发不处理具体逻辑;
  1. // 1. 定义协议
  2. @protocol CustomViewDelegate <NSObject>
  3. - (void)customView:(UIView *)view didTapButton:(UIButton *)button;
  4. @end
  5. // 2. View 持有 delegate 弱引用
  6. @interface CustomView : UIView
  7. @property (nonatomic, weak) id<CustomViewDelegate> delegate;
  8. @end
  9. @implementation CustomView
  10. - (void)buttonTapped:(UIButton *)sender {
  11.     [self.delegate customView:self didTapButton:sender]; // 触发代理方法
  12. }
  13. @end
  14. // 3. Controller 实现协议
  15. @interface ViewController () <CustomViewDelegate>
  16. @end
  17. @implementation ViewController
  18. - (void)viewDidLoad {
  19.     CustomView *view = [[CustomView alloc] init];
  20.     view.delegate = self; // 设置代理
  21. }
  22. - (void)customView:(CustomView *)view didTapButton:(UIButton *)button {
  23.     NSLog(@"Delegate: 按钮点击事件处理"); // Controller 响应事件
  24. }
  25. @end
复制代码
1.2.2 target-action 监听

在 Controller 设置 target-action 监听,Controller 给 View 添加一个 target,当用户的触摸事件发生时,view 产生 action,Controller 接收到之后做出相应的响应,直接建立 View 与控制器的响应链关系,适合简单控件事件;
  1. // 1. View 暴露添加 target 的方法
  2. @interface CustomView : UIView
  3. - (void)addTarget:(id)target action:(SEL)action;
  4. @end
  5. @implementation CustomView {
  6.     id _target;
  7.     SEL _action;
  8. }
  9. - (void)addTarget:(id)target action:(SEL)action {
  10.     _target = target;
  11.     _action = action;
  12. }
  13. - (void)buttonTapped {
  14.     #pragma clang diagnostic push
  15.     #pragma clang diagnostic ignored "-Warc-performSelector-leaks"
  16.     [_target performSelector:_action withObject:self]; // 执行 Action
  17.     #pragma clang diagnostic pop
  18. }
  19. @end
  20. // 2. Controller 设置 Target-Action
  21. @implementation ViewController
  22. - (void)viewDidLoad {
  23.     CustomView *view = [[CustomView alloc] init];
  24.     [view addTarget:self action:@selector(handleButtonTap:)]; // 绑定事件
  25. }
  26. - (void)handleButtonTap:(CustomView *)sender {
  27.     NSLog(@"Target-Action: 按钮点击事件处理"); // Controller 响应事件
  28. }
  29. @end
复制代码
1.2.3 数据源模式 data source

通过数据源模式 data source,通过数据驱动 UI 更新,控制器实现数据获取协议供 View 调用;
  1. // 1. 定义数据源协议
  2. @protocol CustomViewDataSource <NSObject>
  3. - (NSString *)textForButtonInView:(CustomView *)view;
  4. @end
  5. // 2. View 持有 dataSource 引用
  6. @interface CustomView : UIView
  7. @property (nonatomic, weak) id<CustomViewDataSource> dataSource;
  8. - (void)reloadData; // 触发数据更新
  9. @end
  10. @implementation CustomView
  11. - (void)reloadData {
  12.     NSString *text = [self.dataSource textForButtonInView:self]; // 获取数据
  13.     [_button setTitle:text forState:UIControlStateNormal];
  14. }
  15. @end
  16. // 3. Controller 实现数据源
  17. @interface ViewController () <CustomViewDataSource>
  18. @end
  19. @implementation ViewController
  20. - (void)viewDidLoad {
  21.     CustomView *view = [[CustomView alloc] init];
  22.     view.dataSource = self;
  23.     [view reloadData]; // 初始化数据
  24. }
  25. - (NSString *)textForButtonInView:(CustomView *)view {
  26.     return @"DataSource 模式"; // 提供动态数据
  27. }
  28. @end
复制代码
1.2.4 Block(闭包)

Block(闭包):‌View 定义闭包属性,Controller 通过赋值闭包来响应事件。‌优点,代码紧凑,适合简单回调。‌缺点,需注意循环引用(使用 [weak self])。
  1. class CustomView: UIView {
  2.     var onButtonTap: (() -> Void)?
  3.     @objc func buttonTapped() { onButtonTap?() }
  4. }
  5. // Controller 中赋值
  6. customView.onButtonTap = { [weak self] in self?.handleTap() }
复制代码
2. Model 层和 Controller 层双向通信

我们来看下这里的 Model 层通信,先看一段代码。
  1. @implementation DemoViewController
  2. - (void)viewDidLoad {
  3.     [super viewDidLoad];
  4.     //loadDatas
  5.     [[AFHTTPSessionManager manager]GET:url
  6.                             parameters:parameters
  7.                               progress:nil
  8.                                success:^(NSURLSessionDataTask * _Nonnull task, id  _Nullable responseObject)
  9.     {
  10.         //刷新tableView
  11.         _datas = responseObject;
  12.         [_tableView reloadDatas];
  13.         
  14.     } failure:nil];
  15. }
复制代码
这种写法在我刚学习编程的时候也这样写过,先说这样写的好处,以及初学者为什么会这么写:

  • 简单,网络请求完,直接在当前控制器刷新 TableView 的数据源;
  • 比如要找某个界面的网络请求,直接切到这个界面对应的 controller 就行,因为数据请求 写在  Controller 里面,不用去别的地方找,就这里有;
  • 比如当前网络请求接口,需要外部参数,比如前一个界面的 uuid,这样写的好处,可以直接让当前请求在 Controller 中就能拿到资源,不需要传值;
缺点:

  • 又导致 Controller 特别臃肿,里面代码特别多,如果当前控制器需要多次请求,代码量可能过1000行,不好维护;
  • 写在 Controller 里无法复用,除非你在 VC2 里面 copy 当前 VC 中的 网络请求的代码;
  • 如果某些接口有依赖要求,接口1请求完再请求接口2,需要嵌套起来;
  • 特别 low!!会被懂架构的人瞧不起,喷你根本不是 MVC,如果你还用了上面的 View 写在 Controller 的操作的话,恭喜你,最终大法 -  Controller 架构 顺利完成,并不需要什么 Model && View;
这 iOS 的 Controller 就算是 UIViewController,也没看到 Model 啊,没有 Model。(很关键的一步);
模块化划分,每个模块对应自己的一个 Model,例如 Demo 模块,Model 层里面有个 DemoModel,将网络请求&&数据处理写到 Model 中;
2.1  Controller 调用和传值到 Model

Controller 层直接调用 Model 层类方法和实例方法,并通过参数传值。
2.2 Model 层数据如何回调到 Controller 层

Model 层数据如何回调到 Controller 层,Controller 层如何知道 Model 层数据发生了改变。
2.2.1 Block 回调

轻量级单向通信,适合简单回调但需注意循环引用
  1. //Model
  2. @implementation DemoModel
  3. + (void)fetchDatasWithUUid:(NSString *)uuid success:(successBlock)block{
  4.     //Model发送网络请求
  5.     NSDictionary *parameters = @{@"uuid":uuid}
  6.         [[AFHTTPSessionManager manager]GET:url
  7.                                 parameters:parameters
  8.                                   progress:nil
  9.                                    success:^(NSURLSessionDataTask * _Nonnull task, id  _Nullable responseObject)
  10.         {
  11.             //通过block异步回调~
  12.             block(responseObject);
  13.    
  14.         } failure:nil];   
  15. }
  16. //Controller
  17. @implementation DemoViewController
  18. - (void)viewDidLoad {
  19.     [super viewDidLoad];
  20.     //loadDatas
  21.     [DemoModel fetchDatasWithUUid:_uuid success:^(NSArray *array) {
  22.         _datas = array;
  23.         [_tableView reloadDatas];
  24.     }];
  25. }
复制代码
2.2.2 KVO(监听)

KVO(监听),监听 Model 的每个属性的变化来做出响应;
  1. // Model.h
  2. @interface MyModel : NSObject
  3. @property (nonatomic, strong) NSString *data;
  4. @end
  5. // Controller.m
  6. - (void)viewDidLoad {
  7.     [super viewDidLoad];
  8.     [self.model addObserver:self
  9.                  forKeyPath:@"data"
  10.                     options:NSKeyValueObservingOptionNew
  11.                     context:nil];
  12. }
  13. - (void)observeValueForKeyPath:(NSString *)keyPath
  14.                       ofObject:(id)object
  15.                         change:(NSDictionary *)change
  16.                        context:(void *)context {
  17.     if ([keyPath isEqualToString:@"data"]) {
  18.         self.label.text = change[NSKeyValueChangeNewKey]; // 响应变化
  19.     }
  20. }
  21. - (void)dealloc {
  22.     [self.model removeObserver:self forKeyPath:@"data"];
  23. }
复制代码
2.2.3 Notification(通知)

Notification(通知),Model 中创建一个 NSNotificationCenter,在 Controller  中创建一个方法来接收通知。当 Model 发生变化时,他会发送一个通知,而 Controller 会接收通知,一对多广播式通信,适合跨模块解耦但性能开销较大。
4.png

解释一下上面这幅图,一个完整的模块被分为了三个相对独立的部分,分别是Model,View,Controller,对应到我们 App 中的依次为继承自 NSObject 的数据中心,承载 UI 展示和事件响应的 View 以及我们最最常用的 UIViewController。
其中 VC 持有 View 和 Model 部分,View 通过代理或者 Target-Action 的方式把用户的操作传递给 VC,VC 负责根据不同的用户行为做出不同响应。如果需要加载或刷新数据则直接调用 Model 暴露的接口,如果数据可以同步拿到,则直接使用获取到的数据刷新 View。如果数据需要通过网络请求等其他异步的方式获取,VC 则通过监听 Model 发出的数据更新(成功或失败)通知,在收到通知时根据成功或者失败对 View 进行相应的刷新操作。可以看出来整个过程中 View 和 Model 是没有直接交互的,所有的操作都是通过 VC 进行协调的。
基础的 MVC 讲解完毕,其实本质上就是让 Controller 减压,不该控制器管的他别让他知道,如上基础 MVC 操作之后的优势:

  • MVC 架构分明,在同一个模块内,如果视图有问题,找到该模块的 View 就行,其他同理,Controller 代码大大减少,负责 View 的代理事件就可以;
  • 可以复用,比如你一个产品列表的数据,首页也要用,产品页也要用,直接分别在其对应的 VC1 && VC2 调用函数 [ProductModel fetchDatas] 即可,无需写多次,View 的复用同理;
  • 结构分明,便于维护,拓展也是在此基础上拓展,代码干净简洁。
三. MVC 架构常见的疑惑

1. 遗失的网络逻辑(网络数据请求应该放在那里?)

苹果使用的 MVC 的定义是这么说的:所有的对象都可以被归类为一个 Model,一个 View,或是一个控制器。就这些,那么把网络代码放哪里?和一个 API 通信的代码应该放在哪儿?
你可能试着把它放在 Model 对象里,但是也会很棘手,因为网络调用应该使用异步,这样如果一个网络请求比持有它的 Model 生命周期更长,事情将变的复杂。显然也不应该把网络代码放在 View 里,因此只剩下控制器了。这同样是个坏主意,因为这加剧了厚重控制器的问题。那么应该放在那里呢?显然 MVC 的 3 大组件根本没有适合放这些代码的地方。
网络请求与数据处理的归属争议:

  • ‌纯数据模型派:
    认为 Model 应仅定义数据结构,网络请求和数据处理应由 Controller 或单独的服务类(如 NetworkManager)处理。
  • ‌增强 Model 派:
    支持将网络请求封装在 Model 内部,通过扩展方法或静态函数实现,例如:
    1. extension NGLoginModel {
    2.     static func fetchAccount(completion: @escaping (NGLoginModel?) -> Void) {
    3.         NetworkManager.request(url: "api/login") { data in
    4.             let account = NetcallAccount(data: data)
    5.             completion(NGLoginModel(info: account))
    6.         }
    7.     }
    8. }
    复制代码
    这种方式保持数据与获取逻辑的紧密性,但可能增加 Model 的复杂度。

来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
您需要登录后才可以回帖 登录 | 立即注册