一、六大设计原则
缩写 | 英文名称 | 中文名称 |
---|---|---|
SRP | Single Responsibility Principle | 单一职责原则 |
OCP | Open Close Principle | 开闭原则 |
LSP | Liskov Substitution Principle | 里氏替换原则 |
LoD | Law of Demeter ( Least Knowledge Principle) | 迪米特原则 |
ISP | Interface Segregation Principle | 接口分离原则 |
DIP | Dependency Inversion Principle | 依赖倒置原则 |
原则一:单一职责原则
定义
Software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification.
单一职责原则的定义是就一个类而言,应该仅有一个引起他变化的原因。也就是说一个类应该只负责一件事情。
1、类职责的变化往往就是导致类变化的原因:也就是说如果一个类具有多种职责,就会有多种导致这个类变化的原因,从而导致这个类的维护变得困难。
2、往往在软件开发中随着需求的不断增加,可能会给原来的类添加一些本来不属于它的一些职责,从而违反了单一职责原则。如果我们发现当前类的职责不仅仅有一个,就应该将本来不属于该类真正的职责分离出去。
3、不仅仅是类,函数(方法)也要遵循单一职责原则,即:一个函数(方法)只做一件事情。如果发现一个函数(方法)里面有不同的任务,则需要将不同的任务以另一个函数(方法)的形式分离出去。
优点
- 可以降低类的复杂度,一个类只负责一项职责,这样逻辑也简单很多
- 提高类的可读性,和系统的维护性,因为不会有其他奇怪的方法来干扰我们理解这个类的含义
- 当发生变化的时候,能将变化的影响降到最小,因为只会在这个类中做出修改。
案例分析
初始需求:需要创造一个员工类,这个类有员工的一些基本信息。
新需求:增加两个方法:
- 判定员工在今年是否升职
- 计算员工的薪水
不好的设计
//================== Employee.h ==================
@interface Employee : NSObject
//============ 初始需求 ============
@property (nonatomic, copy) NSString *name; //员工姓名
@property (nonatomic, copy) NSString *address; //员工住址
@property (nonatomic, copy) NSString *employeeID; //员工ID
//============ 新需求 ============
//计算薪水
- (double)calculateSalary;
//今年是否晋升
- (BOOL)willGetPromotionThisYear;
@end
新需求的做法看似没有问题,因为都是和员工有关的,但却违反了单一职责原则:因为这两个方法并不是员工本身的职责。
- calculateSalary这个方法的职责是属于会计部门的:薪水的计算是会计部门负责。
- willPromotionThisYear这个方法的职责是属于人事部门的:考核与晋升机制是人事部门负责。
而上面的设计将本来不属于员工自己的职责强加进了员工类里面,而这个类的设计初衷(原始职责)就是单纯地保留员工的一些信息而已。因此这么做就是给这个类引入了新的职责,故此设计违反了单一职责原则。
我们可以简单想象一下这么做的后果是什么:如果员工的晋升机制变了,或者税收政策等影响员工工资的因素变了,我们还需要修改当前这个类。
那么怎么做才能不违反单一职责原则呢?- 我们需要将这两个方法(责任)分离出去,让本应该处理这类任务的类来处理。
好的设计
我们保留员工类的基本信息:
//================== Employee.h ==================
@interface Employee : NSObject
//初始需求
@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *address;
@property (nonatomic, copy) NSString *employeeID;
@end
接着创建新的会计部门类:
//================== FinancialApartment.h ==================
#import "Employee.h"
//会计部门类
@interface FinancialApartment : NSObject
//计算薪水
- (double)calculateSalary:(Employee *)employee;
@end
和人事部门类:
//================== HRApartment.h ==================
#import "Employee.h"
//人事部门类
@interface HRApartment : NSObject
//今年是否晋升
- (BOOL)willGetPromotionThisYear:(Employee*)employee;
@end
通过创建了两个分别专门处理薪水和晋升的部门,会计部门和人事部门的类:FinancialApartment 和 HRApartment,把两个任务(责任)分离了出去,让本该处理这些职责的类来处理这些职责。
这样一来,不仅仅在此次新需求中满足了单一职责原则,以后如果还要增加人事部门和会计部门处理的任务,就可以直接在这两个类里面添加即可。
原则二:开闭原则
定义
Software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification.
开闭原则的定义是软件中的对象(类,模块,函数等)应该对于扩展是开放的,但是对于修改是关闭的。
当需求发生改变的时候,我们需要对代码进行修改,这个时候我们应该尽量去扩展原来的代码,而不是去修改原来的代码,因为这样可能会引起更多的问题。
开闭原则我们可以用一种方式来确保他,我们用抽象去构建框架,用实现扩展细节。这样当发生修改的时候,我们就直接用抽象了派生一个具体类去实现修改。
优点
实践开闭原则的优点在于可以在不改动原有代码的前提下给程序扩展功能。增加了程序的可扩展性,同时也降低了程序的维护成本。
案例分析
设计一个在线课程类:
由于教学资源有限,开始的时候只有类似于博客的,通过文字讲解的课程。 但是随着教学资源的增多,后来增加了视频课程,音频课程以及直播课程。
不好的设计
最开始的文字课程类:
//================== Course.h ==================
@interface Course : NSObject
@property (nonatomic, copy) NSString *courseTitle; //课程名称
@property (nonatomic, copy) NSString *courseIntroduction; //课程介绍
@property (nonatomic, copy) NSString *teacherName; //讲师姓名
@property (nonatomic, copy) NSString *content; //课程内容
@end
接着按照上面所说的需求变更:增加了视频,音频,直播课程:
//================== Course.h ==================
@interface Course : NSObject
@property (nonatomic, copy) NSString *courseTitle; //课程名称
@property (nonatomic, copy) NSString *courseIntroduction; //课程介绍
@property (nonatomic, copy) NSString *teacherName; //讲师姓名
@property (nonatomic, copy) NSString *content; //文字内容
//新需求:视频课程
@property (nonatomic, copy) NSString *videoUrl;
//新需求:音频课程
@property (nonatomic, copy) NSString *audioUrl;
//新需求:直播课程
@property (nonatomic, copy) NSString *liveUrl;
@end
三种新增的课程都在原Course类中添加了对应的url。也就是每次添加一个新的类型的课程,都在原有Course类里面修改:新增这种课程需要的数据。这就导致:我们从Course类实例化的视频课程对象会包含并不属于自己的数据:audioUrl和liveUrl:这样就造成了冗余,视频课程对象并不是纯粹的视频课程对象,它包含了音频地址,直播地址等成员。
很显然,这个设计不是一个好的设计,因为(对应上面两段叙述):
随着需求的增加,需要反复修改之前创建的类。
给新增的类造成了不必要的冗余。
之所以会造成上述两个缺陷,是因为该设计没有遵循对修改关闭,对扩展开放的开闭原则,而是反其道而行之:开放修改,而且不给扩展提供便利。
好的设计
首先在Course类中仅仅保留所有课程都含有的数据:
/================== Course.h ==================
@interface Course : NSObject
@property (nonatomic, copy) NSString *courseTitle; //课程名称
@property (nonatomic, copy) NSString *courseIntroduction; //课程介绍
@property (nonatomic, copy) NSString *teacherName; //讲师姓名
接着,针对文字课程,视频课程,音频课程,直播课程这三种新型的课程采用继承Course类的方式。而且继承后,添加自己独有的数据:
文字课程类:
//================== TextCourse.h ==================
@interface TextCourse : Course
@property (nonatomic, copy) NSString *content; //文字内容
@end
视频课程类:
//================== VideoCourse.h ==================
@interface VideoCourse : Course
@property (nonatomic, copy) NSString *videoUrl; //视频地址
@end
音频课程类:
//================== AudioCourse.h ==================
@interface AudioCourse : Course
@property (nonatomic, copy) NSString *audioUrl; //音频地址
@end
直播课程类:
//================== LiveCourse.h ==================
@interface LiveCourse : Course
@property (nonatomic, copy) NSString *liveUrl; //直播地址
@end
这样一来,上面的两个问题都得到了解决:
随着课程类型的增加,不需要反复修改最初的父类(Course),只需要新建一个继承于它的子类并在子类中添加仅属于该子类的数据(或行为)即可。
因为各种课程独有的数据(或行为)都被分散到了不同的课程子类里,所以每个子类的数据(或行为)没有任何冗余。
而且对于第二点:或许今后的视频课程可以有高清地址,视频加速功能。而这些功能只需要在VideoCourse类里添加即可,因为它们都是视频课程所独有的。同样地,直播课程后面还可以支持在线问答功能,也可以仅加在LiveCourse里面。
我们可以看到,正是由于最初程序设计合理,所以对后面需求的增加才会处理得很好。
原则三:里氏替换原则
定义
In a computer program, if S is a subtype of T, then objects of type T may be replaced with objects of type S (i.e. an object of type T may be substituted with any object of a subtype S) without altering any of the desirable properties of the program (correctness, task performed, etc.)
如果对每一个类型为T1的对象o1,都有类型为T2的对象o2,使得以T1定义的所有程序P在所有对象o1都替换成o2的时候,程序P的行为都没有发生变化,那么类型T2是类型T1的子类型。
里氏替换原则通俗的去讲就是:子类可以去扩展父类的功能,但是不能改变父类原有的功能。他包含以下几层意思:
- 子类可以实现父类的抽象方法,但是不能覆盖父类的非抽象方法。
- 子类可以增加自己独有的方法。
- 当子类的方法重载父类的方法时候,方法的形参要比父类的方法的输入参数更加宽松。
- 当子类的方法实现父类的抽象方法时,方法的返回值要比父类更严格。
优点
可以检验继承使用的正确性,约束继承在使用上的泛滥。
因为继承有很多缺点,他虽然是复用代码的一种方法,但同时继承在一定程度上违反了封装。父类的属性和方法对子类都是透明的,子类可以随意修改父类的成员。这也导致了,如果需求变更,子类对父类的方法进行一些复写的时候,其他的子类无法正常工作。所以里氏替换法则被提出来。
确保程序遵循里氏替换原则可以要求我们的程序建立抽象,通过抽象去建立规范,然后用实现去扩展细节,这个是不是很耳熟,对,里氏替换原则和开闭原则往往是相互依存的。
案例分析
创建两个类:长方形和正方形,都可以设置宽高(边长),也可以输出面积大小。
不好的设计
首先声明一个长方形类,然后让正方形类继承于长方形。
长方形类:
//================== Rectangle.h ==================
@interface Rectangle : NSObject
{
@protected double _width;
@protected double _height;
}
//设置宽高
- (void)setWidth:(double)width;
- (void)setHeight:(double)height;
//获取宽高
- (double)width;
- (double)height;
//获取面积
- (double)getArea;
@end
//================== Rectangle.m ==================
@implementation Rectangle
- (void)setWidth:(double)width{
_width = width;
}
- (void)setHeight:(double)height{
_height = height;
}
- (double)width{
return _width;
}
- (double)height{
return _height;
}
- (double)getArea{
return _width * _height;
}
@end
正方形类:
//================== Square.h ==================
@interface Square : Rectangle
@end
//================== Square.m ==================
@implementation Square
- (void)setWidth:(double)width{
_width = width;
_height = width;
}
- (void)setHeight:(double)height{
_width = height;
_height = height;
}
@end
可以看到,正方形类继承了长方形类以后,为了保证边长永远是相等的,特意在两个set方法里面强制将宽和高都设置为传入的值,也就是重写了父类Rectangle的两个set方法。但是里氏替换原则里规定,子类不能重写父类的方法,所以上面的设计是违反该原则的。
而且里氏替换原则原则里面所属:子类对象能够替换父类对象,而程序执行效果不变。我们通过一个例子来看一下上面的设计是否符合:
在客户端类写一个方法:传入一个Rectangle类型并返回它的面积:
- (double)calculateAreaOfRect:(Rectangle *)rect{
return rect.getArea;
}
我们先用Rectangle对象试一下:
Rectangle *rect = [[Rectangle alloc] init];
rect.width = 10;
rect.height = 20;
double rectArea = [self calculateAreaOfRect:rect];//output:200
长宽分别设置为10,20以后,结果输出200,没有问题。
现在我们使用Rectange的子类Square的对象替换原来的Rectange对象,看一下结果如何:
Square *square = [[Square alloc] init];
square.width = 10;
square.height = 20;
double squareArea = [self calculateAreaOfRect:square];//output:400
结果输出为400,结果不一致,再次说明了上述设计不符合里氏替换原则,因为子类的对象square替换父类的对象rect以后,程序执行的结果变了。
不符合里氏替换原则就说明该继承关系不是正确的继承关系,也就是说正方形类不能继承于长方形类,程序需要重新设计。
好的设计
既然正方形不能继承于长方形,那么是否可以让二者都继承于其他的父类呢?答案是可以的。
既然要继承于其他的父类,它们这个父类肯定具备这两种形状共同的特点:有4个边。那么我们就定义一个四边形的类:Quadrangle。
//================== Quadrangle.h ==================
@interface Quadrangle : NSObject
{
@protected double _width;
@protected double _height;
}
- (void)setWidth:(double)width;
- (void)setHeight:(double)height;
- (double)width;
- (double)height;
- (double)getArea;
@end
接着,让Rectangle类和Square类继承于它:
Rectangle类:
//================== Rectangle.h ==================
#import "Quadrangle.h"
@interface Rectangle : Quadrangle
@end
//================== Rectangle.m ==================
@implementation Rectangle
- (void)setWidth:(double)width{
_width = width;
}
- (void)setHeight:(double)height{
_height = height;
}
- (double)width{
return _width;
}
- (double)height{
return _height;
}
- (double)getArea{
return _width * _height;
}
@end
Square类:
//================== Square.h ==================
@interface Square : Quadrangle
{
@protected double _sideLength;
}
-(void)setSideLength:(double)sideLength;
-(double)sideLength;
@end
//================== Square.m ==================
@implementation Square
-(void)setSideLength:(double)sideLength{
_sideLength = sideLength;
}
-(double)sideLength{
return _sideLength;
}
- (void)setWidth:(double)width{
_sideLength = width;
}
- (void)setHeight:(double)height{
_sideLength = height;
}
- (double)width{
return _sideLength;
}
- (double)height{
return _sideLength;
}
- (double)getArea{
return _sideLength * _sideLength;
}
@end
我们可以看到,Rectange和Square类都以自己的方式实现了父类Quadrangle的公共方法。而且由于Square的特殊性,它也声明了自己独有的成员变量_sideLength以及其对应的公共方法。
注意,这里Rectange和Square并不是重写了其父类的公共方法,而是实现了其抽象方法。
原则四:迪米特原则
定义
You only ask for objects which you directly need.
一个对象应该对其他对象保持最小的了解。
迪米特原则也叫做最少知道原则(Least Know Principle)。如果两个二类不必彼此直接通信,那么这两个类就不应当发生直接的相互作用。如果一个雷需要调用另外一个类的某一个方法的话,可以通过第三者转发这个调用。在网上看到的比较形象的说明这个法则的示例:
- 如果你想让你的狗狗跑的话,你会对狗狗说还是对四条狗腿说?
- 如果你去店里买东西,你会直接扫码付款,还是会把钱包或者手机交给店员让他自己拿?
优点
实践迪米特原则可以良好地降低类与类之间的耦合,减少类与类之间的关联程度,让类与类之间的协作更加直接,从而使得类具有很好的可读性和可维护性。
在类的结构设计上,每个类都应该降低成员的访问权限。基本思想是强调了类之间的松耦合。类之间的耦合越弱,越利于复用,一个处于弱耦合的类被修改,不会被有关系的类造成影响。
案例分析
设计一个汽车类,包含汽车的品牌名称,引擎等成员变量。提供一个方法返回引擎的品牌名称。
不好的设计
Car类:
//================== Car.h ==================
@class GasEngine;
@interface Car : NSObject
//构造方法
- (instancetype)initWithEngine:(GasEngine *)engine;
//返回私有成员变量:引擎的实例
- (GasEngine *)usingEngine;
@end
//================== Car.m ==================
#import "Car.h"
#import "GasEngine.h"
@implementation Car{
GasEngine *_engine;
}
- (instancetype)initWithEngine:(GasEngine *)engine{
self = [super init];
if (self) {
_engine = engine;
}
return self;
}
- (GasEngine *)usingEngine{
return _engine;
}
@end
从上面可以看出,Car的构造方法需要传入一个引擎的实例对象。而且因为引擎的实例对象被赋到了Car对象的私有成员变量里面。所以Car类给外部提供了一个返回引擎对象的方法:usingEngine。
而这个引擎类GasEngine有一个品牌名称的成员变量brandName:
//================== GasEngine.h ==================
@interface GasEngine : NSObject
@property (nonatomic, copy) NSString *brandName;
@end
这样一来,客户端就可以拿到引擎的品牌名称了:
//================== Client.m ==================
#import "GasEngine.h"
#import "Car.h"
- (NSString *)findCarEngineBrandName:(Car *)car{
GasEngine *engine = [car usingEngine];
NSString *engineBrandName = engine.brandName;//获取到了引擎的品牌名称
return engineBrandName;
}
上面的设计完成了需求,但是却违反了迪米特法则。原因是在客户端的findCarEngineBrandName:中引入了和入参(Car)和返回值(NSString)无关的GasEngine对象。增加了客户端与 GasEngine的耦合。而这个耦合显然是不必要更是可以避免的。
接下来我们看一下如何设计可以避免这种耦合:
好的设计
同样是Car这个类,我们去掉原有的返回引擎对象的方法,而是增加一个直接返回引擎品牌名称的方法:
//================== Car.h ==================
@class GasEngine;
@interface Car : NSObject
//构造方法
- (instancetype)initWithEngine:(GasEngine *)engine;
//直接返回引擎品牌名称
- (NSString *)usingEngineBrandName;
@end
//================== Car.m ==================
#import "Car.h"
#import "GasEngine.h"
@implementation Car
{
GasEngine *_engine;
}
- (instancetype)initWithEngine:(GasEngine *)engine{
self = [super init];
if (self) {
_engine = engine;
}
return self;
}
- (NSString *)usingEngineBrandName{
return _engine.brand;
}
@end
因为直接usingEngineBrandName直接返回了引擎的品牌名称,所以在客户端里面就可以直接拿到这个值,而不需要间接地通过原来的GasEngine实例来获取。
我们看一下客户端操作的变化:
//================== Client.m ==================
#import "Car.h"
- (NSString *)findCarEngineBrandName:(Car *)car{
NSString *engineBrandName = [car usingEngineBrandName]; //直接获取到了引擎的品牌名称
return engineBrandName;
}
与之前的设计不同,在客户端里面,没有引入GasEngine类,而是直接通过Car实例获取到了需要的数据。
这样设计的好处是,如果这辆车的引擎换成了电动引擎(原来的GasEngine类换成了ElectricEngine类),客户端代码可以不做任何修改!因为它没有引入任何引擎类,而是直接获取了引擎的品牌名称。
所以在这种情况下我们只需要修改Car类的usingEngineBrandName方法实现,将新引擎的品牌名称返回即可。
原则五:接口分离原则
定义
Many client specific interfaces are better than one general purpose interface.
多个特定的客户端接口要好于一个通用性的总接口。
- 客户端不应该依赖它不需要实现的接口。
- 不建立庞大臃肿的接口,应尽量细化接口,接口中的方法应该尽量少。
需要注意的是:接口的粒度也不能太小。如果过小,则会造成接口数量过多,使设计复杂化。
优点
避免同一个接口里面包含不同类职责的方法,接口责任划分更加明确,符合高内聚低耦合的思想。
案例分析
不好的设计
类A通过接口I依赖类B,类C通过接口I依赖类D,如果接口I对于类A和类B来说不是最小接口,则类B和类D必须去实现他们不需要的方法。
接口I:
@protocol I <NSObject>
- (void)m1;
- (void)m2;
- (void)m3;
- (void)m4;
- (void)m5;
@end
类B
@interface B : NSObject<I>
@end
@implementation B
- (void)m1{ }
- (void)m2{ }
- (void)m3{ }
//实现的多余方法
- (void)m4{ }
//实现的多余方法
- (void)m5{ }
@end
类A
@interface A : NSObject
@end
@implementation A
- (void)m1:(id<I>)I{
[i m1];
}
- (void)m2:(id<I>)I{
[i m2];
}
- (void)m3:(id<I>)I{
[i m3];
}
@end
类D
@interface D : NSObject<I>
@end
@implementation D
- (void)m1{ }
//实现的多余方法
- (void)m2{ }
//实现的多余方法
- (void)m3{ }
- (void)m4{ }
- (void)m5{ }
@end
类C
@interface C : NSObject
@end
@implementation C
- (void)m1:(id<I>)I{
[i m1];
}
- (void)m4:(id<I>)I{
[i m4];
}
- (void)m5:(id<I>)I{
[i m5];
}
@end
好的设计
将臃肿的接口I拆分为独立的几个接口,类A和类C分别与他们需要的接口建立依赖关系。
@protocol I <NSObject>
- (void)m1;
@end
@protocol I2 <NSObject>
- (void)m2;
- (void)m3;
@end
@protocol I3 <NSObject>
- (void)m4;
- (void)m5;
@end
@interface B : NSObject<I,I2>
@end
@implementation B
- (void)m1{ }
- (void)m2{ }
- (void)m3{ }
@end
@interface A : NSObject
@end
@implementation A
- (void)m1:(id<I>)I{
[i m1];
}
- (void)m2:(id<I2>)I{
[i m2];
}
- (void)m3:(id<I2>)I{
[i m3];
}
@end
@interface D : NSObject<I,I3>
@end
@implementation D
- (void)m1{ }
- (void)m4{ }
- (void)m5{ }
@end
@interface C : NSObject
@end
@implementation C
- (void)m1:(id<I>)I{
[i m1];
}
- (void)m4:(id<I3>)I{
[i m4];
}
- (void)m5:(id<I3>)I{
[i m5];
}
@end
建立单一接口,不要建立庞大臃肿的接口,尽量细化接口,接口中的方法尽量少。也就是说,我们要为各个类建立专用的接口,而不要试图去建立一个很庞大的接口供所有依赖它的类去调用。在程序设计中,依赖几个专用的接口要比依赖一个综合的接口更灵活。接口是设计时对外部设定的“契约”,通过分散定义多个接口,可以预防外来变更的扩散,提高系统的灵活性和可维护性。
原则六:依赖倒置原则
定义
- Depend upon Abstractions. Do not depend upon concretions.
- Abstractions should not depend upon details. Details should depend upon abstractions
- High-level modules should not depend on low-level modules. Both should depend on abstractions.
- 依赖抽象,而不是依赖实现。
- 抽象不应该依赖细节;细节应该依赖抽象。
- 高层模块不能依赖低层模块,二者都应该依赖抽象。
1.针对接口编程,而不是针对实现编程。
2.尽量不要从具体的类派生,而是以继承抽象类或实现接口来实现。
3.关于高层模块与低层模块的划分可以按照决策能力的高低进行划分。业务层自然就处于上层模块,逻辑层和数据层自然就归类为底层。
优点
通过抽象来搭建框架,建立类和类的关联,以减少类间的耦合性。而且以抽象搭建的系统要比以具体实现搭建的系统更加稳定,扩展性更高,同时也便于维护。
举一个生活中的例子,电脑中内存或者显卡插槽,其实是一种接口,而这就是抽象;只要符合这个接口的要求,无论是用金士顿的内存,还是其它的内存,无论是4G的,还是8G的,都可以很方便、轻松的插到电脑上使用。而这些内存条就是具体实现,就是细节。
问题提出:
类A直接依赖类B,假如需要将类A改为依赖类C,则必须通过修改类A的代码来达成。这种场景下,类A一般是高层模块,负责复杂的业务逻辑;类B和类C是低层模块,负责基本的原子操作;假如修改类A,会给程序带来不必要的风险。
解决方案:
将类A修改为依赖接口I,类B和类C各自实现接口I,类A通过接口I间接与类B或者类C发生联系,则会大大降低修改类A的几率。
案例分析
有一个发工资的场景:这里,类SalaryManage(类似上面说的类A)负责工资的管理;Director(类似上面说的类B)是总监类,现在我们要通过SalaryManage类来给总监发放工资了,主要代码片段如下所示:
不好的设计
Director.m:
- (void)calculateSalary {
NSLog(@"%@总监的工资是20000",_strName);
}
SalaryManage.m:
- (void)calculateSalary:(Director *)director{
[director calculateSalary];
}
调用代码:
Director *director = [[Directoralloc] init];
director.strName = @"张三";
SalaryManage *salaryManage = [[SalaryManagealloc] init];
[salaryManage calculateSalary:director];
这样给总监发放工资的功能已经很好的实现了,现在假设需要给经理发工资,我们发现工资管理类SalaryManage没法直接完成这个功能,需要我们添加新的方法,才能完成。再假设我们还需要给普通员工、财务总监、研发总监等更多的岗位发送工资,那么我们就只能不断的去修改SalaryManage类来满足业务的需求。产生这种现象的原因就是SalaryManage与Director之间的耦合性太高了,必须降低它们之间的耦合度才行。
好的设计
我们引入一个委托EmployeeDelegate,它提供一个发放工资的方法定义,如下所示:
@protocol EmployeeDelegate <NSObject>
- (void)calculateSalary;
@end
然后我们让具体的员工类Director、Manager等都实现该委托方法,如下所示:
修改后的SalaryManage计算工资方法:
- (void)calculateSalary:(id<EmployeeDelegate>)employee{
[employee calculateSalary];
}
调用代码:
Director *director = [[Directoralloc] init];
director.strName = @"张三";
Manager *manager = [[Manageralloc] init];
manager.strName = @"李四";
SalaryManage *salaryManage = [[SalaryManagealloc] init];
[salaryManage calculateSalary:director];
[salaryManage calculateSalary:manager];
这样修改后,无论以后怎样扩展其他的岗位,都不需要再修改SalaryManage类了。代表高层模块的SalaryManage类将负责完成主要的业务逻辑(发工资),如果需要对SalaryManage类进行修改,引入错误的风险极大。所以遵循依赖倒置原则可以降低类之间的耦合性,提高系统的稳定性,降低修改程序造成的风险。
同样,采用依赖倒置原则给多人并行开发带来了极大的便利,比如在上面的例子中,刚开始SalaryManage类与Director类直接耦合时,SalaryManage类必须等Director类编码完成后才可以进行编码和测试,因为SalaryManage类依赖于Director类。按照依赖倒置原则修改后,则可以同时开工,互不影响,因为SalaryManage与Director类一点关系也没有,只依赖于协议(Java和C#中称为接口)EmployeeDelegate。参与协作开发的人越多、项目越庞大,采用依赖导致原则的意义就越重大。
总结
对这六个原则的遵守并不是是与否的问题,而是多和少的问题,也就是说,我们一般不会说有没有遵守,而是说遵守程度的多少。任何事都是过犹不及,设计模式的六个设计原则也是一样,制定这六个原则的目的不是要我们刻板的遵守他们,而是根据实际情况灵活运用。对他们的遵守程度只要在一个合理的范围内,就算是良好的设计。我们用下图来说明一下:
图中的每一条维度各代表一项原则,我们依据对这项原则的遵守程度在维度上画一个点,则如果对这项原则遵守的合理的话,这个点应该落在红色的同心圆内部;如果遵守的差,点将会在小圆内部;如果过度遵守,点将会落在大圆外部。一个良好的设计体现在图中,应该是六个顶点都在同心圆中的六边形。怎么去用它,用好它,就要依靠设计者的经验。否则一味者去使用设计原则可能会使代码出现过度设计的情况。大多数的原则都是通过提取出抽象和接口来实现,如果发生过度的设计,就会出现很多抽象类和接口,增加了系统的复杂度。
在下图中,设计1、设计2属于良好的设计,他们对六项原则的遵守程度都在合理的范围内;设计3、设计4设计虽然有些不足,但也基本可以接受;设计5则严重不足,对各项原则都没有很好的遵守;而设计6则遵守过渡了,设计5和设计6都是迫切需要重构的设计。