独孤九剑--设计模式(iOS结构型篇)
独孤九剑--设计模式(iOS行为型篇)
前言
如果把开发看做是武林世界,底层原理、算法等就是内功心法,那编程语言、设计模式等无疑是外功招式;只专注一门达到精通级别,有人也能独领风骚;内外兼修那才是至尊王者方能称霸武林;
设计模式就是独孤九剑,剑谱由前辈实践总结,一招一式精妙绝伦,照着修炼亦可大有所为;
设计模式
设计模式就是前人经过实践,归纳总结的一套针对特定问题的代码设计解决方案;它就是一套解决特定问题的模板,在遇到类似问题时,可以直接套用对应的模板,高效、高质量的完成开发;
作用
- 降低开发成本,提高开发效率;
- 代码实现更优雅、更容易被他人理解;
- 提高代码复用率、可维护性、可拓展性;
设计原则
设计模式进行设计时需要遵循六大设计原则:
- 单一职责
类的设计尽量做到只有一个原因引起变化,一个类只专注做一件事 - 开闭原则
软件实体如类、模块和方法应该对扩展开放,对修改关闭;
功能迭代修改代码时,我们应尽量通过扩展软件实体的行为来实现变化,而不是通过修改内部原有的代码来完成变化; - 依赖倒置原则
高层模块不应该依赖低层模块,两者都应该依赖其抽象;
抽象不应该依赖细节;
细节应该依赖抽象;
在iOS中更加精简的定义就是面向协议编程
- 接口分离原则
具体类不应该依赖它不需要的接口;
类间的依赖关系应该建立在最小的接口上;
精简的定义就是:接口/协议尽量细分,如UITableViewDelegate/UITableViewDataSource,都是用于tableView的协议,但是系统按照数据源和事件响应细分了2个 - 里氏替换原则
所有使用基类的地方必须能透明地使用其子类,也即子类能替代基类使用;
如NSArray使用的地方都可以用NSMutableArray替代 - 迪米特法则
一个对象应该对其他对象有最少的了解;
A耦合了B,A应该只需知道耦合的对应接口,而不需要知道B更多的细节实现;
UML图
学习、使用设计模式需要读懂UML图(侧重类图),有必要的还需要自己能画图;
UML类图学习:
https://design-patterns.readthedocs.io/zh_CN/latest/read_uml.htmlUML图制作:
https://github.com/jgraph/drawio-desktop/releases
drawio支持流程图、UML多种图的制作,简单强大易用;
同时也支持在线编制:
https://app.diagrams.net
类型
- 创建型
原型模式、单例模式、工厂模式、建造者模式; - 结构型
适配器模式、代理模式、外观模式、桥接模式、装饰器模式、组合模式、享元模式 - 行为型
策略模式、观察者模式、模板方法模式、命令模式、备忘录模式、访问者模式、中介者模式、状态模式、责任链模式
主流的设计模式有20多种,下面只介绍下在iOS中可能比较重要的一些设计模式
单例模式 Singleton Pattern
单例模式确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例,这个类称为单例类,它提供全局访问的方法。
适用场景
- 系统只需要一个实例对象,如系统要求提供一个唯一的文件管理对象;或者需要考虑资源消耗太大而只允许创建一个对象。
- 客户调用类的单个实例只允许使用一个公共访问点,除了该公共访问点,不能通过其他途径访问该实例。
单例模式不应滥用,单例模式的对象是唯一的且可以全局使用,涉及到数据更改容易导致数据错乱、线程不安全等问题,将很难维护;另一个问题是单例会隐性地让毫不相关的类产生耦合等问题;
单例模式应该是iOS开发者最熟悉、最常用的一种模式,很容易理解,这里就不多费笔墨;但是对于熟悉的东西往往容易忽略一些细节;
Objective-C单例的简单实现
@implementation CameraManager
+ (CameraManager *)defaultManager {
static CameraManager *shareInstance = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
if (shareInstance == nil) {
shareInstance = [[CameraManager alloc] init];
}
});
return shareInstance;
}
@end
可能很多小伙伴编码就到此结束,但这里有个潜在的问题;其他语言,如C++,java等,构造方法可以隐藏。OC中的方法,实际上都是公开的,虽然我们提供了一个方便的类方法的访问入口,但是里面的alloc方法依旧是可以调用到的。也就是说依旧可以使用alloc的方式创建对象,每次alloc都是一个新的实例,也就违背单例模式只有一个实例的作用。
Objective-C单例的完善
- 无论是采用哪种方式创建,保证给出的对象是同一个;
在对象创建的时候,无论是alloc还是new,都会调用到allocWithZone
方法。在通过拷贝创建对象时,都会会调用到copyWithZone
或mutableCopyWithZone
方法。因此,可以重写这些方法,让创建的对象唯一。
+ (CameraManager *)defaultManager {
....
shareInstance = [[super allocWithZone:nil] init];
....
return shareInstance;
}
+ (instancetype)allocWithZone:(struct _NSZone *)zone {
return [CameraManager defaultManager];
}
- 通过其他方式创建对象时,直接抛错禁止创建
可以是编译时抛错,还可以是运行时抛出异常;
- 编译报错
@interface CameraManager : NSObject
+ (CameraManager *)defaultManager;
+ (instancetype) alloc __attribute__((unavailable("call sharedInstance instead")));
+ (instancetype) new __attribute__((unavailable("call sharedInstance instead")));
- (instancetype) copy __attribute__((unavailable("call sharedInstance instead")));
- (instancetype) mutableCopy __attribute__((unavailable("call sharedInstance instead")));
@end
- 运行时抛异常
- (instancetype)init {
NSAssert(NO, @"There can only be one CameraManager instance.");
// 或者
// [NSException raise:NSInternalInconsistencyException format:@"There can only be one CameraManager instance."];
return nil;
}
系统库中就有很多单例应用:UIApplication、NSFileManager、NSNotificationCenter 等等...
实测发现NSFileManager、NSNotificationCenter都没有处理其他alloc的情况;UIApplication使用的是运行时抛错的方式禁用其他创建方式;
Swift单例的实现
基于语言特性,Swift实现单例将特别简单、完美
class CameraManager {
static let defaultManager = CameraManager()
private init() {}
}
工厂模式
工厂模式主要是为创建对象提供过渡接口,将对象的创建具体细节使用工厂类封装屏蔽起来,提高调用的灵活性;另外对象的创建和对象本身业务处理分离,降低了系统的耦合度,使得两者修改起来都相对容易;
工厂模式可以分为三类:简单工厂模式、工厂方法模式、抽象工厂模式
简单工厂模式 Simple Factory Pattern
简单工厂模式专门定义一个工厂类来负责创建其他类的实例,工厂类可以根据参数的不同返回不同类的实例。被创建的实例通常都具有共同的父类(或实现同一个接口/协议)。
UML类图
- Factory:工厂角色
负责实现创建所有实例的内部逻辑 - Product:抽象产品角色
所创建的对象的父类(抽象类、接口、协议) - ConcreteProduct:具体产品角色
所创建的对象具体类的实例。
示例
App需要使用不同的外接相机,根据连接的wifi名区分不同相机并实例化:
Product: 相机公有协议:
@protocol CameraProtocol <NSObject>
@optional
- (void)connect;
- (void)take;
@end
ConcreteProduct: 各个具体相机对象
@implementation XhwCamera
- (void)connect {
NSLog(@"xhw connect");
}
- (void)take {
NSLog(@"xhw take");
}
@end
@implementation AzyCamera
- (void)connect {
NSLog(@"azy connect");
}
- (void)take {
NSLog(@"azy take");
}
@end
// 相机工厂类 负责创建不同相机对象 (对应Factory)
@implementation CameraFactory
+ (id<CameraProtocol>)createCameraWifi:(NSString *)wifi {
if ([wifi isEqualToString:@"xhw"]) {
return [[XhwCamera alloc] init];
} else if ([wifi isEqualToString:@"azy"]) {
return [[AzyCamera alloc] init];
}
return nil;
}
@end
调用方使用:
id<CameraProtocol> camera = [CameraFactory createCameraWifi:@"xhw"];
[camera connect];
[camera take];
适用场景
-
优点
- 工厂类封装了必要的判断逻辑,决定创建哪个类;使用者不必关心具体的创建逻辑,实现了解耦;同时代码也更容易维护
- 当存在类似功能的不同类时,使用者使用时容易搞混;交由工厂类统一管理,更方便使用者调用;
-
缺点
- 工厂类集中了所有产品创建逻辑,一旦不能工作,整个系统将会受到影响
- 违背“开闭原则”,一旦添加新产品就必须修改工厂类的逻辑;当产品类越来越多,工厂类的逻辑也会更多更乱,不利于系统扩展和维护
- 简单工厂模式由于使用了静态工厂方法,造成工厂角色无法形成基于继承的等级结构。
基于以上优缺点,简单工厂模式适用场景:
- 使用方只知道传入工厂类的参数,并不关心创建对象的逻辑时;
- 工厂类负责创建的产品类比较少时。
工厂方法模式 Factory Method Pattern
工厂方法模式是在简单工厂模式的基础上,进一步抽象化和推广的模式。工厂方法模式里不再只由一个工厂类决定实例化哪些产品类,而是把产品类的创建交给抽象工厂的子类去完成。
简单工厂模式中工厂类做了大量的创建逻辑,一个工厂耦合多个产品类;工厂方法模式抽象了工厂类,具体的工厂类只负责创建一个产品类,避免了简单工厂的一些缺点;
UML类图
Product:抽象产品角色;ConcreteProduct:具体产品角色
Factory:抽象工厂角色;ConcreteFactory:具体工厂角色
示例
还是简单工厂那个例子,现在将其重构为工厂方法:
抽象工厂:
@protocol CameraFactoryProtocol <NSObject>
// 实例方法、类方法均可以
// 实例方法
//- (id<CameraProtocol>)createCamera;
// 类方法 创建具体的相机(产品)
+ (id<CameraProtocol>)createCamera;
@end
具体的工厂(有多少种产品就多少种工厂):
@implementation XhwCameraFactory
+ (id<CameraProtocol>)createCamera {
return [[XhwCamera alloc] init];
}
@end
@implementation AzyCameraFactory
+ (id<CameraProtocol>)createCamera {
return [[AzyCamera alloc] init];
}
@end
具体产品类不用更改;
调用方使用:
id<CameraProtocol>camera = [XhwCameraFactory createCamera];
[camera connect];
[camera take];
适用场景
-
优点
- 包含了简单工厂的优点
- 符合开闭原则:新增一种产品时,只需要扩展相应的具体产品类和具体的工厂类,无需修改原有工厂类;
- 符合单一职责原则:每个具体工厂类只负责创建对应的产品
- 可以使用实例工厂方法,可以形成基于继承的等级结构
-
缺点
- 在添加新产品时,需要编写新的具体产品类,而且还要提供与之对应的具体工厂类,系统中类的个数将成对增加,在一定程度上增加了系统的复杂度,有更多的类需要编译和运行,会给系统带来一些额外的开销
- 一个具体工厂只能创建一种具体产品
基于以上优缺点,工厂方法模式适用场景:
- 基于简单工厂的适用场景;
- 工厂只会对应一种产品;
- 后续可能扩展产品
抽象工厂模式 Abstract Factory Pattern
工厂方法模式在一定程度上优化了简单工厂的缺点,但是其一个具体工厂只能创建一种具体产品;抽象工厂模式就是对工厂方法模式的进一步优化;
抽象工厂模式设计了多个抽象产品类,每个抽象产品类可以派生出多个具体产品类。一个抽象工厂类可以派生出多个具体工厂类。每个具体工厂类可以创建多个具体产品的实例;
UML类图
AbstractFactory:抽象工厂;ConcreteFactory:具体工厂
AbstractProduct:抽象产品;Product:具体产品
示例
基于工厂方法模式的例子,现在增加一个需求:除了连接相机外还需要支持连接不同厂家的音响设备;还是Xhw和Azy两个厂家,按照工厂方法模式的设计,那么就需要新建一套音响抽象工厂类、2个具体的音响工厂类、1个抽象产品类、2个具体产品类;调用方使用时,也同样需要先创建另一个工厂;
其实不管是相机设备,还是音响设备,虽然是不同产品但是都是相同厂家;相机和音响都属于外接设备,完全可以在高一层的抽象出外接设备
工厂;这时相当于把相机和音响当做了一个产品族
;
实现代码:
- 抽象工厂 (更高抽象)
@protocol PeripheralFactory <NSObject>
// 外接设备目前支持 Camera、Speaker两种产品
- (id<CameraProtocol>)createCamera;
- (id<SpeakerProtocol>)createSpeaker;
@end
- 新建新增的抽象产品
@protocol SpeakerProtocol <NSObject>
- (void)connect;
- (void)play;
@end
- 具体工厂 (创建对应的产品)
@implementation XhwFactory
- (nonnull id<CameraProtocol>)createCamera {
return [[XhwCamera alloc] init];
}
- (nonnull id<SpeakerProtocol>)createSpeaker {
return [[XhwSpeaker alloc] init];
}
@end
@implementation AzyFactory
- (nonnull id<CameraProtocol>)createCamera {
return [[AzyCamera alloc] init];
}
- (nonnull id<SpeakerProtocol>)createSpeaker {
return [[AzySpeaker alloc] init];
}
@end
使用:
AzyFactory *fc = [[AzyFactory alloc] init];
id<CameraProtocol> camera = [fc createCamera];
[camera connect];
id<SpeakerProtocol> speaker = [fc createSpeaker];
[speaker connect];
[speaker play];
适用场景
先引入2个概念:
产品等级结构
:产品等级结构即产品的继承结构,如一个抽象类是电视机,其子类有海尔电视机、海信电视机、TCL电视机,则抽象电视机与具体品牌的电视机之间构成了一个产品等级结构,抽象电视机是父类,而具体品牌的电视机是其子类。
产品族
:在抽象工厂模式中,产品族是指由同一个工厂生产的,位于不同产品等级结构中的一组产品,如海尔电器工厂生产的海尔电视机、海尔电冰箱,海尔电视机位于电视机产品等级结构中,海尔电冰箱位于电冰箱产品等级结构中。
- 优点
- 包含工厂方法模式的优点
- 具体工厂可以创建多个产品
- 当一个产品族中的多个对象被设计成一起工作时,它能够保证客户端始终只使用同一个产品族中的对象。这对一些需要根据当前环境来决定其行为的软件系统来说,是一种非常实用的设计模式
- 符号开闭原则:增加新的具体工厂和产品族很方便,无须修改已有系统。
- 缺点
- 开闭原则的倾斜性(增加新的工厂和产品族容易,增加新的产品等级结构麻烦),抽象工厂角色中设计了所有可能被创建的产品集合,要支持新种类的产品就意味着要对该接口进行扩展,而这将涉及到对抽象工厂角色及其所有子类的修改;
- 产品等级结构、产品族较多时,代码量也成倍增加;
基于以上优缺点,抽象工厂模式适用场景:
- 基于工厂方法模式的适用场景;
- 系统有多个系列产品,而系统中只消费其中某一系列产品
iOS系统中的工厂模式
iOS中有类簇的概率,其实它就是使用了工厂模式;
如 NSArray,NSMutableArray;NSNumber等;
id arrAlloc = [NSArray alloc];
NSArray *arr = [arrAlloc init];
id marrAlloc = [NSMutableArray alloc];
NSMutableArray *marr = [marrAlloc init];
NSLog(@"%@", NSStringFromClass([arrAlloc class]));
NSLog(@"%@", NSStringFromClass([arr class]));
NSLog(@"%@", NSStringFromClass([marrAlloc class]));
NSLog(@"%@", NSStringFromClass([marr class]));
输出结果:
__NSPlaceholderArray
__NSArray0
__NSPlaceholderArray
__NSArrayM
NSArray真实类型是__NSArray0,NSMutableArray真实类型
__NSArrayM;在alloc分配内存后,创建的是一个中间对象__NSPlaceholderArray,等到init时才会动态决定创建对应的对象;
Apple并没有开源Foundation源码,但是可以通过GNUStep的Foundation代码窥探其实现,可以发现的确是将创建对象的工作转交给了中间对象;
造者模式 Builder Pattern
将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。
建造者模式是一步一步创建一个复杂的对象,它允许用户只通过指定复杂对象的类型和内容就可以构建它们,用户不需要知道内部的具体构建细节;
UML类图
Builder:抽象建造者,ConcreteBuilder:具体建造者
Director:指挥者,Product:产品角色
示例
我们再完善下上面示例的外接相机代码;
外接相机在connect前,需要保证内部模块都初始化完成;相机现在分为3大部分:拍摄模块,wifi模块,存储模块;
@interface XhwCamera : NSObject<CameraProtocol>
// 实际开发中这3部分比较复杂, 现在为了演示简单都使用字符串替代
@property (nonatomic, strong) NSString *shootModule;
@property (nonatomic, strong) NSString *wifiModule;
@property (nonatomic, strong) NSString *storageModule;
// 拍摄配置 如分辨率、帧率等
@property (nonatomic, strong) NSString *shootConfig;
@end
简单做法
在Camera对象init时,将这3部分一起初始化;
但想象下,如果shootModule,wifiModule,storageModule都比较复杂,需要写一大堆配置代码时Camera的init将也会臃肿复杂:
implementation XhwCamera
- (instancetype)init {
//...
//...
_shootModule = ...
//...
//...
_wifiModule = ...
}
当shootModule,wifiModule,storageModule需要调整依赖关系或者顺序关系时,还有当这些模块初始化需要由使用者传入参数时,XhwCamera将也很难维护;
使用建造者模式
- 抽象建造者,声明建造需要的接口
@protocol CameraBuilderProtocol <NSObject>
- (void)buildShootModule; // 拍摄模块
- (void)buildWifiModule; // wifi连接模块
- (void)buildStorageModule; // 存储模块
// 返回构建对象
- (id<CameraProtocol>)build;
@end
- 具体建造者:xhw相机
@implementation XhwCameraBuilder
- (instancetype)init {
if (self = [super init]) {
_camera = [[XhwCamera alloc] init];
}
return self;
}
- (void)buildShootModule {
_camera.shootConfig = @"默认配置";
//.... 复杂过程
_camera.shootModule = @"拍摄模块初始完成";
}
- (void)buildStorageModule {
//.... 复杂过程
_camera.storageModule = @"存储模块初始完成";
}
- (void)buildWifiModule {
//.... 复杂过程
_camera.wifiModule = @"wifi模块初始完成";
}
- (nonnull id<CameraProtocol>)build {
return _camera;
}
@end
- 构建的Director,控制builder项及顺序、依赖等
@implementation CameraDirector
- (void)constuctBuilder:(id<CameraBuilderProtocol>)builder {
[builder buildShootModule];
[builder buildWifiModule];
[builder buildStorageModule];
}
@end
- 使用
XhwCameraBuilder *builder = [[XhwCameraBuilder alloc] init];
CameraDirector *director = [[CameraDirector alloc] init];
[director constuctBuilder:builder];
id<CameraProtocol> camera = [builder build];
[camera connect];
完整的建造者模式
如果拍摄的配置需要由客户设置,而不是使用默认的;builder需要支持参数设置;
以拍摄增加 分辨率、帧率 配置为例,代码重构如下:
- builder增加设置的接口
- (void)setShootConfig:(NSString *)config {
_camera.shootConfig = config;
}
- CameraDirector增加传入参数
- (void)constuctBuilder:(id<CameraBuilderProtocol>)builder resolution:(NSString *)resolution frameRate:(NSString *)frameRate {
NSString *config = [NSString stringWithFormat:@"拍摄配置:分辨率=%@,帧率=%@", resolution, frameRate];
// buildShootModule初始化需要在shootConfig之后 这个由Director控制了
[builder setShootConfig:config];
[builder buildShootModule];
[builder buildWifiModule];
[builder buildStorageModule];
}
另一种方式,可以为不同的配置创建不同的具体建造者;
在使用时,只需要更换不同的建造者就可以创建不同配置的产品实例;
比如新建一个 高配 的相机builder,相机所有配置都是用更高性能的设置,
@implementation XhwCameraHeightBuilder
- (void)buildShootModule {
_camera.shootConfig = @"拍摄配置:分辨率=1280x720,帧率=30";
_camera.shootModule = @"拍摄模块初始完成";
}
- (void)buildStorageModule {
// ...默认高级配置
_camera.storageModule = @"存储模块初始完成";
}
适用场景
-
优点
- 易于解耦
将产品本身与产品创建过程进行解耦,可以使用相同的创建过程来得到不同的产品;符号依赖倒置原则。 - 易于精确控制对象的创建
将复杂产品的创建步骤分解在不同的方法中,使得创建过程更加清晰 - 易于拓展
增加新的具体建造者无需修改原有类库的代码,易于拓展,符合开闭原则。
- 易于解耦
缺点
如果产品的内部变化复杂,可能会导致需要定义很多具体建造者类来实现这种变化,导致系统变得很庞大。
基于以上优缺点,该模式适用场景:
- 需要生成的产品对象有复杂的内部结构,这些产品对象通常包含多个子模块(或成员属性)。
- 需要生成的产品对象的属性相互依赖,需要指定其生成顺序。
- 隔离复杂对象的创建和使用,并使得相同的创建过程可以创建不同的产品。
参考:
https://www.jianshu.com/p/6e5eda3a51af
https://design-patterns.readthedocs.io/zh_CN/latest/index.html