iOS

iOS设计模式

RT

Posted by Renchao on July 25, 2017

iOS设计模式

MVX

MVC

混乱的MVC架构

作为最出名并且应用最广泛的架构模式,MVC 并没有一个明确的定义,网上流传的 MVC 架构图也是形态各异,作者查阅了很多资料也没有办法确定到底什么样的架构图才是标准的 MVC 实现。

设计 MVC 的重要目的就是在人的心智模型与计算机的模型之间建立一个桥梁,而 MVC 能够解决这一问题并为用户提供直接看到信息和操作信息的功能

ASP.NET的MVC

MVC-with-ASP.NET

我们应该可以这么简单地理解:

  1. 控制器负责管理视图和模型;
  2. 视图负责展示模型中的内容;
Spring的MVC

与 ASP.NET 不同,Spring MVC 对于 MVC 架构模式的实现就更加复杂了,增加了一个用于分发请求、管理视图的 DispatchServlet:

MVC-with-Spring

在这里不再介绍 Spring MVC 对于 HTTP 请求的处理流程,我们对其中 Model、View 和 Controller 之间的关系进行简单的分析:

  1. 通过 DispatchServlet 将控制器层和视图层完全解耦;
  2. 视图层和模型层之间没有直接关系,只有间接关系,通过控制器对模型进行查询、返回给 DispatchServlet 后再传递至视图层;

虽然 Spring MVC 也声称自己遵循 MVC 架构模式,但是这里的 MVC 架构模式和 ASP.NET 中却有很大的不同。

iOS的MVC

iOS 客户端中的 Cocoa Touch 自古以来就遵循 MVC 架构模式,不过 Cocoa Touch 中的 MVC 与 ASP.NET 和 Spring 中的 MVC 截然不同。

下图为理想的MVC结构

Model-View-Controlle

img

对图的解释

双黄线表示禁止同行、白虚线表示自由穿越、白实现代表可以穿越但是有条件。

箭头的方向代表“发起会话”的方向,绿色箭头中,发起对话的是C,作出回答的是M和V。C可以主动要求M和V做事,但是M和V只能答复C,不能主动要求。也就是说,C可以导入M和V的API,进行主动活动。

C到V这个箭头上还有outlet(输出口),outlet可以看作是从C指向V的指针,它在C中被定义。outlet给我们提供了很大的 方便,它使我们在C的内部就可以轻松准确地向V施令。C可以拥有很多的outlet,可以不止一个,这也使它可以更高效的和V进行交流。

但是因为双黄线,使得M和V不能相互通信,只能通过C进行传递。C可以直接和M对话,M通过Notification和KVO机制和C间接通信。

C可以直接和V对话,通过outlet直接操作V,outlet直接对应到V中的控件,V通过action向C报告事件的发生。C是V的直接数据源(可能是C从M中取得并加工的)。C是V的代理,以同步V和C。

V如何向C发送消息?

  1. 目标操作(target-action):

    它是这样工作的,C会在自己的内部“悬挂”一个目标(target),如图中的红白相间的靶子,对应的,它还会分发一个操作(action,如图中的黄色箭头)给将要和它交流的视图对象(可能是屏幕上的一个按钮),当按钮被按时,action 就会被发送给与之对应的target,这样V就可以和C交流了。但是在这种情况下,V只是知道发送action给对应的target,它并不知道C中的类,也不知道它到底发送了什么。target-action是我们经常使用的方法。

  2. 委托(delegate):

    有时候,V需要和C进行同步,你知道,用户交互不仅仅是什么按按钮,划滑块,还有很多种形式。好了,让我们来看看图中的delegate黄色箭头,你发现箭头上又分出了四个小箭头:should,did,will,还有一个没标注的。绝大部分的delegate信息都是should,will,did这三种形式。和英文意思相对应,should代表视图对象将询问C中的某个对象“我应该这么做么?”,举个例子,有一个web视图,有人点击了一个链接,web视图就要问“我应该打开这个链接么?这样做安全么?”。这就是should信息。那will和did呢?will就是“我将要做这件事了”,did就是“我已经做了这件事”。

    C把自己设置为V的委托(delegate),它让V知道:如果V想知道更多的关于将如何显示的信息的话,就向C发送delegate信息。通过接受V发过来的delegate信息,C就会做出相应的协调和处理。还有一点,每个V只能有一个delegate。

  3. 数据源(datasource):

    V不能拥有它所要显示的数据,记住这点非常重要。V希望别人帮助它管理将要显示的数据,当它需要数据时,它就会请求别人的帮助,把需要的数据给它。再者,iPhone的屏幕很小,它不能显示包含大量信息的视图。看图中的datasource箭头,和delegate类似,V会发送cout,data at信息给C来请求数据。

实际开发中的MVC

img

在 iOS 中,由于 UIViewController 类持有一个根视图 UIView,所以视图层与控制器层是紧密耦合在一起的,这也是 iOS 项目经常遇到视图控制器非常臃肿的重要原因之一。

什么是标准的MVC

到底什么才是标准的 MVC 这个问题,到现在作者也没有一个确切的答案;不过多个框架以及书籍对 MVC 的理解有一点是完全相同的,也就是它们都将整个应用分成 Model、View 和 Controller 三个部分,而这些组成部分其实也有着几乎相同的职责:

  • 数据Model: 负责封装数据、存储和处理数据运算等工作
  • 视图View: 负责数据展示、监听用户触摸等工作
  • 控制器Controller: 负责业务逻辑、事件响应、数据加工等工作
臃肿的Controller

总体来说,Controller 层要负责以下的问题(包括但不仅限于):

  1. 管理根视图的生命周期和应用生命周期
  2. 负责将视图层的 UIView 对象添加到持有的根视图上;
  3. 负责处理用户行为,比如 UIButton 的点击以及手势的触发;
  4. 储存当前界面的状态;
  5. 处理界面之间的跳转;
  6. 作为 UITableView 以及其它容器视图的代理以及数据源;
  7. 负责 HTTP 请求的发起;

除了上述职责外,UIViewController 对象还可能需要处理业务逻辑以及各种复杂的动画,这也就是为什么在 iOS 应用中的 Controller 层都非常庞大、臃肿的原因了,而 MVVM、MVP 等架构模式的目的之一就是减少单一 Controller 中的代码。

MVP

MVP将MVC中的Controller换成了Presenter, 结构如下:

Standard-MVP

img

MVC 与 MVP 之间的区别其实并不明显,作者认为两者之间最大的区别就是 MVP 中使用 Presenter 对视图和模型进行了解耦,它们彼此都对对方一无所知,沟通都通过 Presenter 进行。

在 MVP 中,Presenter 可以理解为松散的控制器,其中包含了视图的 UI 业务逻辑,所有从视图发出的事件,都会通过代理给 Presenter 进行处理;同时,Presenter 也通过视图暴露的接口与其进行通信。

顾名思义:

  • Model:与MVC中的model没 有太大的区别。主要提供数据的存储功能,一般都是用来封装网络获取的json数据的集合。Presenter通过调用Model进行对象交互。
  • View:这里的View与MVC中的V又有一些小差别,这个View可以是viewcontroller、view等控件。Presenter通过向View传model数据进行交互。
  • Presenter:作为model和view的中间人,从model层获取数据之后传给view,使得View和model没有耦合。

MVP 的第一个主要变种就是被动视图(Passive View);顾名思义,在该变种的架构模式中,视图层是被动的,它本身不会改变自己的任何的状态,所有的状态都是通过 Presenter 来间接改变的。

视图成为了完全被动的并且不再根据模型来更新视图本身的内容,也就是说,不同于 MVC 中的依赖关系;在被动视图中,视图层对于模型层没有任何的依赖。因为视图层不依赖与其他任何层级也就最大化了视图层的可测试性,同时也将视图层和模型层进行了合理的分离,两者不再相互依赖。

被动视图的示意图中一共有四条线,用于表示 Model、View 和 Presenter 之间的通信:

Passive-View-with-Tags

  1. 当视图接收到来自用户的事件时,会将事件转交给 Presenter 进行处理;
  2. 被动的视图向外界暴露接口,当需要更新视图时 Presenter 通过视图暴露的接口更新视图的内容;
  3. Presenter 负责对模型进行操作和更新,在需要时取出其中存储的信息;
  4. 当模型层改变时,可以将改变的信息发送给观察者 Presenter;

在 MVP 的变种被动视图中,模型的操作以及视图的更新都仅通过 Presenter 作为中间人进行。

另一个 MVP 与 MVC 之间的重大区别就是,MVP(Passive View)中的视图和模型是完全解耦的,它们对于对方的存在完全不知情,这也是区分 MVP 和 MVC 的一个比较容易的方法。

MVVM

因为不管是MVC还是MVP都有一个问题, Controller和Presenter这个”协调员”太关键了, 代码稍微写多点, 它们就变成了Massive Controller或者Massive Presenter。

既然controller越来越臃肿,越来越难以维护,我们怎么去优化和瘦身呢?回头再仔细看看我们所谓的业务逻辑,是干什么的?无非就是根据几个数据得出一个数据用来控制view的显示。比如展示的是什么文案,按钮能不能响应,页面能不能跳转等等。那MVVM就干了这件事,帮忙分担一下controller里面的部分业务逻辑。MVVM更合理的应该叫做MV-CM。

我们可以将网络请求的接口,及返回的数据写在ViewModel里面,ViewModel再通知Controller来取得相应的数据,并显示在view上。还可以将逻辑计算等方法封装在ViewModel里面,供Controller调用。当然如果这部分计算复用性很高,还可以封装到其他公用的类里面。

而这个ViewModel将会是一个随时可以被其他功能模块调用的状态。而且,一个Controller可以使用一个或多个ViewModel。这样将会大大提高代码分复用性,及降低后期的维护。

MVVM 在使用当中,通常还会利用双向绑定技术,使得 Model 变化时,ViewModel 会自动更新,而 ViewModel 变化时,View 也会自动变化。而这个过程我们可以使用KVO和Notification来实现,但这样并不是最理想和高效的方式,所以我们需要结合ReactiveCocoa一起使用。

img

img

这个时候,controller将不再直接和真实的model进行绑定了,而通过ViewModel,viewModel进行持有真实的Model。

在 iOS 开发中实践 MVVM 的话,通常会把大量原来放在 ViewController 里的视图逻辑和数据逻辑移到ViewModel 里,从而有效的减轻了 ViewController 的负担。另外通过分离出来的 ViewModel 获得了更好的测试性,我们可以针对 ViewModel 来测试,解决了界面元素难于测试的问题。MVVM 通常还会和一个强大的绑定机制一同工作,一旦 ViewModel 所对应的Model 发生变化时,ViewModel 的属性也会发生变化,而相对应的 View 也随即产生变化。

单例

目的

在开发中经常会用到单例设计模式,目的就是为了在程序的整个生命周期内,只会创建一个类的实例对象,而且只要程序不被杀死,该实例对象就不会被释放。单例设计模式确保对于一个给定的类只有一个实例存在,这个实例有一个全局唯一的访问点。它通常采用延迟加载的方式在第一次用到实例的时候再去创建它。

作用

  • 在应用这个模式时,单例对象的类必须保证只有一个实例存在。许多时候整个系统只需要拥有一个的全局对象,这样有利于我们协调系统整体的行为。比如在APP开发中我们可能在任何地方都要使用用户的信息,那么可以在登录的时候就把用户信息存放在一个文件里面,这些配置数据由一个单例对象统一读取,然后服务进程中的其他对象再通过这个单例对象获取这些配置信息。这种方式简化了在复杂环境下的配置管理。
  • 有的情况下,某个类可能只能有一个实例。比如说你写了一个类用来播放音乐,那么不管任何时候只能有一个该类的实例来播放声音。再比如,一台计算机上可以连好几个打印机,但是这个计算机上的打印程序只能有一个,这里就可以通过单例模式来避免两个打印任务同时输出到打印机中,即在整个的打印过程中我只有一个打印程序的实例。

实现

  • 互斥锁
  • GCD的once方法

外观模式

1.外观模式针对复杂的子系统提供了单一的接口,不需要暴漏一些列的类和API给用户,你仅仅暴漏一个简单统一的API。

2.使用者完全不需要关心背后的复杂性。这个模式非常适合有一大堆很难使用或者理解的类的情况。

3.外观模式解耦了使用系统的代码和需要隐藏的接口和实现类。它也降低了外部代码对内部子系统的依赖性。当隐藏在门面之后的类很容易发生变化的时候,此模式就很有用了,因为当背后的类发生变化的时候,门面类始终保持了同样的API。

假如某个模块需要对一些数据做展示,这些数据的来源可能是不同数据库、可能是通过网络请求返回,总之载入数据的过程及其复杂,这时候可以合理运用外观模式,封装复杂的数据加载处理过程,对外公开简单的接口返回数据。如下图:

img

装饰器模式

装饰器模式在不修改原来代码的情况下动态的给对象(而不是类)增加新的行为和职责,它通过一个对象包装被装饰对象的方法来修改类的行为,这种方法可以做为子类化的一种替代方法。相对而言这种方式比子类继承更为灵活。

Category(类别)

Delegation(委托)

观察者模式

观察者模式本质上时一种发布-订阅模型,用以消除具有不同行为的对象之间的耦合,通过这一模式,不同对象可以协同工作,同时它们也可以被复用于其他地方Observer从Subject订阅通知,ConcreteObserver实现重现ObServer并将其重载其update方法。一旦Subject的实例需要通知Observer任何新的变更,Subject会发送update消息来通知存储在其内部类中所注册的Observer、在ConcreteObserverupdate方法的实际实现中,Subject的内部状态可被取得并进行后续处理。

通知(Notification)

在Cocoa Touch框架中NSNotificationCenterNSNotification对象实现了一对多的模型。通过NSNotificationCenter可以让对象之间进行通讯,即便这些对象之间并不认识。下面我们来看下NSNotificationCenter发布消息的方法:

NSNotification  *subjectMessage = [ NSNotification  notificationWithName:@"subjectMessage"  object: self];

NSNotificationCenter  * notificationCenter = [ NSNotificationCenter  defaultCenter];
    [notificationCenter postNotification:subjectMessage];

通过上面的代码我们创建了一个名为subjectMessageNSNotification对象,然后通过notificationCenter来发布这个消息。通过向NSNotificationCenter类发送defaulCenter消息,可以得到NSNotificationCenter实例的引用。每个进程中只有一个默认的通知中心,所以默认的NSNotificationCenter是个单例对象。如果有其他观察者定于了其对象的相关事件则可以通过以下的方法来进行操作:

    NSNotificationCenter  * notificationCenter1 = [ NSNotificationCenter  defaultCenter];
    [notificationCenter addObserver: self  selector: @selector(update:) name:@"subjectMessage"  object: nil ];

经过以上步骤我们已经向通知中心注册了一个事件并且通过selector制定了一个方法update:下面我们可以实现以下这个方法:

- (void)update:(NSNotification*)notification{
        if ([[notification name] isEqualToString:@"subjectMessage"]) {
            NSLog(@"%@",@"猴子派来的救兵去哪了?");
        }
}

当然最后如果我们需要对监听进行销毁:

- (void)dealloc {
    [[NSNotificationCenter defaultCenter] removeObserver:self];
}

KVO

KVO是Cocoa提供的一种称为键值观察的机制,对象可以通过它得到其他对象特定属性的变更通知。而这个机制是基于NSKeyValueObserving非正式些,Cocoa通过这个协议为所有遵循协议的对象提供了一种自动化的属性监听的功能。

虽然通知KVO都可以对观察者进行实现,但是他们之间还是略有不同的,由上面的例子我们可以看出通知是由一个中心对象为所有观察者提供变更通知,主要是广义上关注程序事件,而KVO则是被观察的对象直接向观察者发送通知,主要是绑定于特定对象属性的值。