定义
- 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.
即:
- 依赖抽象,而不是依赖实现。
- 抽象不应该依赖细节;细节应该依赖抽象。
- 高层模块不能依赖低层模块,二者都应该依赖抽象。
定义解读
- 针对接口编程,而不是针对实现编程。
- 尽量不要从具体的类派生,而是以继承抽象类或实现接口来实现。
- 关于高层模块与低层模块的划分可以按照决策能力的高低进行划分。业务层自然就处于上层模块,逻辑层和数据层自然就归类为底层。
优点
通过抽象来搭建框架,建立类和类的关联,以减少类间的耦合性。而且以抽象搭建的系统要比以具体实现搭建的系统更加稳定,扩展性更高,同时也便于维护。
代码讲解
下面通过一个模拟项目开发的例子来讲解依赖倒置原则。
需求点
实现下面这样的需求:
用代码模拟一个实际项目开发的场景:前端和后端开发人员开发同一个项目。
不好的设计
首先生成两个类,分别对应前端和后端开发者:
前端开发者:
//================== FrondEndDeveloper.h ==================
@interface FrondEndDeveloper : NSObject
- (void)writeJavaScriptCode;
@end
//================== FrondEndDeveloper.m ==================
@implementation FrondEndDeveloper
- (void)writeJavaScriptCode{
NSLog(@"Write JavaScript code");
}
@end
后端开发者:
//================== BackEndDeveloper.h ==================
@interface BackEndDeveloper : NSObject
- (void)writeJavaCode;
@end
//================== BackEndDeveloper.m ==================
@implementation BackEndDeveloper
- (void)writeJavaCode{
NSLog(@"Write Java code");
}
@end
这两个开发者分别对外提供了自己开发的方法:writeJavaScriptCode
和writeJavaCode
。
接着创建一个Project
类:
//================== Project.h ==================
@interface Project : NSObject
//构造方法,传入开发者的数组
- (instancetype)initWithDevelopers:(NSArray *)developers;
//开始开发
- (void)startDeveloping;
@end
//================== Project.m ==================
#import "Project.h"
#import "FrondEndDeveloper.h"
#import "BackEndDeveloper.h"
@implementation Project
{
NSArray *_developers;
}
- (instancetype)initWithDevelopers:(NSArray *)developers{
if (self = [super init]) {
_developers = developers;
}
return self;
}
- (void)startDeveloping{
[_developers enumerateObjectsUsingBlock:^(id _Nonnull developer, NSUInteger idx, BOOL * _Nonnull stop) {
if ([developer isKindOfClass:[FrondEndDeveloper class]]) {
[developer writeJavaScriptCode];
}else if ([developer isKindOfClass:[BackEndDeveloper class]]){
[developer writeJavaCode];
}else{
//no such developer
}
}];
}
@end
在Project
类中,我们首先通过一个构造器方法,将开发者的数组传入project
的实例对象。然后在开始开发的方法startDeveloping
里面,遍历数组并判断元素类型的方式让不同类型的开发者调用和自己对应的函数。
思考一下,这样的设计有什么问题?
问题一:
假如后台的开发语言改成了GO语言,那么上述代码需要改动两个地方:
-
BackEndDeveloper
:需要向外提供一个writeGolangCode
方法。 -
Project
类的startDeveloping
方法里面需要将BackEndDeveloper
类的writeJavaCode
改成writeGolangCode
。
问题二:
假如后期老板要求做移动端的APP(需要iOS和安卓的开发者),那么上述代码仍然需要改动两个地方:
- 还需要给
Project
类的构造器方法里面传入IOSDeveloper
和AndroidDeveloper
两个类。而且按照现有的设计,还要分别向外部提供writeSwiftCode
和writeKotlinCode
。 -
Project
类的startDeveloping
方法里面需要再多两个elseif
判断,专门判断IOSDeveloper
和AndroidDeveloper
这两个类。
开发安卓的代码也可以用Java,但是为了和后台的开发代码区分一下,这里用了同样可以开发安卓的Kotlin语言。
很显然,在这两种假设的场景下,高层模块(Project)都依赖了低层模块(BackEndDeveloper)的改动,因此上述设计不符合依赖倒置原则。
那么该如何设计才可以符合依赖倒置原则呢?
答案是将开发者写代码的方法抽象出来,让Project类不再依赖所有低层的开发者类的具体实现,而是依赖抽象。而且从下至上,所有底层的开发者类也都依赖这个抽象,通过实现这个抽象来做自己的任务。
这个抽象可以用接口,也可以用抽象类的方式来做,在这里笔者用使用接口的方式进行讲解:
较好的设计
首先,创建一个接口,接口里面有一个写代码的方法writeCode
:
//================== DeveloperProtocol.h ==================
@protocol DeveloperProtocol <NSObject>
- (void)writeCode;
@end
然后,让前端程序员和后端程序员类实现这个接口(遵循这个协议)并按照自己的方式实现:
前端程序员类:
//================== FrondEndDeveloper.h ==================
@interface FrondEndDeveloper : NSObject<DeveloperProtocol>
@end
//================== FrondEndDeveloper.m ==================
@implementation FrondEndDeveloper
- (void)writeCode{
NSLog(@"Write JavaScript code");
}
@end
后端程序员类:
//================== BackEndDeveloper.h ==================
@interface BackEndDeveloper : NSObject<DeveloperProtocol>
@end
//================== BackEndDeveloper.m ==================
@implementation BackEndDeveloper
- (void)writeCode{
NSLog(@"Write Java code");
}
@end
最后我们看一下新设计后的Project
类:
//================== Project.h ==================
#import "DeveloperProtocol.h"
@interface Project : NSObject
//只需传入遵循DeveloperProtocol的对象数组即可
- (instancetype)initWithDevelopers:(NSArray <id <DeveloperProtocol>>*)developers;
//开始开发
- (void)startDeveloping;
@end
//================== Project.m ==================
#import "FrondEndDeveloper.h"
#import "BackEndDeveloper.h"
@implementation Project
{
NSArray <id <DeveloperProtocol>>* _developers;
}
- (instancetype)initWithDevelopers:(NSArray <id <DeveloperProtocol>>*)developers{
if (self = [super init]) {
_developers = developers;
}
return self;
}
- (void)startDeveloping{
//每次循环,直接向对象发送writeCode方法即可,不需要判断
[_developers enumerateObjectsUsingBlock:^(id<DeveloperProtocol> _Nonnull developer, NSUInteger idx, BOOL * _Nonnull stop) {
[developer writeCode];
}];
}
@end
新的Project
的构造方法只需传入遵循DeveloperProtocol协议的对象构成的数组即可。这样也比较符合现实中的需求:只需要会写代码就可以加入到项目中。
而新的startDeveloping
方法里:每次循环,直接向当前对象发送writeCode方法即可,不需要对程序员的类型做判断。因为这个对象一定是遵循DeveloperProtocol
接口的,而遵循该接口的对象一定会实现writeCode
方法(就算不实现也不会引起重大错误)。
现在新的设计接受完了,我们通过上面假设的两个情况来和之前的设计做个对比:
假设1:后台的开发语言改成了GO语言
在这种情况下,只需更改BackEndDeveloper
类里面对于DeveloperProtocol
接口的writeCode
方法的实现即可:
//================== BackEndDeveloper.m ==================
@implementation BackEndDeveloper
- (void)writeCode{
//Old:
//NSLog(@"Write Java code");
//New:
NSLog(@"Write Golang code");
}
@end
而在Project
里面不需要修改任何代码,因为Project
类只依赖了接口方法WriteCode
,没有依赖其具体的实现。
我们接着看一下第二个假设:
假设2:后期老板要求做移动端的APP(需要iOS和安卓的开发者)
在这个新场景下,我们只需要将新创建的两个开发者类:IOSDeveloper
和AndroidDeveloper
分别实现DeveloperProtocol
接口的writeCode
方法即可。
同样,Project
的接口和实现代码都不用修改:客户端只需要在Project
的构建方法的数组参数里面添加这两个新类的实例即可,不需要在startDeveloping
方法里面添加类型判断,原因同上。
我们可以看到,新设计很好地在高层类(Project
)与低层类(各种developer
类)中间加了一层抽象,解除了二者在旧设计中的耦合,使得在低层类中的改动没有影响到高层类。
同样是抽象,新设计同样也可以用抽象类的方式:创建一个Developer
的抽象类并提供一个writeCode
方法,让不同的开发者类继承与它并按照自己的方式实现writeCode
方法。这样一来,在Project
类的构造方法就是传入已Developer
类型为元素的数组了。有兴趣的小伙伴可以自己实现一下~
下面来看一下这两个设计的UML 类图,可以更形象地看出两种设计上的区别:
UML 类图对比
未实践依赖倒置原则:
实践了依赖倒置原则:
在实践了依赖倒置原则的 UML 类图中,我们可以看到
Project
仅仅依赖于新的接口;而且低层的FrondEndDevelope
和BackEndDevelope
类按照自己的方式实现了这个接口:通过接口解除了原有的依赖。(在 UML 类图中,虚线三角箭头表示接口实线,由实现方指向接口)
如何实践
今后在处理高低层模块(类)交互的情景时,尽量将二者的依赖通过抽象的方式解除掉,实现方式可以是通过接口也可以是抽象类的方式。
网友评论