概念
设计模式的概念是建筑师 Christopher Alexander 提出的,用于解决建筑行业经常发生的问题,后来这一概念被应用到其他领域,尤其是软件开发过程。说白了就是一套反复使用、多人知晓、经过分类的代码设计经验的总结。恰当的使用设计模式,可以使代码更加健壮、易复用和易扩展。此外还有一个好处,两个都会设计模式的人交流起来一定是高效的。打个比方,设计模式好比汉语中成语,用最简洁的语言传递信息。
在面向对象的世界里,有一些设计原则影响着设计模式,比如:“优先使用对象组合而不是类继承”和“针对接口编程而不是针对实现编程”。
例如,通过给程序的 变动部分 定义接口而对其封装和隔离,这些部分的变动就独立于程序的其他部分,因为他们不依赖任何细节。以后就可以变更或扩展这些可变的部分而不影响程序的其他部分。程序将因此能够灵活而可靠的进行变更,因为我们消除了部分与部分之间的依赖关系并减少了耦合。
设计模式起源-MVC
模型-视图-控制器(MVC)是 Cocoa Touch 中很多机制和技术的基础。在 MVC 设计模式中,对象根据功能分为三组,分别扮演模型、视图和控制器的角色。此外,MVC 也定义了三者之间的通信方式。
模型对象用于解释“是什么”,维护应用程序的数据,并定义操作数据的特定逻辑。模型是为了解释特定问题的数据类型,通常是可以复用的。比如通讯录中的列表页和详情页都用到了 Person 类。
视图对象用于向用户展示信息,可以响应用户操作,并懂得如何将自己展现在屏幕上。视图可以和一个模型的部分或是多个模型合作,同时视图也可以是复用的,比如系统中常用的 UITableView 等。
虽然视图和模型关系密切,但是在 MVC 的应用程序中,两者之间没有耦合。注释1
除非性能原因需要视图对数据进行缓存,否则不应该在视图中持有和存储它所展示的数据。
控制器作为模型和视图的中间人或协调人,它建立起沟通渠道,使视图得以知晓模型的变更而给与响应。
注释1
我们在使用 UITableView 的过程中经常对自定义Cell 进行数据绑定操作,代码如下:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
CustomModel *model = _dataSource[indexPath.row];
CustomCell *cell = [tableView dequeueReusableCellWithIdentifier:@"CustomCell"];
[cell configWithModel:model];
}
#import "CustomCell.h"
#import "CustomModel.h"
// 多数情况下这么做只是为了控制器的代码看上去更加简洁,但是这么做已经违反了 MVC 的设计原则。
// 根据 MVC 的设计原则,视图和模型是独立的,系统中的类也确实是这样的,比如 UITableView。
@implementation CustomCell
- (void)configWithModel:(CustomModel *)model {
}
@end
MVC 之间的通信
控制器是如何协调模型和视图工作的呢,也就是三者之间是如何通信的呢?控制器可以不受限制的访问模型(即持有模型对象,将其作为自己的属性),如图所示从控制器指向模型一个单向箭头。类似的从控制器到视图的通信也是不受限制的,如图所示。而模型和视图是完全独立的,互不持有注释1
。 控制器可以直接访问视图和模型,视图和模型相互独立。
那从视图向控制器是如何通信的呢?因为通常来说视图是可以复用的,所以不能对控制器知道太多,既然视图不知道哪个控制器持有了它,所以只能以一种“盲”的方式和控制器通信。其中一种方式叫做target/action
,控制器将自己作为视图的目标 target,同时提供一个动作 action,就像一支箭一样,告诉视图说“如果有人点击或是滑动了你,你就发送这个动作给我”。有时视图中发生的情况更复杂一些,控制器想要更具体的知道视图发生了什么,比如:will/should/did,以 UIScrollView 为例,我想在视图正在滚动和滚动结束时做一些事,这时 scrollView 自己没有办法去做,所以它委托
控制器来做这些事,这是另外一种“盲”的通信方式。再以 UITableView 为例,tableView 中是没有持有数据的,而是通过 dataSource
对其赋值,这也是一种特殊的代理。
那从模型到控制器是如何通信的呢?模型和视图一样,不会知道控制器太多。但是当它发生变化是,控制器要知道。也是有两种方式,一种叫做通知
,另一种叫做键值观察,简称 KVO
。
通常一个项目中会有很多组 MVC,不同组之间也会有交互发生。这时就可以把一组 MVC 看做是另外一组的视图,从而建立起整个项目。
设计原则
针对接口编程,而不是针对实现编程
Objective-C作为面向对象语言,自然可以通过继承的方式让我们能够从现成的类继承所需的大部分功能,从而快速定义新类。Objective-C 不支持多继承,但是有一种类似的东西叫做协议(protocol)。协议也是对象之间的一种合约,但本身不能实例化为对象。
协议并不定义任何实现,而只声明方法,以确定复合协议的类的行为。因此协议是只定义了抽象行为的“接口”。实现协议的类定义这些方法的实现,以执行真正的操作。另外一种定义通过抽象基类定义一些抽象方法,生成一些子类可以共享的默认行为。
使用协议有两个好处:
- 只要对象符合客户端所要求的接口,客户端就不必在意所使用对象的确切类型;
- 客户端只知道定义接口的协议或者抽象类,因此客户端对对象的类一无所知。
优先使用对象组合而不是类继承
类继承可以让我们实现复用,快速定义新类。继承也被称为白箱复用
(white-box reuse),因为父类内部的实现细节对子类是可见的。对象组合可以代替继承。由于对象的内部细节对其他对象是不可见,所以这种复用方式也被成为黑箱复用
(black-box reuse)。继承和组合各有优缺点。
继承
-
优点:
- 继承简单直接,关系在编译是静态定义;
- 被复用的实现易于修改。
-
缺点:
- 因为继承是在编译时定义,所以无法在运行时变更从父类继承来的实现;
- 子类的部分描述常常定义在父类中;
- 子类直接面对父类实现的细节,因此破坏了封装;
- 父类实现的任何变更都会强制子类也进行变更,因为它们的实现联系在了一起;
- 新的问题场景下继承的实现不再适用,必须重写父类或是继承来的实现。
组合
-
优点:
- 不会破坏封装,因为只通过接口来访问对象;
- 大大减少实现的依存关系,因为对象的实现是通过接口来定义的;
- 可以在运行时将任意对象替换为其他同类型的对象;
- 有助于保持类的封装以专注于单一任务;
- 类及其层次接口能够保持简洁,不至于过度膨胀而无法管理。
-
缺点:
- 设计中设计较多对象;
- 系统的行为将依赖与不同对象间的关系,而不是定义于单个类中;
tips
:尽管有以上缺点,对象组合仍有诸多好处。可以在部分使用继承来克服这些缺点。优先使用对象组合而不是类继承,并不是说完全不适用类继承。需要根据具体情况对如何用类和对象做出清晰的判断。如果系统设计合理,继承与组合可以互相配合。设计类时通常倾向与考虑对象组合。然后找出冗余行为,进行细化。如果找到冗余行为,也许意味着此处应该使用继承。
UML-类图
类图用来说明类的结构和类于类之间关系的示意图。Objective-C 中的协议和分类与其他面向对象语言略有不同。
1)依赖
依赖关系是类与类之间的联接。一个类依赖于另一个类的定义。如,一个人(Person)可以买车(Car)和房子(House),Person类依赖于Car和House的定义,因为Person引入了Car和House。与关联不同的是,Person类中没有Car和House的属性,Car和House的实例是以参量的方式传入到buy()方法中的。一般而言,依赖关系在Java语言中体现为局部变量,方法形参,或者对静态方法的调用。
2)关联
关联是类与类之间的联接,使一个类知道另一个类的属性和方法。关联可以是双向,也可以是单向的。一般使用成员变量来实现。
3)聚合
聚合是一种强的关联关系。是整体和个体之间的关系。例如,汽车类与引擎类,轮胎类之间的关系就是整体与个体之间的关系。与关联关系一样,聚合关系也是通过实例变量实现的。但是关联关系涉及的两个类在同一层次,而聚合关系中两个类处在不平等的层次上,一个代表整体,一个代表部分。
4)组合
组合也是关联关系的一种,一种比聚合关系强的关系。组合关系中的部分类不能独立于整体类存在。整体类和部分类有相同的生命周期。如Person类和Leg类。
5)继承/泛化
泛化和继承其实是一个逆过程 泛化就是有子类抽象出一个父类 而继承就是由父类具体化一个子类 例如足球比联赛跟什么西甲 意甲 英超之间就是泛化/继承的关系
6)组合和聚合的区别
组合和聚合都属于关联,所以它们之间难免有相似之处,区别举例来说明:
程老师的《大话》里举大那个大雁的例子很贴切 在此我就借用一下 大雁喜欢热闹害怕孤独 所以它们一直过着群居的生活 这样就有了雁群 每一只大雁都有自己的雁群 每个雁群都有好多大雁 大雁与雁群的这种关系就可以称之为聚合 另外每只大雁都有两只翅膀 大雁与雁翅的关系就叫做组合 有此可见 聚合的关系明显没有组合紧密 大雁不会因为它们的群主将雁群解散而无法生存 而雁翅就无法脱离大雁而单独生存——组合关系的类具有相同的生命周期
聚合关系图:
聚合关系图:
image
网友评论