阅读须知:
1、本文《六大设计原则上》介绍单一职责原则、里氏替换原则、开闭原则,迟点发布的《六大设计原则下》会介绍接口隔离原则、依赖导致原则、迪米特法则。
2、本文的运行程序
指的是对某个类的创建调用方法。具体内容读者在下面遇到便知。
3、本系列的例子是为了让读者更好理解原则,并不是说代码列出来的代码风格就是最好的,仅供参考。
4、每一个原则笔者尽量用多个例子去阐述,让读者更好的理解。
简写 | 英文名称 | 中文名称 |
---|---|---|
SRP | Single Responsibility Principle | 单一职责原则 |
LSP | Liskov Substitution Principle | 里氏替换原则 |
OCP | Open Closed Principle | 开闭原则 |
ISP | Interface Segregation Principle | 接口隔离原则 |
DIP | Dependence Inversion Principle | 依赖倒置原则 |
LoD | Law of Demeter | 迪米特法则 |
把这6个原则的首字母(里氏替换原则和迪米特陆则的首字母重复,只取一个)巧合联合起来就是SOLID (solid,稳定的),也就是说把这6个原则结合使用的好处:建立稳定、灵活、健壮的设计。开闭原则是六大原则重中之重,是最基础的原则,是其他5大原则的精神领袖。
单一职责
定义:
单一职责原则又称单一功能原则,这里的职责是指类变化的原因, 单一职责原则规定一个类应该有且仅有一个引起它变化的原因,否则类应该被拆分( There should never be more than one reason for a class to change )。
仅一个原因?读到这里,大家可能会纳闷,别急,下面我会通过例子说明
优点
- 类的复杂性降低,实现什么职责都有清晰明确的定义:
- 可读性提高,复杂性降低,那当然可读性提高了:
- 可维护性提高,可读性提高,那当然更容易维护了;
- 变更引起的风险降低,变更是必不可少的,如果接口的单一职责做得好,一个接口修改只对相应的实现类有影响,对其他的接口无影响,这对系统的扩展性、维护性都有非常大的帮助。
缺点
- 一个职责的变化可能会削弱或者抑制这个类实现其他职责的能力;
- 当客户端需要该对象的某一个职责时,不得不将其他不需要的职责全都包含进来,从而造成冗余代码或代码的浪费。
实现:
不好的设计
你想要拍照和播放音乐的设备,如果将两个功能放在一起,不就是现在的智能手机吗,好,我们创建一个类,名叫MobilePhone
,有拍照功能,播放音乐功能。
MobilePhone类:
//------ MobilePhone.h ------
@interface MobilePhone : NSObject
@property (nonatomic, copy) NSString *type; // 手机型号
@property (nonatomic, copy) NSString *size; // 尺寸
@property (nonatomic, assign) int volume; // 音量
@property (nonatomic, copy) NSString *pixels; // 像素
// 拍照
- (void)photograph;
// 播放音乐
- (void)playMusic;
@end
//------ MobilePhone.m ------
@implementation MobilePhone
+ (void)photograph {
NSLog(@"photograph");
}
+ (void)playMusic {
NSLog(@"playMusic");
}
@end
其类图如下:
这样粒度太大,什么功能都在MobilePhone
类,如果有十个功能,这个类岂不是变得非常冗余,非常难重用。我们尝试拆分一下功能,增加Photograph
,PlayMusic
较好的设计1
把拍照和播放音乐分别用两个单独类实现。这样Photograph
类如果要增加拍照功能、修改拍照方法、修改属性,播放音乐的PlayMusic
类不用理会,而播放音乐要增加什么音效,Photograph
也不用理会。因为Photograph
只会因为拍照方面的修改,PlayMusic
也只会因播放音乐方面的修改。这就符合单一职责原则:一个类应该有且仅有一个引起它变化的原因
MobilePhone类:
------ MobilePhone.h ------
@interface MobilePhone : NSObject
@property (nonatomic, copy) NSString *type; // 手机型号
@property (nonatomic, copy) NSString *size; // 尺寸
@property (nonatomic, assign) int volume; // 音量
@property (nonatomic, copy) NSString *pixels; // 像素
@end
------ MobilePhone.m ------
@implementation MobilePhone
@end
Photograph类:
//------ Photograph.h ------
#import "MobilePhone.h"
@interface Photograph : NSObject
// 照相
+(void)photograph:(MobilePhone *)phone;
//------ Photograph.m ------
@implementation Photograph
+ (void)photograph:(MobilePhone *)phone {
NSLog(@"photograph...,当前像素是%@", phone.pixels);
}
@end
PlayMusic类:
//------ PlayMusic.h ------
#import "MobilePhone.h"
@interface PlayMusic : NSObject
// 播放音乐
+ (void)playMusic:(MobilePhone *)phone;
@end
//------ PlayMusic.m ------
@implementation PlayMusic
+ (void)playMusic:(MobilePhone *)phone {
NSLog(@"playMusic..,当前音量为%d", phone.volume);
}
@end
运行程序
MobilePhone *phone = [MobilePhone new];
phone.pixels = @"3264×2488";
phone.volume = 80;
[Photograph photograph:phone];
[PlayMusic playMusic:phone];
结果为:
photograph…,当前像素是3264×2488
playMusic…,当前音量为80
其类图如下:
(虚线普通箭头是“依赖”的意思,箭头由依赖方指向被依赖方)
较好的设计2
接下来我们再用接口方式展示一下单一职责原则,定义拍照协议PhotographProtocol
和播放音乐协议PlayMusicProtocol
,然后MobilePhone
想要哪个功能就去实现哪个
PhotographProtocol接口:
@protocol PhotographProtocol <NSObject>
// 照相
- (void)photograph;
@end
PlayMusicProtocol接口:
@protocol PlayMusicProtocol <NSObject>
// 播放音乐
- (void)playMusic;
@end
MobilePhone类:
//------ MobilePhone.h ------
@interface MobilePhone : NSObject<PlayMusicProtocol, PhotographProtocol>
@property (nonatomic, copy) NSString *type; // 手机型号
@property (nonatomic, copy) NSString *size; // 尺寸
@property (nonatomic, assign) int volume; // 音量
@property (nonatomic, copy) NSString *pixels; // 像素
@end
//------ MobilePhone.m ------
@implementation MobilePhone
- (void)playMusic {
NSLog(@"playMusic");
}
- (void)photograph {
NSLog(@"photograph");
}
@end
运行程序:
MobilePhone *phone = [MobilePhone new];
[phone playMusic];
[phone photograph];
结果为:
playMusic
photograph
其类图如下:
(虚线空心箭头是“遵从”的意思,也就是实现类实现协议(接口),箭头由实现类指向协议)
较好的设计3
我们一定要遵循单一职责原则吗?在现有的需求上能做到当然可以去做,但是往往有的时候,需求不是在设计的时候发生改变,而是一定程度之后,你已经有了一定的代码量了,可能修改的开销很高,这个时候就仁者见仁智者见智。就如上述,若是将手机类拆分,则影响了底层调用的实现,也需要修改,弱是调用的地方太多,那么修改的地方也会很多,若是发布了,改起来也不是很方便,但是当然,也有一定的手法来做这件事情,比如手机类保留,让手机类拥有一个摄像机类对象和一个音乐播放器类对象,然后播放音乐方法则调用音乐播放器类实例的播放音乐功能,照相功能则调用摄像机类实例的照相功能,这样可以在不影响原有的东西的基础上又遵循原则。请看下列代码
Photograph类:
@interface Photograph : NSObject
// 照相
+(void)photograph;
@end
//------ Photograph.m ------
@implementation Photograph
+ (void)photograph {
NSLog(@"photograph");
}
@end
PlayMusic类:
//------ PlayMusic.h ------
@interface PlayMusic : NSObject
// 播放音乐
+ (void)playMusic;
@end
//------ PlayMusic.m ------
@implementation PlayMusic
+ (void)playMusic {
NSLog(@"playMusic");
}
@end
MobilePhone类:
//------ MobilePhone.h ------
@interface MobilePhone : NSObject
- (void)work;
@end
//------ MobilePhone.m ------
#import "Photograph.h"
#import "PlayMusic.h"
@implementation MobilePhone
- (void)work {
Photograph *photo = [Photograph new];
[photo photograph];
PlayMusic *play = [PlayMusic new];
[play playMusic];
}
@end
里氏替换原则
在说里氏替换原则前,我们先说下继承。
继承
在面向对象的语言中,继承必不可少。
优点
- 代码共享,减少创建类的工作量,每个子类都拥有父类的方法和属性
- 提高代码的重用性
- 子类可以形似父类,但又异于父类
- 提高代码的可扩展性,子类可以增加更多的属性或者方法,很多开源框架也是通过继承来完成的
- 提高产品或项目的开放性
缺点
- 继承是侵入性的。只要继承,就必须拥有父类的所有属性和方法
- 降低代码的灵活性。子类必须拥有父类的属性和方峰,让子类自由的世界中 多了些约束
- 增强了耦合性,当父类的常量 、变量和方法被修改时,需要考虑子类的修改,而且在缺乏规范的环境下,这种修改可能带来非常糟糕的结果———大段的代码需要重构
定义:
继承有缺点也有缺点,就像鸡蛋里也能挑出骨头一样。里氏替换原则是为了让“利”因素发挥最大作用,同时减少“弊”带来的麻烦。
定义一:如果对每个类型为S的对象o1,都有类型为T的对象o2,使得以T定义的所有程序P在所有的对象o1都代换成o2时,程序P的行为没有发生变化,那么类型S是类型T的子类型。
定义二:所有引用基类的地方必须能透明地使用其子类的对象。
定义二通俗来讲,只要父类能出现的地方子类就可以出现,而且替换为子类也不会产生任何错误或异常,使用者可能根本就不需要知道是父类还是子类。但是,反过来就不行,有子类的地方,父类未必就能适应。
优点
- 里氏替换原则是实现开闭原则的重要方式之一
- 它克服了继承中重写父类造成的可复用性变差的缺点
- 它是动作正确性的保证。即类的扩展不会给已有的系统引入新的错误,降低了代码出错的可能性。
实现
不好的设计
鸟一般都会飞行,如燕子的飞行速度是每小时120千米,但新西兰的几维鸟由于翅膀退化无法飞行。现举例里氏替换原则在“几维鸟不是鸟”实例的应用。
首先声明一个鸟类(Bird),然后用燕子(Swallow)和几维鸟(BrownKiwi)去继承鸟类
鸟类:
------ Bird.h ------
@interface Bird : NSObject
{
@protected
double _flySpeed;
}
- (void)setFlySpeed:(double)flySpeed;
- (double)getFlyTime:(double)distance;
@end
------ Bird.m ------
#import "Bird.h"
@implementation Bird
- (void)setFlySpeed:(double)flySpeed {
_flySpeed = flySpeed;
}
- (double)getFlyTime:(double)distance {
return distance/_flySpeed;
}
@end
几维鸟类:
------ BrownKiwi.h ------
@interface BrownKiwi : Bird
@end
------ BrownKiwi.m ------
@implementation BrownKiwi
- (void)setFlySpeed:(double)flySpeed {
flySpeed = 0;
}
@end
燕子类:
------ Swallow.h ------
@interface Swallow : Bird
@end
------ Swallow.m ------
@interface Swallow : Bird
@end
可以看到,几维鸟类重写了父类方法- (void)setFlySpeed:(double)flySpeed
,因为几维鸟不能飞翔,如果不重写父类方法,那么它的飞行速度就变成了120千米了,这与事实不符。
程序运行:
Bird *bird = [Bird new];
[bird setFlySpeed:120];
double time = [bird getFlyTime:300];
NSLog(@"需要%.1f小时", time);
结果为:需要2.5小时
BrownKiwi *brownKiwi = [BrownKiwi new];
[brownKiwi setFlySpeed:120];
double time = [brownKiwi getFlyTime:300];
NSLog(@"需要%.1f小时", time);
结果为:需要inf小时
从结果看出,几维鸟程序运行出错。从里氏替换原则角度看,只要父类能出现的地方子类就可以出现。这明显不对,因为父类Bird运行程序正确,子类几维鸟运行出错。这违背了里氏替换原则原则。
其类图如下:
(实线空心箭头是“继承”的意思,箭头由子类指向父类)
较好的设计
接着上面的例子,取消几维鸟原来的继承关系,定义鸟和几维鸟更一般的的父类。我们很容易想到动物类,动物类的都可以奔跑。让 Bird 和 BrownKiwi 去继承动物类,燕子类还是继续继承 Bird 类。几维鸟飞行速度为0,但奔跑速度不为0,可以计算出其奔跑300千米所要花费的时间
Animal类
------ Animal.h ------
@interface Animal : NSObject
{
@protected
double _runSpeed;
}
- (void)setRunSpeed:(double)speed;
- (double)getRunTime:(double)distance;
@end
鸟类:
------ Bird.h ------
@interface Bird : Animal
{
@protected
double _flySpeed;
}
- (void)setFlySpeed:(double)flySpeed;
- (double)getFlyTime:(double)distance;
@end
------ Bird.m ------
#import "Bird.h"
@implementation Bird
- (void)setFlySpeed:(double)flySpeed {
_flySpeed = flySpeed;
}
- (double)getFlyTime:(double)distance {
return distance/_flySpeed;
}
@end
几维鸟类:
------ BrownKiwi.h ------
@interface BrownKiwi : Animal
@end
------ BrownKiwi.m ------
@implementation BrownKiwi
@end
现在,由于父类的更改,父类出现的地方子类都可以出现,这符合里氏替换原则,属于比较好的设计。
其类图如下:
(实线空心箭头是“继承”的意思,箭头由子类指向父类)
开闭原则
定义
Software entities like classes, modules and functions should be open for extension but closed for modifications. (一个软件实体如类、模块和函数应该对扩展开放,对修改关闭。)
定义明确告诉我们软件实体应当对扩展开放,对修改关闭
这里的软件实体包括以下部分:
- 项目或软件产品中按照一定的逻辑规则划分的模块
- 抽象和类。
- 方法。
一个软件产品只要在生命周期内,都会发生变化,既然变化是一个事实,我们就应该在设计尽量适应这些变化,以提高项目的稳定性和灵活性。开闭原则告诉我们应尽量通过扩展实体的行为来实现变化,而不是通过修改已有的代码来完成变化,它是软件实体的未来事件而制定的对现行开发设计进行约束的一个原则。
优点
1、开闭原则对测试的影响
软件遵守开闭原则的话,软件测试时只需要对扩展的代码进行测试就可以了。因为原有的测试代码仍然能够正常运行。
2、可以提高代码的可复用性
粒度越小,被复用的可能性就越大;在面向对象的程序中,根据原子和抽象编程可以提高代码的可复用性。
3、可以提高软件的可维护性
软件遵守开闭原则,其稳定性高和延续性强,从而易于扩展和维护。
实现
我们以书店销售书籍为例,,其类图如下:
(虚线空心箭头是“遵从”的意思,也就是实现类实现协议(接口),箭头由实现类指向协议)
BookiProtocol接口:
------ BookiProtocol.h ------
@protocol BookiProtocol <NSObject>
- (NSString *)getName;
- (int)getPrice;
- (NSString *)getAuthor;
@end
NovelBook类:
------ NovelBook.h ------
@interface NovelBook : NSObject<BookiProtocol>
{
@protected
NSString *_name;
int _price;
NSString *_author;
}
- (instancetype)initWithName:(NSString *)name price:(int)price author:(NSString *)author;
@end
------ NovelBook.m ------
@implementation NovelBook
- (instancetype)initWithName:(NSString *)name price:(int)price author:(NSString *)author {
if (self = [super init]) {
_name = name;
_price = price;
_author = author;
}
return self;
}
- (NSString *)getName {
return _name;
}
- (int)getPrice {
return _price;
}
- (NSString *)getAuthor {
return _author;
}
@end
运行程序:
NSMutableArray *bookArr = [NSMutableArray array];
NovelBook *book1 = [[NovelBook alloc] initWithName:@"天龙八部" price:3200 author:@"金庸"];
NovelBook *book2 = [[NovelBook alloc] initWithName:@"巴黎圣母院" price:5600 author:@"雨果"];
NovelBook *book3 = [[NovelBook alloc] initWithName:@"悲惨世界" price:3500 author:@"雨果"];
NovelBook *book4 = [[NovelBook alloc] initWithName:@"金瓶梅" price:4350 author:@"兰陵笑笑生"];
[bookArr addObject:book1]; [bookArr addObject:book2];
[bookArr addObject:book3]; [bookArr addObject:book4];
NSLog(@"---------- 书店卖出去的书籍记录如下:----------");
for (Book *book in bookArr) {
NSLog(@"书籍名称:%@ 书籍作者:%@ 书籍价格:%.2f", [book getName], [book getAuthor], [book getPrice]/100.0);
}
结果如下:
---------- 书店卖出去的书籍记录如下:----------
书籍名称:天龙八部 书籍作者:金庸 书籍价格:32.00
书籍名称:巴黎圣母院 书籍作者:雨果 书籍价格:56.00
书籍名称:悲惨世界 书籍作者:雨果 书籍价格:35.00
书籍名称:金瓶梅 书籍作者:兰陵笑笑生 书籍价格:43.50
我们知道,有时经济不景气,对零售业影响比较大,书店为了生存开始打折销售:所有40元以上的书籍9折销售,其他的8折销售。对已经投产的项目来说,这就是一个变化,我们应该如何应对这样一个需求变化?有如下三种方法可以解决这个问题:
不好的设计
修改协议(接口)
BookiProtocol接口:
------ BookiProtocol.h ------
@protocol BookiProtocol <NSObject>
.....
// 增加此方法
- (int)getOffPrice;
@end
NovelBook类:
------ NovelBook.m ------
@implementation NovelBook
.....
// 增加此方法
- (int)getOffPrice {
_price = _price > 4000 ? _price*0.9 : _price*0.8;
return _price;
}
@end
运行程序:
for (Book *book in bookArr) {
NSLog(@"书籍名称:%@ 书籍作者:%@ 书籍价格:%.2f", [book getName], [book getAuthor], [book getPrice]/100.0);
}
替换成
for (NovelBook *book in bookArr) {
NSLog(@"书籍名称:%@ 书籍作者:%@ 书籍价格:%.2f", [book getName], [book getAuthor], [book getOffPrice]/100.0);
}
结果如下:
---------- 书店卖出去的书籍记录如下:----------
书籍名称:天龙八部 书籍作者:金庸 书籍价格:25.60
书籍名称:巴黎圣母院 书籍作者:雨果 书籍价格:50.40
书籍名称:悲惨世界 书籍作者:雨果 书籍价格:28.00
书籍名称:金瓶梅 书籍作者:兰陵笑笑生 书籍价格:39.15
在BookProtocol
上增加一个方法getOffPrice
,专门用来进行打折处理,所有的实现类实现该方法。但是后果就是NovelBook
要修改,运行程序中的方法也修改,同时BookProtocol
作为协议应该是稳定且可靠的,不应该经常发生变化,否则协议作为契约的作用就失去了效能。因此,该方案否定。
较好的设计1
修改实现类
其他类都不用动,直接去NovelBook
修改下面方法
NovelBook类:
- (int)getPrice {
_price = _price > 4000 ? _price*0.9 : _price*0.8;
return _price;
}
修改NovelBook
类的方法,直接在getPrice
中实现打折处理,估计大家最常用的也是这个方法,这个方法也非常好。但是修改了实现类,采购员如果想看打折前的价格就无法实现了,所以这并不是最优方案。
较好的设计2
通过扩展实现
增加一个子类OffNovelBook
,重写方法getPrice
,高层次的模块,也就是运行程序的代码那里通过OffNovelBook
产生新对象。
OffNovelBook类:
------ OffNovelBook.h ------
@interface OffNovelBook : NovelBook
@end
------ OffNovelBook.m ------
@implementation OffNovelBook
- (int)getPrice {
_price = _price > 4000 ? _price*0.9 : _price*0.8;
return _price;
}
@end
运行程序:
OffNovelBook *book1 = [[OffNovelBook alloc] initWithName:@"天龙八部" price:3200 author:@"金庸"];
OffNovelBook *book2 = [[OffNovelBook alloc] initWithName:@"巴黎圣母院" price:5600 author:@"雨果"];
OffNovelBook *book3 = [[OffNovelBook alloc] initWithName:@"悲惨世界" price:3500 author:@"雨果"];
OffNovelBook *book4 = [[OffNovelBook alloc] initWithName:@"金瓶梅" price:4350 author:@"兰陵笑笑生"];
[bookArr addObject:book1]; [bookArr addObject:book2];
[bookArr addObject:book3]; [bookArr addObject:book4];
NSLog(@"---------- 书店卖出去的书籍记录如下:----------");
for (OffNovelBook *book in bookArr) {
NSLog(@"书籍名称:%@ 书籍作者:%@ 书籍价格:%.2f", [book getName], [book getAuthor], [book getPrice]/100.0);
}
其类图如下:
(虚线空心箭头是“遵从”的意思,也就是实现类实现协议(接口),箭头由实现类指向协议)
(实线空心箭头是“继承”的意思,箭头由子类指向父类)
开闭原则对扩展开放,对修改关闭,并不意味着不做任何修改,低层模块的变更,必然要有高层模块进行耦合,否则就是一个孤立无意义的代码片段。
上面这个例子的开闭原则不知道大家领悟了没,下面我们再通过一个更容易的业务来讲解此原则。
接着上面的卖书例子,上面书店只卖小说,如果书店还卖计算机书籍呢?同时,计算机书籍还要查找范围,比如iOS开发书籍、数据库开发数据等。我们很自然想到,计算机书籍跟小说类似,应该遵从BookProtocol
,所以类图如下:
(虚线空心箭头是“遵从”的意思,也就是实现类实现协议(接口),箭头由实现类指向协议)
------ ComputerBook.h ------
@interface ComputerBook : NSObject<BookiProtocol>
{
@protected
NSString *_name;
int _price;
NSString *_author;
NSString *_scope; // 范围
}
- (instancetype)initWithName:(NSString *)name price:(int)price author:(NSString *)author scope:(NSString *)scope;
- (NSString *)getScope; // 返回范围
@end
------ ComputerBook.m ------
@implementation ComputerBook
- (instancetype)initWithName:(NSString *)name price:(int)price author:(NSString *)author scope:(NSString *)scope {
if (self = [super init]) {
_name = name;
_price = price;
_author = author;
_scope = scope;
}
return self;
}
- (NSString *)getName {
return _name;
}
- (int)getPrice {
return _price;
}
- (NSString *)getAuthor {
return _author;
}
- (NSString *)getScope {
return _scope;
}
如果还要增加电影书籍,教科书书籍,就跟计算机书籍、小说一样,遵从BookProtocol
即可,然后再根据自身需求,增加或者减少实现类代码。
写在最后:
敬请期待下篇,本文如有疑问,多多留言。如大佬们有更好的办法,求指教,谢谢。
共同学习,写下你的评论
评论加载中...
作者其他优质文章