货拉拉用户iOS端基于MVP架构的深度优化方案与实践

1,758 阅读24分钟

一、背景介绍

  我们项目工程中常见的架构模式有MVC,MVP,MVVM,VIPER等,随着时间的推移和项目的不断迭代,iOS应用的代码规模往往会逐渐增长。这导致了代码的复杂性和维护成本的提高。在此背景下,对应用的架构进行深度优化成为必要的任务,优化架构的目的是提高代码的可维护性、可测试性和可扩展性,降低开发和维护的成本,同时提供良好的用户体验。在现有的架构中,可能存在以下问题:

  1. Massive View Controller(臃肿的视图控制器):由于大量的业务逻辑和视图相关的代码集中在视图控制器中,使得它变得庞大而难以维护。
  2. 低内聚和高耦合:不同模块之间的职责和关注点没有明确的划分,导致代码的耦合度过高,不易进行单独的开发和测试。
  3. 受UI框架限制的测试:由于视图和业务逻辑紧密耦合,导致单元测试变得困难,并且需要依赖UI框架进行测试。

  架构优化方案的目标是解决这些问题,提高代码质量和开发效率。通过合理的架构设计,可以使代码更加模块化、可测试和可维护,同时提供灵活性和可扩展性。

  本文将结合用户端首页和确认订单页的MVP架构模式,提出一种基于MVP的架构模式,即MVP+Context架构模式,希望对大家手中的MVP架构的优化提供一些优化思路和借鉴。

二、常见架构模式

这里先简要回顾下我们在实际开发中常用的几种架构模式及其优缺点,对移动端App常见架构比较熟悉的同学可以跳过这部分,直接看第三部分。

  1. MVC架构

Model-View-Controller,这是一种常见的架构模式,它将程序分为三个互相关联但又相互独立的角色(模型Model层,视图View层,控制器Controller层)

  1. Model层:包含了业务逻辑和处理数据的部分,如调用API接口获取数据,或者处理保存在数据库的数据等。
  2. View层:负责数据的展示,即用户看到的界面。它接收用户的行为并转发到Controller进行处理。
  3. Controller层:Model和View之间的中介,它控制用户界面与用户输入的反馈,用户与View交互时产生的事件都会被Controller所接收。Controller会根据接收到的信息去调用Model,Model处理完业务逻辑后,Controller再将处理的结果返回给View进行展示。

在较小型的项目中采用的比较普遍,能解决了开发中遇到的模块间高耦合问题,让代码结构更清晰,但会随着业务的复杂度增加,控制器层会变的过于繁重,导致代码维护和测试难度提高。

  1. MVP架构

Model-View-Presenter,将程序也分为三个角色

  1. Model(模型层):同MVC中的定义一样,Model是包含了业务逻辑和处理数据的部分,如调用API接口获取数据,或者处理保存在数据库的数据等。
  2. View(视图层):展示用户界面及其元素,将用户输入传递给Presenter并接收Presenter的指令以更新自己。
  3. Presenter(控制者):MVP中的主要部分,它旨在减轻Model和View的负担。它获取来自View的用户交互,并确定如何响应。根据应用程序逻辑,它会用Model获取数据,接着将数据格式化并返回给View显示。

这种模式下View和Model之间的所有交互都必须通过Presenter来进行,这各使它们之间的耦合性降低,提高了代码的可维护性和可测试性,但由于Presenter取得了View和Model的双重职责,可能会导致Presenter变得过于庞大。此外,因为Presenter和特定的View是紧密联系的,所以一旦View发生改变,Presenter也需要对应地进行改动。

  1. MVVM架构

Model-View-ViewModel,将程序分为三个角色:

  1. Model(模型层):同MVC中的定义一样,Model是包含了业务逻辑和处理数据的部分,如调用API接口获取数据,或者处理保存在数据库的数据等。
  2. View(视图层):展示用户界面及其元素,将用户输入转化为数据流,依靠数据双向绑定实现数据的更新和页面的刷新。
  3. ViewModel(视图模型层):作为桥梁连接Model与View,控制视图逻辑,处理用于展示的数据,转发用户对于View的操作至Model,同时,ViewModel并不直接持有View,而是通过数据绑定来与View进行交互,从而保证了View和Model的解耦。

最大特点是数据双向绑定,简化了View的逻辑。开发者只需专注于数据的处理,一切视图更新的工作都会被自动完成,极大提高了开发效率。并且,因为分层明确,各个部分的职责清晰,所以代码结构清晰,易于维护和测试,但缺点是由于过度的解耦和数据双向绑定,当支撑的业务变得复杂时,可能会对底层的数据流失去控制,难以调试和优化。并且,对于大型项目或者团队,由于对MVVM理解的不同,可能会导致不一致的代码风格。

  1. VIPER架构

View-Interactor-Presenter-Entity-Routing,一种基于单一职责原则和清晰的模块界限的架构模式,包含五个主要组成部分:

  1. View(视图层):负责显示用户界面和接收用户输入。它将用户输入传递给Presenter。
  2. Presenter(演示者):将从View层获取指令转到Interactor处理并接收处理后的数据,并将其格式化为适合View显示的格式,然后将这些数据传递给View去显示更新。
  3. Interactor(交互器):负责处理具体的业务逻辑。它获取数据(可能来自网络、数据库等),处理业务规则和数据,并将结果传递给Presenter。
  4. Entity(实体层):应用的基本数据对象,类似MVC架构中的Model层,例如数据库对象或网络请求的JSON对象等。
  5. Routing(路由层):负责页面之间的导航逻辑跳转。

VIPER架构的优点在于模块职责划分清晰,模块间的耦合度低,特别适合大项目的开发, 其主要缺点是由于层的划分较多,增加了代码复杂性,对于小型项目而言可能会显得过度设计。****

三、传统MVP架构实践

在使用MVP架构过程中,我们可能都会遇到这样的问题,View和Model之间的交互都通过Presenter来进行,使得Presenter承担了对接View和Model的双重职责,导致Presenter类代码量过大,代码理解的难度增加,此外,为了实现视图和模型之间的解耦,通常需要在视图和Presenter之间定义接口,这带来了较多的接口和实现类的冗余代码,增加了代码的维护成本。

以下是货拉拉用户端App根据业务划分的几大核心模块,各模块在不断迭代过程中都经历了不同的架构模式,目前各模块采用的架构模式如下:

首页确认页等待应答页详情页列表页IM消息个人中心页
架构模式MVPMVP+ContextMVPMVPMVCMVVM + RACMVP

我们都知道,一旦确定了某个模块的架构,需要进行架构更改就意味着整个模块的重构。重构过程会遇到一系列问题,耗费时间和精力。因此,在启动模块项目之前,首先要评估并明确采用哪种架构模式。从上面的介绍可以看出,我们尝试了不同模块的不同架构,其中以MVP架构为主要选择。目前,各模块的架构也已稳定下来。本文将重点探索MVP架构,通过对货拉拉首页的MVP架构进行深入分析,找出常规MVP架构的优势和劣势。并且,针对其劣势,介绍了一种深度优化方案- MVP+Context架构。希望这个方案能为大家在现有MVP架构的应用中提供一些优化思路和新的技术方案。

1. 首页界面层级划分

下图是货运App的首页,由一个主控制器管理两个核心业务模式:标准模式和物流模式。这些模式之间相互独立,并能根据业务需要支持更多的独立业务模式。每个模式都采用了常规的MVP架构。本文重点讨论标准模式下的MVP架构。

1.jpg

2. 标准模式业务下的MVP架构

下图为标准模式下的MVP架构图

2.png

3. 简化MVP架构

我们对首页标准模式的MVP架构进行抽简和说明:

3.png

4. 绑定后的引用关系表

我们会在HLLEGHomeVC控制器中进行子视图View及其对应的Presenter的创建和相互绑定:

- (id<HomePresenterInterface>)presenter {
    if (!_presenter) {
        _presenter = [[HomePresenter alloc] initWithView:self];
        PricePresenter *pricePresenter = [[PricePresenter alloc] initWithView:self.priceView parentView:self];
        _presenter.pricePresenter = pricePresenter;
        _presenter.categoryPresenter = [[BusinessPresenter alloc] initWithView:self.categoryView parentView:self];
               ...
        _presenter.navPresenter = [[NavBarPresenter alloc] initWithView:self.navBarView parentView:self];
    }
    return _presenter;
}

相互绑定后的引用关系表如下:

主视图View主Presenter子视图View子Presenter
主视图View-相互引用--
主Presenter相互引用--被引用
子视图View---相互引用
子Presenter-引用相互引用-

5. 模块间如何通信

由于对象在建立引用关系时,为减小类型耦合,并没有相互暴露具体类型,而是通过协议Interface来进行引用的,所以不能直接通过对象访问方法或属性的形式调用,每个对象能调用的只是协议里面定义好的方法,所以实际的通信过程中,每个节点的访问需在对应的Interface中定义好协议方法。

弊端:

  1. 胶水代码多

胶水代码的问题在于在实现某个功能时需要进行多个步骤。举个例子,当我们需要在点击后发起网络请求并在收到数据后刷新页面时,就需要在相关的Presenter和View的协议Interface中添加相应的方法。单个Presenter和View之间存在的胶水代码可能并不明显,但如果这个功能涉及到多个子View,那么就需要在不同的协议文件中都进行方法的声明。由此产生的胶水代码会显著增加。

下面就以网络请求刷新页面这个案例说明一下可能存在胶水代码的点:

假设页面上有一个按钮,当用户点击该按钮时触发网络请求来刷新页面:

  要实现这个功能,可能会有四个文件,背景色代表可能存在胶水代码的地方。这个案例相对简单,主要是为了方便理解胶水代码存在的地方。但是当业务变得复杂时,胶水代码会更加明显,现在来看一下这四个文件:

  1. 子视图TestView
  2. TestViewInterface
  3. TestPresenter
  4. TestPresenterInterface

  子视图TestView接收点击发起请求:

// TestView.m
- (void)clickButtonEvent {
    if (self.testPresenter responseToSelecor(@selector(requestWithData:))) {
        [self.testPresenter requestWithData:{}];
    } 
}

  TestPresenter执行请求获取到数据后告知子视图:

// TestPresenter.m
- (void)requestWithData:(NSDictionary *)params {
    [NetWorkTools requestWithInfo:params: ^(int ret, NSString *msg, id response) {
        if ([self.view responseToSelector(@selector(updateView:))]) {
            [self.view updateView:response];
        }
    }];
}
  1. 声明协议方法:

  在TextView对应的协议文件TestViewInterface.h声明:

// TextViewInterface.h
/// 属性页面
- (void)updateView:(NSDictionary *)response;

在TextPresenter对应的协议文件TestPresenterInterface.h声明:

// TestPresenterInterface.h

/// 发起请求数据
- (void)requestWithData:(NSDictionary *)params;

需要注意的是,胶水代码并不一定存在每个地方,根据具体的实现逻辑和需求,可能只存在某一部分或多部分中。胶水代码通常是为了连接不同部分的代码,使得它们能够协同工作以实现特定的功能。

  1. 向上访问链路长,不够直接

在MVP架构中,子View的Presenter引用了父视图的Presenter。当用户在子View中操作某个按钮时,需要触发父视图执行网络请求并刷新数据的操作。这个过程中就会涉及多级Presenter之间的调用。如果业务复杂、视图层级深(如存在主视图,子视图,孙视图等等),就可能会存在多级的访问链路,就可能会出现如下情况(由于视图层级太多时访问链路的表述可能过于复杂,这里先举只存在主视图/子视图的简单情况,便于大家能理解):

  1. 主view访问子presenter:self.childView.childPresenter
  2. 主presenter访问子presenter:self.view.childView.childPresenter
  3. 子view访问主presenter:self.presenter.parentPresenter
  4. 子presenter访问主View:self.view.parentView.presenter

6. 数据模型如何访问

由于模块间通信链路长和胶水代码过多的问题,获取彼此间的模型数据变得困难。举个例子,在某个业务中存在一个核心的聚合接口数据模型,通常情况下这个模型会被主Presenter引用。但是,当子View、子Presenter、孙View、孙Presenter,甚至更深层次的View和Presenter需要获取该核心模型中的某个字段数据时,就变得并不直接,可以说相当困难。

此外,有些子View控件可能没有定义Presenter,这就更加无法直接获取主Presenter,进而获取到指定模型中的数据。

这其实是在特定业务场景下的问题,例如在业务埋点中,有些埋点可能需要获取较多、较复杂的字段数据,这就需要定义额外的方法来支持埋点。由此数据获取就面临较大的困难。

4.png

四、MVP + Context架构方案

在MVP + Context架构中,Context主要作为一个中间件,功能是在各个Presenter之间及所有子视图View中进行信息的传递和数据共享,每个子视图都可以拥有自己独立的Presenter(除此之外,我们同样也可以构建一个全局共享的Presenter,各个子视图依据需求创建基于此公共Presenter的分类)。并由此,每个Presenter都可以通过Context与其他Presenter进行交互和数据共享,从而实现职责范围之外的处理子视图逻辑。

这种架构方案让每个子视图的Presenter具备更大的灵活性和自由度,它们可以独立管理各自的数据和逻辑,并通过Context与其他Presenter进行交互。这样一来,子视图的Presenter能够更容易地根据自身需求进行数据共享和访问。

1. 角色划分

在MVP+Context架构中,我们可以将模块划分成以下几种角色:

  1. Context中间件:在各个Presenter之间及所有子视图View中进行信息的传递和数据共享

  2. 主控制器VC:这部分主要负责绑定(Context, Presenter, 主视图View, Interactor),监听通知事件,管理生命周期,以及主视图的交接。

  3. 主视图View:这部分主要负责管理自身的业务逻辑,事件和数据则交由context进行传递处理。

  4. 主Presenter:

    1.   这里我们暂采用设定一个Presenter的思路,将多个业务概念按照分类进行划分,当然也可以设定每个子View的独立Presenter,Presenter之间通过Context进行通信(这里需要注意的是定义各自独立的Presenter后,要做好生命周期管理,以避免因为长期持有Presenter引用而导致的内存泄漏问题)
  5. Interactor:

处理Presenter中业务逻辑的具体实现,能将业务划分的更加细腻化,这个可以根据自己的需求考虑是否增加,要避免过度设计,比如登录业务可能会包含以下case

1: 校验手机号

2:校验密码

3:开始登录

2. 架构流程图

如下的流程图显示了MVP+Context的整体架构,在这个架构中,每个View视图(包括孙View)都可以直接获取到Context对象。通过Context对象,可以轻松地获取到主View对象、主Presenter对象以及主控制器VC。此外,业务数据模型引用在主Presenter对象中,通过Context对象可以随时获取到业务数据对象。这种设计满足了低耦合高聚合的开发规范,每个对象相互独立,但又能通过Context这个中间件进行交互。在这种架构下,各对象之间的关系通畅而有序。

5.png

3. 自动绑定

 在MVP架构中,我们都知道到在页面展示前需要进行角色的绑定,为了实现这一点,我们可以在控制器VC的viewDidLoad生命周期函数中执行中间件Context与主VC、主View、主Presenter和Interactor之间的相互绑定操作。

为了简化绑定操作并提高开发效率,我们可以将这些绑定动作抽象并封装在页面基HLLUBaseViewController中,通过在基类中提供绑定方法,我们能够轻松创建对应角色的对象,并自动完成绑定过程。

这种设计对于开发新模块非常有用。我们只需按需创建所需角色的类,并调用绑定方法。这样,我们能够快速完成角色的绑定,减少开发人员的工作量,提高开发效率。

通过将绑定操作提取到页面基类中,我们实现了代码的复用和一致性。这样,我们在开发新模块时只需专注于业务逻辑的实现,而无需重复关注角色的绑定过程,从而使开发变得更高效。

绑定可参考如下代码:


@interface HLLUBaseViewController ()
@property (nonatomic, strong) HLLContext *context;
@end 
@implementation HLLUBaseViewController

- (void)startBinding:(NSString*)moduleName {
    self.context = [[HLLContext alloc] init]; 
    // 创建presentor,将presentor放入context中,并相互绑定
    Class presenterClass = NSClassFromString([NSString stringWithFormat:@"HLL%@Presenter", moduleName]);
    if (presenterClass != NULL) {
        self.context.presenter = [presenterClass new];
        self.context.presenter.context = self.context;
    }
    
    // 创建Interactor,将Interactor放入context中,并相互绑定
    Class interactorClass = NSClassFromString([NSString stringWithFormat:@"HLL%@Interactor", moduleName]);
    if (interactorClass != NULL) {
        self.context.interactor = [interactorClass new];
        self.context.interactor.context = self.context;
    }
    
    // 创建主视图,并将主视图放入context中,并相互绑定
    Class viewClass = NSClassFromString([NSString stringWithFormat:@"HLL%@View", moduleName]);
    if (viewClass != NULL) {
        self.context.view = [viewClass new];
        self.context.view.context = self.context;
    }
    
    // 相互绑定
    self.context.presenter.view = self.context.view;
    self.context.presenter.baseController = self;
    self.context.interactor.baseController = self;
    self.context.view.presenter = self.context.presenter;
    self.context.view.interactor = self.context.interactor;
    
    self.context.view.frame = self.view.bounds;
    
    // 主视图移交
    self.view = self.context.view;
}
@end

4. 解决内存泄露

需要注意的是,在基类控制器进行绑定时,有可能会产生内存泄漏的问题。因此,当中间件Context与控制器VC相互引用时,我们需要中间件Context对控制器VC进行弱引用,以打破循环引用。

这样做是为了确保在控制器VC被释放时,中间件Context也能相应地释放,避免内存泄漏的问题。

6.png

下面是我们验证内存泄漏的结果。可以清楚地看到,当页面被销毁后,HLLContext、主VC、主Presenter以及所有子View视图都被正确地释放,没有产生任何内存泄漏问题。

5. 中间件无处不在

前面已经提到,在展示视图VC之前,我们需要将中间件Context与主视图View,主Presenter和Interactor等进行相互绑定。然而,当业务层级较深时,子视图、孙视图或更深层级的视图如何直接获取到中间件Context成为了一个问题。为了解决这个问题,在我们的处理方式中,我们使用Runtime运行时对象关联(objc_setAssociatedObject)来实现。当任意子视图需要获取中间件Context时,我们通过递归的方式不断向上级视图寻找,直到找到为止。一旦找到后,我们通过引用来持有该Context,以后就不需要再次向上级视图查找,这样也可以确保程序的性能。

需要注意的是,任何子视图在引用Context时应该进行弱引用(OBJC_ASSOCIATION_ASSIGN),以防止内存泄漏的情况发生。

参考代码如下:


  @implementation NSObject (HLLCT)

  - (void)setContext:(HLLContext *)object {
      objc_setAssociatedObject(self, @selector(context), object, OBJC_ASSOCIATION_ASSIGN);
  }

  - (HLLContext *)context {
      id curContext = objc_getAssociatedObject(self, @selector(context));
      if (curContext == nil && [self isKindOfClass:[UIView class]]) {
          UIView* view = (UIView*)self;
          UIView* sprView = view.superview;
          while (sprView != nil) {
              if (sprView.context != nil) {
                  curContext = sprView.context;
                  break;
              }
              sprView = sprView.superview;
          }
          // 缓存下来
          if (curContext != nil) {
              [self setContext:curContext];
          }
      }
      return curContext;
  }
  @end

6.  简化组件通信

在传统的MVP模式中,要实现组件之间的通信,需要编写大量的胶水代码。尤其是在业务复杂、页面层级众多的情况下,相互之间的通信成为一个巨大的问题。此外,获取数据模型也变得非常困难。特别是在处理复杂的业务埋点时,这一点更加明显。

然而,通过中间件Context的引入,这些问题都得到了解决。不论业务有多复杂,中间件Context能够提供直接的组件之间通信方式。而且,获取数据模型也变得非常简单。通过中间件Context,通信链路变得清晰明了,同时也提高了代码的可维护性和可扩展性。

8.png

最终通信链路表如下:

主View主PresenterInteractor控制器
主View--self.contextself.contextself.context
主Presenterself.context--self.contextself.context
Interactorself.contextself.context--self.context
控制器self.contextself.contextself.context--
任意子视图self.contextself.contextself.contextself.context

五、中间件如何支持Swift

我们已经详细介绍了基于 Objective-C 的 MVP + Context 架构。我相信大家对这个架构有了一定的了解。那么,在 Swift 项目中如何应用这个架构呢?实际上,原理非常相似。在接下来的内容中,我们将通过一个 Swift Demo 来说明。

  1. 绑定中间件

我们依然将绑定的操作放在模块的VC控制器中,将控制器VC,中间件,Presenter,主视图View以及Interactor进行相互绑定,参考如下代码:

public func startBinding(bundleName:String, prefixName: String) {
        self.isMVPStructor = true
        
        self.context = self.theContext
        
        // 绑定Presenter
        let presenterClass: AnyClass? = NSClassFromString("(bundleName).HLLU(prefixName)Presenter")
        if let presenterClass = presenterClass as? HLLUBasePresenter.Type {
            let presenter = presenterClass.init()
            self.context?.presenter = presenter
            self.context?.presenter?.context = self.context
        }
        
        // 绑定Interactor
        let interactorClass: AnyClass? = NSClassFromString("(bundleName).HLLU(prefixName)Interactor")
        if let interactorClass = interactorClass as? HLLUBaseInteractor.Type {
            let interactor = interactorClass.init()
            self.context?.interactor = interactor
            self.context?.interactor?.context = self.context
        }
        
        // 绑定view
        let viewClass: AnyClass? = NSClassFromString("(bundleName).HLLU(prefixName)View")
        if let viewClass = viewClass as? HLLUBaseView.Type {
            let view = viewClass.init()
            view.frame = view.frame
            self.context?.view = view
            self.context?.view?.context = self.context
        }
        
        // 相互绑定
        self.context?.presenter?.view = self.context?.view
        self.context?.presenter?.baseController = self
        self.context?.interactor?.baseController = self
        self.context?.view?.presenter = self.context?.presenter
        self.context?.view?.interactor = self.context?.interactor
        self.view = self.context?.view
    }

2. 中间件随处可得

在 Objective-C 中,我们通过为 NSObject 添加分类并结合对象关联(OBJC_ASSOCIATION_ASSIGN)来动态地给所有对象增加一个 context 属性,从而全局获取中间件 Context 对象。然而,Swift 本身并不支持分类。因此,我们可以利用 Swift 的协议(Protocol)来实现类似的功能。我们可以定义一个 HLLUContextProtocol 协议,其中包含一个计算属性 context。通过扩展该协议,为计算属性提供默认实现,然后使 NSObject 遵循该协议,以确保所有类都能获取到中间件。

另外,对于子视图 View,它会自动递归地查找上一级视图的中间件 Context。一旦找到,就立即返回并进行持有。这样,下次使用时就可以直接获取,而无需再向上查找,从而提高了程序的性能

参考代码如下:

private var kAssociatedObjectKey = "kAssociatedObjectKey"

protocol HLLUContextProtocol {
    var context: HLLUContext? { get set }
}
extension HLLUContextProtocol {
    var context: HLLUContext? {
        get {
            var curContext: HLLUContext? = objc_getAssociatedObject(self, &kAssociatedObjectKey) as? HLLUContext
            if let curContext = curContext {
                return curContext
            }
            guard let view = self as? UIView else {
                return curContext
            }
            var superView: UIView? = view.superview
            while superView != nil {
                if let context = superView?.context as? HLLUContext {
                    curContext = context
                    break
                }
                superView = superView?.superview
            }
            superView?.context = curContext
            return curContext
        }
        set {
            objc_setAssociatedObject(self, &kAssociatedObjectKey, newValue, .OBJC_ASSOCIATION_ASSIGN)
        }
    }
}
extension NSObject: HLLUContextProtocol {}

3. 基本属性有哪些

我们的架构中包含主控制器 VC、主视图 View、Presenter 和 Interactor 这些角色。它们之间存在相互引用关系,因此我们需要谨慎处理属性的引用关系,使用 weak 修饰符来避免内存泄漏。下面是这些角色属性修饰方式的参考:

  • 主控制器 VC:使用 weak 修饰符来引用 context
  • 主视图 View:使用 weak 修饰符来引用 presenter和interactor
  • Presenter:使用 weak 修饰符来引用 vc 和 view
  • Interactor:使用 weak 修饰符来引用 vc
  • Context:使用 strong 修饰符来引用 presenter、view、interactor

请注意,通过使用 weak 修饰符来设置这些属性,确保了对象之间的不存在强强引用,从而避免了可能导致内存泄漏。

class HLLUBaseViewController: UIViewController {    
    weak var context: HLLUContext?
}

class HLLUBaseInteractor: NSObject {
   weak var baseController: HLLUBaseViewController?
    
   required override init() {
        super.init()
    }
}

class HLLUBasePresenter: NSObject {
   weak var baseController: HLLUBaseViewController?
   weak var view: HLLUBaseView?
    
   required override init() {
        super.init()
    }
}

class HLLUBaseView: UIView {
    weak var presenter: HLLUBasePresenter?
    weak var interactor: HLLUBaseInteractor?
    
     var textView: HLLUTextView = {
        let view = HLLUTextView(frame: .zero)
        return view
    }()
    
    required override init(frame: CGRect) {
        super.init(frame: frame)
        setupView()
    }   
}

class HLLUContext: NSObject {
    var presenter: HLLUBasePresenter?
    var interactor: HLLUBaseInteractor?
    var view: HLLUBaseView?
}

六、基于Context架构的实践

我们以货拉拉APP的一个“确认订单”页面的场景作为例子来说明下这套架构的具体实践,下面是我们订单确认模块的整体流程图,不同子模块组件划分如下:

9.png

从上面整体流程图可以看到,我们在采用MVP+Context 的方案中只需要做好以下几个重要步骤:

步骤一:创建Context进行主模块类绑定

protocol MainPresenterInterface {
    var vehiclePresenter:VehiclePresenterInterface
    var pricePresenter:PircePresenterInterface
}

protocol MainViewInterface {
    var vehicleView:VehicleViewInterface
    var priceView:PirceViewInterface
}

protocol ContextInterface {
    var mainPresenter:MainPresenterInterface
    var mainView:MainViewInterface
    var extraData:Type
    var commonData:Type
}

class Module_xx_ViewController {
    var context: ContextType
    
    override func viewDidLoad() {
        super.viewDidLoad()
    }
    
    func bindContext() {
        //调用类似 5.1 章中的 bing 方法,将 context 和主presenter、 view 、子p/v进行绑定
        startBinding(bundleName:String, prefixName: String) 
    }
}
步骤二:模块直接的调用

组件化开发避免不了模块之间的通讯,通过MVP+Context方式,可以让调用链路简洁,在 5.2 章节中,我们通过扩展协议的方式,让每个模块的View类都能获取其Context的引用,因此不管我们的模块层级嵌套多少层,都能够立即获取到被通讯模块的 P/V:

//伪代码演示

//价格模块
class PricePresenter:PricePresenterInterface {
    func serviceChangeCalcilatePrice() {
        //服务变动触发计价
        ...
        //计价完成刷新服务对应价格
        self.view.context.mainView.serviceItemView.showServicePrice(priceInfo)
    }
}

//额外服务Presenter模块
class ServicePresenter:ServicePresenterInterface {
    //add service
    func addService(type:ServiceType) {
        //处理额外服务变动逻辑、修改数据
        ...
        // 通知计价模块计价
        self.view.context.mainPresenter.pricePresenter.triggerCalculatePrice()
    }
}

//额外服务View模块
class ServiceView:ServiceViewInterface {    
    func buttonCheckService(type:ServiceType) {
        self.presenter.addService(type)
    }
    func showServicePrice(priceInfo) {
        //展示价格信息
    }
}

七、快速生成MVP架构模版

  在基类 HLLBaseController 中,我们进行了模块间的相互绑定。然而,在绑定时,我们通过运行时创建了诸如 HLLHomeView、HLLHomePresenter 和 HLLHomeInteractor 等类。由于这些类名已经确定,为了避免在创建时出错,我们可以根据需要考虑通过脚本输入模块名来自动生成指定的 View、Presenter 和 Interactor。

  可以根据模块名自动生成指定类(这里以Objective-C开发模式下为例):

#!/bin/bash

# 输入模块名
read -p "Enter the module name: " moduleName

# 生成Presenter模块
presenterHeaderFile="${moduleName}Presenter.h"
presenterImplementationFile="${moduleName}Presenter.m"

echo "@interface ${moduleName}Presenter : NSObject" > $presenterHeaderFile
echo "@property (nonatomic, weak) HLLContext *context;" >> $presenterHeaderFile
echo "@property (nonatomic, weak) HLLBaseViewController *baseController;" >> $presenterHeaderFile
echo "@property (nonatomic, weak) id<HLLViewProtocol> view;" >> $presenterHeaderFile
echo "@end" >> $presenterHeaderFile

echo "@implementation ${moduleName}Presenter" > $presenterImplementationFile
echo "@end" >> $presenterImplementationFile

# 生成Interactor模块
interactorHeaderFile="${moduleName}Interactor.h"
interactorImplementationFile="${moduleName}Interactor.m"

echo "@interface ${moduleName}Interactor : NSObject" > $interactorHeaderFile
echo "@property (nonatomic, weak) HLLContext *context;" >> $interactorHeaderFile
echo "@property (nonatomic, weak) HLLBaseViewController *baseController;" >> $interactorHeaderFile
echo "@end" >> $interactorHeaderFile

echo "@implementation ${moduleName}Interactor" > $interactorImplementationFile
echo "@end" >> $interactorImplementationFile

# 生成View模块
viewHeaderFile="${classviewClassName}.h"
viewImplementationFile="${classviewClassName}.m"

echo "@interface ${classviewClassName} : UIView <HLLViewProtocol>" > $viewHeaderFile
echo "@property (nonatomic, weak) HLLContext *context;" >> $viewHeaderFile
echo "@property (nonatomic, weak) ${presenterClassName} *presenter;" >> $viewHeaderFile
echo "@property (nonatomic, weak) ${interactorClassName} *interactor;" >> $viewHeaderFile
echo "@end" >> $viewHeaderFile

echo "@implementation ${viewHeaderFile}" > $viewImplementationFile
echo "@end" >> $viewImplementationFile

八、结论

  本文主要对传统MVP架构在实际项目中的表现进行了分析,并针对其存在的不足进行了深度探讨和优化。我们提出了使用Context中间件的优化方案,并详细解释了在实际应用这种深度优化时需要注意的事项,包括如何进行自动绑定,以及如何处理内存泄露等。此外,我们也介绍了如何将该优化架构在Swift项目中进行应用,以及在实际的货拉拉App业务中如何实施这种架构的过程。

  每种架构都各有优缺点,架构优化是一个逐步前进的过程。在实际项目开发时,选择何种架构需要根据具体业务进行评估,既要能保证业务的高效迭代,又要避免过度设计。希望本文能为大家在实施自家App项目架构时提供一些参考和思路。