谈谈设计模式
什么是设计模式?
维基百科中的设计模式 (计算机):
在软件工程中,设计模式(design pattern)是对软件设计中普遍存在(反复出现)的各种问题,所提出的解决方案。
设计模式并不直接用来完成代码的编写,而是描述在各种不同情况下,要怎么解决问题的一种方案。面向对象设计模式通常以类别或对象来描述其中的关系和相互作用,但不涉及用来完成应用程序的特定类别或对象。设计模式能使不稳定依赖于相对稳定、具体依赖于相对抽象,避免会引起麻烦的紧耦合,以增强软件设计面对并适应变化的能力。
并非所有的软件模式都是设计模式,设计模式特指软件“设计”层次上的问题。还有其他非设计模式的模式,如架构模式。同时,算法不能算是一种设计模式,因为算法主要是用来解决计算上的问题,而非设计上的问题。
可以这么说,计算机中设计模式指的是一套广为人知、被反复使用、经过分类编目的代码设计经验。使用设计模式是为了可重用代码,让代码更容易被他人理解,最重要的是保证代码可靠性。
设计模式七大原则
一篇很好的iOS设计模式原则介绍:iOS开发 - 面向对象设计的六大设计原则(附 Demo & UML类图)
开闭原则
开闭原则的核心是:对扩展开放,对修改关闭。在程序需要进⾏拓展的时候,不能去修改原有的代码,⽽是要扩展原有代码,实现⼀个热插拔的效果。所以⼀句话概括就是:为了使程序的扩展性好,易于维护和升级。想要达到这样的效果,我们需要使⽤接⼝和抽象类等,大部分具体设计模式中反复应用这一原则。
单⼀职责原则
一个类只做一件事。每个类应该实现单⼀的职责,如若不然,就应该把类拆分。
eg:UIView负责事件的传递、响应,CALayer负责视图的显示、动画,他们各自都有自己的单一职责。
里氏替换原则
里氏替换原则的主要内容:任何基类可以出现的地方,子类⼀定可以出现。该原则是继承复⽤的基⽯,只有当衍⽣类可以替换掉基类,软件单位的功能不受到影响时,基类才能真正被复用,而衍生类也能够在基类的基础上增加新的行为。原则上,⼦类对父类的方法尽量不要重写和重载,因为⽗类代表了定义好的结构,通过这个规范的接⼝与外界交互。除非万不得已,⼦类不应该随便破坏它。
eg:
父类
- (void)carName {
NSLog(@"小汽车");
}
子类
//遵循里氏替换原则
- (void)carColor {
NSLog(@"红色的");
}
//违背里氏替换原则,影响到了父类原有功能
- (void)carName {
NSLog(@"小汽车,红色的");
}
依赖倒转原则
面向接口编程,依赖于抽象而不依赖于具体。高层模块不应该依赖底层模块,二者都应该依赖其抽象;我们可以依赖抽象类也可以依赖接口,但是不要依赖具体的子类或者实现类。依赖倒转原则是开闭原则的基础。
eg:
违背依赖倒转原则
@implementation LiteraryClassic
- (void)read {
NSLog(@"读文学经典");
}
@end
@implementation XiaoMing
- (void)read:(LiteraryClassic *)literaryClassic {
[literaryClassic read];
}
@end
这样设计的XiaoMing类没有遵循依赖倒转原则,没有中间接口,导致只能实现“读文学经典”一种功能。
遵循依赖倒转原则
//IRead协议
@protocol IRead<NSObject>
- (void)read;
@end
//LiteraryClassicNew类遵循IRead协议
@interface LiteraryClassic : NSObject<IRead>
@end
@implementation LiteraryClassic
- (void)read {
NSLog(@"读文学经典");
}
@end
//NovelNew类遵循IRead协议
@interface Novel : NSObject<IRead>
@end
@implementation Novel
- (void)read {
NSLog(@"读小说");
}
@end
//IReader协议
@protocol IReader<NSObject>
- (void)read:(id<IRead>)iread;
@end
//XiaoMing类遵循IReader协议
@interface XiaoMing : NSObject<IReader>
@end
@implementation XiaoMing
- (void)read:(id<IRead>)iread {
[iread read];
}
@end
测试功能
id <IReader> ireader = [[XiaoMing alloc] init];
id <IReader> read = [[Novel alloc] init];
[ireader read:read];
read = [[LiteraryClassic alloc] init];
[ireader read:read];
输出
读小说
读文学经典
这样的设计遵循了依赖倒转原则,有了中间接口,依赖于抽象而不依赖于具体,可以实现“读文学经典”、“读小说”两种功能,当有更多需求时还能进行扩展。
接口隔离原则
每个接口中不存在子类用不到却必须实现的方法,如果不然,就要将接口拆分。使⽤多个隔离的接口,⽐使用单个接口(多个接口⽅法集合到⼀个的接口)要好。
eg:
//违背依赖倒转原则,无法单独控制行驶和开空调
- (void)usedCar {
NSLog(@"行驶");
NSLog(@"开空调");
}
//遵循依赖倒转原则,单独控制行驶和开空调
- (void)usedCar {
NSLog(@"行驶");
}
- (void)openTheAircondition {
NSLog(@"开空调");
}
迪米特法则
迪米特法则的核心为:⼀个类对自己依赖的类知道的越少越好。也就是说⽆论被依赖的类多么复杂,都应该将逻辑封装在⽅法的内部,通过public方法提供给外部。这样当被依赖的类变化时,才能最小的影响该类。
最少知道原则的另⼀个表达⽅式是:只与直接的朋友通信。类之间只要有耦合关系,就叫朋友关系。耦合分为依赖、关联、聚合、组合等。我们称出现成员变量、⽅法参数、⽅法返回值中的类为直接朋友。局部变量、临时变量则不是直接的朋友。我们要求陌生的类不要作为局部变量出现在类中。
eg:
//违背迪米特法则
//类A
- (void)isUseCar {
if ([B.person isEqualToString: @"男人"]) {
NSLog(@"开车");
} else if ([B.person isEqualToString: @"女人"]) {
NSLog(@"不开车");
}
}
//遵循迪米特法则
//类A
- (void)useCar {
if (B.isUseCar) {
NSLog(@"开车");
} else {
NSLog(@"不开车");
}
}
//类B
- (BOOL)isUseCar {
if ([B.person isEqualToString: @"男人"]) {
return YES;
} else if ([B.person isEqualToString: @"女人"]) {
return NO;
}
}
遵循迪米特法则进行设计,后面对B类中的判断条件进行更改,不会影响到A类,只需更改B类中代码即可。这样达到了高内聚、低耦合的目标。
合成复用原则
合成复用原则是尽量⾸先使用合成/聚合的⽅式,而不是使用继承。此原则和里氏替换原则氏相辅相成的,两者都是详细实现"开-闭"原则的规范。我们先看什么是合成和聚合:
-
合成
合成是指一个总体对依托他而存在的关系,如一个人对他的房子和家具。该关系依赖性不强,比如人没了,这个关系就自然消失了。 -
聚合
聚合是比合成关系更强的一种依赖关系,如有一台汽车,汽车对引擎、轮胎的关系就是聚合关系。这些关系就是带有聚合性质的。车没了,该车的引擎和轮胎自然也没了。在我们的设计中,这种关系不应该频繁出现,因为这样会增大设计的耦合度。
明确了合成和聚合关系,再来理解合成复用原则应该就清楚了:我们要尽量找出应用中可能需要变化之处,把它们独立出来,不要和那些不需要变化的代码混在一起。
设计模式三大分类
创建型模式
通常和对象的创建有关,涉及到对象实例化的方式。
主要模式:单例模式、抽象工厂模式、建造者模式、工厂模式、原型模式。
结构型模式
描述的是如何组合类和对象以获得更大的结构。
主要模式:适配器模式、桥接模式、装饰模式、组合模式、外观模式、享元模式、代理模式。
行为型模式
用来对类或对象怎样交互和怎样分配职责进行描述。
主要模式:模版方法模式、命令模式、迭代器模式、观察者模式、中介者模式、备忘录模式、解释器模式、状态模式、策略模式、职责链模式、访问者模式。
工厂模式
工厂模式就是我们在创建对象时不会对客户端暴露创建逻辑,并且是通过使用一个共同的接口来指向新创建的对象。
工厂模式分为简单工厂模式,工厂方法模式,抽象工厂模式。
简单工厂模式(Simple Factory Pattern)
定义
专门定义一个类(工厂类)来负责创建其他类的实例。可以根据创建方法的参数来返回不同类的实例,被创建的实例通常都具有共同的父类。(总结来说,其实就是把一大堆的if-else判断放到工厂类里)
成员分工
工厂类
工厂类通常包含一个静态方法(类方法),由该方法根据输入类型负责创建具体的产品(对象)。
抽象产品基类(接口)
它的作用是降低客户端和具体产品之间的耦合度。而且符合了“开-闭”原则,以后需要加入新的产品线,客户端调用的代码也基本无需修改。
具体产品类
真正实现业务逻辑的子类。
适用场景
- 有一组相似的对象,需要集中统一创建
- 创建对象的过程较为复杂
- 对象很多,并且有扩展需求
- 客户端不需要知道创建对象的过程
- 客户端使用的对象存在变动的可能,或者根本不知道使用哪一个具体对象
举例
可口可乐和百事可乐继承于可乐抽象类
// 简单工厂实现
@implementation SimpleFactory
+ (Cola *)createColaWithType:(NSInteger)type {
switch (type) {
case 0:
//可口可乐
return [[CocaCola alloc] init];
case 1:
//百事可乐
return [[PesiCola alloc] init];
default:
return nil;
break;
}
}
@end
使用工厂产品
id<ColaDelegate> cocaCola = [SimpleFactory createColaWithType:@"0"];
[cocaCola sell];
id<ColaDelegate> pesiCola = [SimpleFactory createColaWithType:@"1"];
[pesiCola sell];
优点
- 根据所需产品找对应工厂进行生产,不关心产品细节,也不需要知道产品类的类名。
- 当系统中加入新产品时,不需要修改抽象工厂和抽象产品提供的接口,也无须修改客户端和其他的具体工厂和具体产品,而只要添加一个具体工厂和与其对应的具体产品就可以了,符合了开闭原则。
缺点
- 当系统中加入新产品时,除了需要提供新的产品类之外,还要提供与其对应的具体工厂类。因此系统中类的个数将成对增加,增加了系统的复杂度。
工厂方法模式(Factory Method Pattern)
定义
工厂方法模式又称为工厂模式,工厂父类负责定义创建产品对象的公共接口,而工厂子类则负责生成具体的产品对象,即通过不同的工厂子类来创建不同的产品对象。
适用场景
- 类想让其子类决定在运行时创建什么,无法在编译时准确确定
- 类有若干个辅助类为其子类,而你想将返回某个子类这一信息局部化
举例
工厂方法和简单工厂有一些区别,简单工厂是由一个代工厂生产不同的产品,而工厂方法是对工厂进行抽象化,不同产品都由专门的具体工厂来生产。如可口可乐工厂专门生产可口可乐,百事可乐工厂专门生产百事可乐。
//工厂抽象类
@implementation Factory
+ (Cola *)createCola {
return [[Cola alloc] init];
}
@end
//可口可乐工厂
@implementation CocaColaFactory
+ (Cola *)createCola {
return [[CocaCola alloc] init];
}
@end
//百事可乐工厂
@implementation PesiColaFactory
+ (Cola *)createCola {
return [[PesiCola alloc] init];
}
@end
优点
- 根据所需产品找对应工厂进行生产,不关心产品细节,也不需要知道产品类的类名。
- 当系统中加入新产品时,不需要修改抽象工厂和抽象产品提供的接口,也无须修改客户端和其他的具体工厂和具体产品,而只要添加一个具体工厂和与其对应的具体产品就可以了,符合了“开-闭”原则。
缺点
- 当系统中加入新产品时,除了需要提供新的产品类之外,还要提供与其对应的具体工厂类。因此系统中类的个数将成对增加,增加了系统的复杂度。
抽象工厂模式(Abstract Factory Pattern)
定义
抽象工厂模式提供一个创建一系列相关或相互依赖对象的接口,而无须指定它们具体的类。
适用场景
- 将一个系列的产品族统一到一起创建
- 容易改变产品的系列
抽象工厂和工厂方法的区别(举例)
生产产品的工厂是抽象的。主题工厂不仅仅生产可口可乐或百事可乐,瓶子和箱子也需要专属定制,专门生产同一主题的不同商品。
优点
- 不需要生产产品细节,只需要知道产品属于那个工厂就行,当一个产品族中的多个对象被设计成一起工作时,它能够保证客户端始终只使用同一个产品族中的对象。这对一些需要根据当前环境来决定其行为的软件系统来说,是一种非常实用的设计模式。
缺点
- 规定了所有可能被创建的产品集合,产品族中扩展新的产品困难,需要修改抽象工厂的接口
- 代码结构比较复杂