设计模式
MVC?
Model:表示业务数据对象;View:展现数据的 UI;Controller:Model 跟 View 之间的粘合剂。一方面对 View 上的行为作出反应,通常会涉及到 Model 的更改;
另一方面将 Model 的改动反映到 View 上。由于 Controller 作为粘合剂的存在,View 和 Model 只需要跟 Controller 交互,而不知道另一方的存在。
这样,View 和 Model 作为独立可复用的组件,Controller 里处理业务逻辑。听起来这样的架构很清晰直观,实际应用中,MVC 对于不是很复杂的 App 也是非常高效的。
但对稍复杂些的 App,MVC 使用起来就会非常吃力。你可能听过 MVC 也被简称为 Massive View Controller,这就是原因所在 。
View Controller 承担的职责太多:网络请求、数据访问和存储、UI 的调整和组合、业务逻辑、View 的 delegate、data source、状态的维护与单一责任准则(Single Responsibility Principle
)背道而驰。
过于臃肿的 View Controller 使 App 的维护成本非常高。尽管把网络请求以及数据访问和存储放到了 Model 里,但由于对象边界的定义不够清晰,大部分 View Controller 依然很臃肿,上千行的 View Controller 很常见。
关于 View Controller 有个准则:如果一个 View Controller 超过了 300 行代码,那它一定做了责任范围以外的事。
更不幸的是由于一些职责移交给 Model,导致 Model 也变得臃肿起来。原来唯一可以做 Unit Test 的 Model 现在测试也很困难。
为解决 Massive View Controller 的问题,MVVM、VIPER 等架构应运而生。要发挥 MVVM 的优势,需要有 Reactive。Reactive 增加学习成本的同时,也让调试变得更困难。
VIPER虽然能平衡责任的分配,但由于引入过多对象,维护成本高。一个简单的页面也要求新增多个类和大量傻瓜代码。
MVVM ?
MVVM 架构是 MV(X) 里面最新的一个,它的出现已经考虑到了 MV(X) 模式之前所遇到的问题。
理论上来说,Model - View - ViewModel 看起来非常棒。View 和 Model 已经很熟悉了,中间人的角色VC控制器也很熟悉了,但是在MVVM架构里中间人的角色由VC控制器变成了ViewModel。
iOS 里面的 ViewModel 到底是个什么东西呢?ViewModel能持有Model进而修改Model,同时能够随时监听Model的属性数据变化进而来开启事件,因为ViewModel同时持有View,所以能够在监听Model变化开启的事件里直接对View进行更新。
绑定View和ViewModel的方法包括:
1、使用基于 KVO 的绑定库,比如 RZDataBinding 或者 SwiftBond。
2、使用全量级的函数式响应编程框架,比如ReactiveCocoa、RxSwift 或者 PromiseKit。
实际上,现在提到「MVVM」你应该就会想到 ReactiveCocoa,反过来也是一样。
虽然我们可以通过简单的绑定来实现 MVVM 模式,但是 ReactiveCocoa(或者同类型的框架)会让你更大限度的去理解 MVVM。
当然响应式编程框架(FRF)也有一点不好的地方负责的地方就是学习曲线陡峭,如果半懂不懂很容易让事情更糟。什么地方出错需要花费更多的时间去调试。
MVP?
MVP(被动变化的 View),在 MVC 里面 View 和 Controller 是耦合紧密的,但对 MVP 里面的 Presenter 来讲,它根本不关注 ViewController 的生命周期,而且 View 也能被简单 mock 出来,所以在 Presenter 里面基本没什么布局相关的代码,它的职责只是通过数据和状态更新 View。
在 MVP 架构里面,UIViewController 的那些子类其实是属于 View 的,而不是 Presenter。
这种区别提供了极好的可测性,但是这是用开发速度的代价换来的,因为你必须要手动的去创建数据和绑定事件。
MVP 架构拥有三个真正独立的分层,所以在组装的时候会有一些问题,而 MVP 也成了第一个披露了这种问题的架构。
因为我们不想让 View 知道 Model 的信息,所以在当前的 ViewController(角色其实是 View)里面去进行组装肯定是不正确的,我们应该在另外的地方完成组装。
比如,我们可以创建一个应用层(app-wide
)的 Router 服务,让它来负责组装和 View-to-View 的转场。
这个问题不仅在 MVP 中存在,在接下来要介绍的模式里面也都有这个问题。
MVP把大部分的职责都分配到了 Presenter 和 Model 里面,而 View 基本上不需要做什么(在上面的例子里面,Model 也什么都没做)。
可测性 - 简直棒,我们可以通过 View 来测试大部分的业务逻辑。
易用 - 就我们上面那个简单的例子来讲,代码量差不多是 MVC 架构的两倍,但是 MVP 的思路还是蛮清晰的。
MVP 架构在 iOS 中意味着极好的可测性和巨大的代码量。
还存在着另一种的 MVP - Supervising Controller MVP。这个版本的 MVP 包括了 View 和 Model 的直接绑定,与此同时 Presenter(Supervising Controller)仍然继续处理 View 上的用户操作,控制 View 的显示变化。
MVVM比较MVP?
-
划分性。MVVM 框架里面的 View 比 MVP 里面负责的事情要更多一些。MVVM是通过 ViewModel 的数据绑定来更新View的数据,而MVP只是把所有的事件统统交给 Presenter 去进行数据处理但并不负责更新View的数据,这也是MVP无法很好使用FRP响应式框架的原因。
-
可测性。在MVVM中, ViewModel 对View 是一无所知的,而View则完全依赖ViewModel处理数据并释放出处理结果的信号。因此MVVM的测试ViewModel里面的逻辑漏洞变得特别简单。
-
易用性。在实际的应用当中 MVVM 会更简洁一些。在 MVP 下你必须要把 View 的所有事件都交给 Presenter 去处理,而且需要手动的去更新 View 的状态;而在 MVVM 下,你只需要用使用KVO或是ReactiveCocoa绑定就可以解决。而且MVVM 所有框架中唯一自动更新视图的框架,因为在 View 上已经做了数据绑定,只要ViewModel持有的Model属性的特定属性值发送变化,自动就会发送数据数据变化的信号,然后更新View上UI控件的值。
VIPER ?
VIPER框架不属于任何一种 MV(X) 框架。
MV(X) 本质上是把模块分成三层,X无论是什么,都是器管道纽带连接的作用。
但是VIPER能够具备更细的颗粒度,将模块分成了五层。
1、Interactor(交互器) 处理数据(Entities)或网络请求的业务逻辑。
2、Presenter(展示器) 处理UI相关的业务逻辑,可以调用 Interactor 中的方法来属性数据。3、Entities(实体)作为一纯粹的数据对象只是用来规范数据和记录数据。
4、Router(路由)主要是联系 VIPER各个模块之间的纽带。
MV(X)对比VIPER ?
-
划分性。Controller/Presenter/ViewModel 的职责里面,只有 UI 的展示功能被转移到了 Presenter 里面。Presenter 不具备直接更改数据的能力。但是毫无疑问的,VIPER 在职责划分方面是做的最好的,毕竟颗粒度分了五层。
-
可测性。理所当然的,职责划分的越好,测试起来就越容易。
-
易用性。良好的职责划分就意味着一个小小的任务,可能就需要你为各种类写大量的接口,维护代价直线升高。
后台架构?
服务器端开发常用Service Oriented Architecture服务导向架构,把业务分成了多个逻辑独立的组件。
一个组件相当于一个 Service,封装了与其业务相关的功能,如 UserService 负责用户的注册、登入等,而 BabyService 有 Baby 的增加、移除、以及数据的记录等。
Service 是对整个架构纵向逻辑切分的结果。
抛开业务逻辑谈 Service 意义不大,Service 通常与数据库表的设计紧密相关。
横向的逻辑切分将 Baby App iOS 的架构自上而下切分成三个层(Layer):应用层(Application Layer)、服务层(Service Layer)、数据层(Data Access Layer)。
服务层和数据层把复杂的逻辑封装起来,作为 Framework 提供接口给上层调用。
应用层只能调用服务层暴露出来的接口,而不能直接调用数据层。
层次结构加强了可重用性和可测试性。应用层调用服务层提供的简单接口获得数据或者实行用户操作。
服务层也不需要知道数据层中网络请求,服务器同步,以及数据持久化的具体实现。
服务层,数据层,以及应用层都能很容易实现各自的单元测试(Unit Test)。Framework 是很棒的工具。把服务层和数据层打包成 Framework,不仅帮助构建解耦可重用的代码,同时 App 的结构和业务逻辑也更加清晰。
应用层(Application Layer)应用层也可以叫展示层(Presentation Layer),负责 UI 跟 展示逻辑。
从Code角度说,就是 UIView 跟 UIViewController 的集合。
复杂的逻辑都封装到了下层,UIViewController 就变得十分轻量。
View Controller主要负责三件事:
1、从 Service 获得数据(ViewModel)并展示
2、响应用户操作,调用相应的 Service 接口
3、监听 Service 层发出的消息,并执行相应操作,如更新 UI。
从 Service 获取的 ViewModel 实例并不是 NSManagedObject 或者其他持久化的 model 实例,跟 MVVM 中的 ViewModel 也不一样。
它只是简单的 Swift Struct,提供应用层需要的数据值。
使用 Struct 结构体的好处主要是:
1、值类型(Value Type): 简单、容易理解,线程安全
2、松耦合,减少 View Controller 之间可能的交互
3、减少了 Statefulness 和 Mutability似的应用运行更高效、占用内存更少。
使用 Struct 也就意味着想要底层持久化 Model 的更改反映到 UI 上,你必须通过 Service 再抓一次数据。
也许有人认为这是使用 Struct 的一个缺点,其实不是,这应该是优点。因为 Immutable 的 ViewModel ,让 View Controller 变得更加简单,你不用担心其他地方的代码会更改你的 ViewModel 实例。
调试起来也会更加方便,代码更容易理解、可读更高。
WWDC 中有好几个视频都对 Struct 的使用和优势进行了详解。服务层(Service Layer)服务层定义了一系列 Service 和供给应用层使用的 ViewModel。
Service 封装了 App 主要的业务逻辑,负责把底层持久化的 Model 和网络请求返回的 JSON 转换为 ViewModel,再提供给应用层使用。
这样的分离即加强了 Immutablility 和 Statelessness,也让应用层中的 ViewController 更轻量,只需几行 Service calls。
Service 虽然承担大部分业务逻辑,但一个 Service 通常也就 300 行左右的代码量,这得益于数据层的封装和抽象。
数据层(Data Access Layer)的作用是提供简化的数据访问接口,主要有 3 个模块:数据存储(Persistence
)、网络请求(Network
)、数据同步(Data Synchronization
)。
数据存储我们使用的是 Core Data,也可以用 Realm 或者其他数据库代替。
网络请求我们使用了 Moya 进行抽象,使 API 的设计和调用更简洁,并支持我们 Server 自定义的错误。数据同步模块,会自动同步本地和服务器端的用户数据。
MVC 因 Controller 的臃肿而遭到众人诟病。但其实 MVC 作为最基础的设计模式,展现了一个架构的精髓 - 抽象分离。
从整体看,数据层是 Model,业务层是 Controller,应用层是 View。
如果看细节的地方,应用层跟也务层提供的 ViewModel 也可以看做一个 MVC:ViewModel - UIViewController - UIView.
单例?
单例作为Cocoa框架中被广泛使用的核心设计模式之一。事实上,苹果开发者库把单例作为 "Cocoa 核心竞争力" 之一。应用中我们经常和单例打交道,比如 UIApplication 和 NSFileManager 等等。我们在开源项目、苹果示例代码和 StackOverflow 中见过了无数使用单例的例子。但这也不可避免地产生了不好的影响:
1、全局状态
使用全局可变的状态使得程序变得难以理解,难以调试。因为在面向对象编程的一大基本原则就是,对于可变的变量状态而言,作用域越小越能避免一个员工两个老板的误会。
2、隐式耦合
程序的任意模块都可以访问单例意味着任何和这个单例交互产生的副作用都会影响程序其他地方的任意代码。这就表示即使是两个完全独立的模块。也会因为单例提供的共享状态而产生副作用影响。由于单例具有全局和多状态的特性,导致隐式地在两个看起来完全不相关的模块之间建立了耦合。
3、单元测试
一个全局属性变量的值长期存在不消失(持久化状态)会严重干扰单元测试。比如说,A和B是两个不同的模块,但是都共同持有了一个单例对象的属性值,这时候就不满足单元测试的前提条件,两个模块相互独立。
4、生命周期
在程序中添加一个单例时, “永远只会有一个实例”的原则可能会被打破。例如账号注销用户切换,旧用户的存储在全局单例之中的所有状态数据都必须清理掉。同时也希望登录的新用户能够使用一个全新的全局单例,而不是简单的删除单例的数据值。那么就必须在用户注销的时候置空旧用户的单例类对象,新用户登录的时候实例化一个新的单例类对象。但是假如说现在用户单例类正在存储异步下载的图片,如果突然置空旧用户的单例类对象会造成继续下载的图片保留到了新用户的单例里。所以必须保证在置空旧用户的单例类对象之前将异步下载图片的操作给关闭掉。但是问题就在于置空旧用户的单例类对象时你很难准确地判断出单例实例的所有者(因为单例自己管理自己的生命周期),准确判断出“关闭”的单例是一个旧用户的单例将变得非常的困难。
杂谈?
雀圣和菜鸟正在观看同一个人打麻将,雀圣能够察觉到的内容会远远超过新手,并非雀圣火眼金睛,而是掌握了无形的武器,通过建立一整套思维抽象,雀圣能够透过现象看到本质,把对原始现象的感知转换成对目前局势简明扼要的理解。雀圣在看到牌局的一瞬间,就会联想到某种进攻战术的成功。这就叫观察能力。
最好学些架构的方式就是先经过照猫画虎式的时实践,然后系统地培训设计方法。
同一个应用里面,即便有几种混合的架构模式也是很正常的一件事情。比如:开始的时候,你用的是 MVC 架构,后来你意识到有一个特殊的页面用 MVC 做的的话维护起来会相当的麻烦;这个时候你可以只针对这一个页面用 MVVM 模式去开发,对于之前那些用 MVC 就能正常工作的页面,你完全没有必要去重构它们,因为两种架构是完全可以和睦共存的
MVVM 配合绑定机制效果最好,这个绑定机制就是RAC,使用MVC只能感受到RAC的部分好处。一说到绑定,自然就是想到了 KVO(Key-Value Observation)。然而,对于一个简单的绑定都需要很大的样板代码,更不用说有许多属性需要绑定了。所以ReactiveCocoa横空出世,虽然MVVM 并未强制我们使用 ReactiveCocoa。但是MVVM 在良好的绑定框架下更能释放出潜力。
模块如何划分?
桌面负责模拟控制台和状态显示、嵌入式负责设备控制和状态数据读取开发技术如何选型
如何适应可能发生的变化、交互机制
如何设计程序?
看别人的设计成果,体会别人的设计过程,试着自己来设计。讨论功能需求、讨论非功能需求中的质量属性需求、解决待完成任务的限制条件的约束需求。