学习初衷与讲解方式

这篇博客是原作者对于三年iOS工作经验的记录,里面简单介绍了关于设计模式方面的知识。在学习设计模式之前,需要对面向对象设计的几大设计原则需要有一定的了解,为后面的设计模式的学习打好基础。

下面是总结的对设计模式的六大设计原则:

SOLID:SRP(单一职责原则)、OCP(开闭原则)、LSP(里氏替换原则)、LoD(迪米特法则(最少知道原则))、ISP(接口分离原则)、DIP(依赖倒置原则)

注意,通常所说的SOLID(上方表格缩写的首字母,从上到下)设计原则没有包含本篇介绍的迪米特法则,而只有其他五项。另外,本篇不包含合成/聚合复用原则(CARP),因为笔者认为该原则没有其他六个原则典型,而且在实践中也不容易违背。有兴趣的同学可以自行查资料学习。

在下一章节笔者将分别讲解这些设计原则,讲解的方式是将概念与代码及其对应的UML 类图结合起来讲解的方式。

代码的语言使用的是笔者最熟悉的Objective-C语言。虽然是一个比较小众的语言,但是因为有 UML 类图的帮助,而且主流的面向对象语言关于类,接口(Objective-C里面是协议)的使用在形式上类似,所以笔者相信语言的小众不会对知识的理解产生太大的阻力。

另外,在每个设计模式的讲解里,笔者会首先描述一个应用场景(需求点),接着用两种设计的代码来进行对比讲解:先提供相对不好的设计的代码,再提供相对好的设计的代码。而且两种代码都会附上标准的 UML 类图来进行更形象地对比,帮助大家来理解。同时也可以帮助不了解 UML 类图的读者先简单熟悉一下 UML 类图的语法。

六大设计原则

本篇讲解六大设计原则的顺序按照难易程度排列。在这里最先讲解开闭原则,因为其在理解上比较简单,而且也是其他设计原则的基石。

注意:

  1. 六个原则的讲解所用的例子之间并没有关联,所以阅读顺序可以按照读者的喜好来定。
  2. Java语言里的接口在Objective-C里面叫做协议。虽然demo是用OC写的,但是因为协议的叫法比较小众,故后面一律用接口代替协议这个说法。

原则一:开闭原则(open close principle)

定义
Software entities (classes,modules,functions,etc.)should be open for extension,but closed for modification.
即:一个软件实体如类、模块和函数应该对扩展开放,对修改关闭

定义的解读

  • 用抽象构建框架,用实现扩展细节
  • 不以改动原有类的方式来实现需求,而是应该以实现事先抽象出来的接口(或具体类继承抽象类)的方式来实现。

优点

实践开闭原则的优点在于可以在不改动原有代码的前提下给程序扩展功能。增加了程序的可扩展性,同时也降低了程序的维护成本。

代码讲解

下面通过一个简单的关于在线课程的例子讲解一下开闭原则的实践。

需求点

设计一个在线课程类:

由于教学资源有限,开始的时候只有类似于博客的,通过文字讲解的课程。但是随着教学资源的增多,后来增加了视频的课程、音频、直播课程等。

先来看一下不好的设计:

不好的设计

最开始的文字课程类:

@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类声明了最初的在线课程所需要包含的数据:

  • 课程名称
  • 课程介绍
  • 讲师姓名
  • 文字内容

接着按照上面所少的需求变更:增加了视频、音频、直播课程:

@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类实例化的视频课程对象会包含并不属于自己的数据:audioUrlliveUrl:这样就造成了冗余,视频课程对象并不是纯粹的视频课程对象,它包含了音频地址,直播地址等成员。

很显然,这个设计不是一个好的设计,因为(对应上面两段叙述):

1.随着需求的增加,需要反复修改之前创建的类。
2.给新增的类造成了不必要的冗余。

之所以会造成上述两个缺陷,是因为该设计没有遵循对修改关闭,对扩展开放的开闭原则,而是反其道而行之:开放修改,而且不给扩展提供便利。

难么怎么做可以遵循开闭原则呢?下面看一下遵循开闭原则的较好的设计:

较好的设计
首先在Course类中仅仅保留所有课程都含有的数据

@interface Course : NSObject

@property (nonatomic, copy) NSString *courseTitle;         //课程名称
@property (nonatomic, copy) NSString *courseIntroduction;  //课程介绍
@property (nonatomic, copy) NSString *teacherName;         //讲师姓名

@end

接着,针对文字课程,视频课程,音频课程,直播课程这三种新型的课程采用继承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

这样一来,上面的两个问题都得到了解决:

1.随着课程类型的增加,不需要反复修改最初的父类(Course),只需要新建一个继承于它的子类并在子类中添加仅属于该子类的数据(或行为)即可。
2.因为各种课程独有的数据(或行为)都被分散到了不同的课程子类里,所以每个子类的数据(或行为)没有任何冗余。

而且对于第二点:或许今后的视频课程可以有高清地址,视频加速功能。而这些功能只需要在VideoCourse类里添加即可,因为它们都是视频课程所独有的。同样地,直播课程后面还可以支持在线问答功能,也可以仅加在LiveCourse里面。

我们可以看到,正是由于最初程序设计合理,所以对后面需求的增加才会处理得很好。
下面来看一下这两个设计的UML类图,可以更形象地看出两种设计上的区别:

UML 类图对比

未实践开闭原则:

avatar


实践了开闭原则:

avatar

在实践了开闭原则的UML 类图中,四个课程类继承了Course类并添加了自己独有的属性。(在UML 类图中:实线空心三角箭头代表继承关系:由子类指向其父类)

如何实践

为了更好地实践开闭原则,在设计之初就要想清楚在该场景里哪些数据(或行为)是一定不变(或很难再改变)的,哪些是很容易变动的。将后者抽象成接口或抽象方法,以便于在将来通过创造具体的实现应对不同的需求。

自己的看法

开闭原则保证了基础功能模块对外开放,而外部具体模块可以扩展其功能,封闭其对内的侵入(修改)。作者是采用继承的方式讲解这一设计理念,其实在Java中充斥着大量的抽象类和接口,其作用和这一理念非常类似。

原则二:单一职责原则(Single Responsibility Principle)

定义

A class should have a single responsibility, where a responsibility is nothing but a reason to change.

即:一个类只允许有一个职责,即只有一个导致该类变更的原因。

定义的解读

  • 类职责的变化往往就是导致类变化的原因:也就是说如果一个类具有多种职责,就会有多种导致这个类变化的原因,从而导致这个类的维护变得困难。
  • 往往在软件开发中随着需求的不断增加,可能会给原来的类添加一些本来不属于它的一些职责,从而违反了单一职责原则。如果我们发现当前类的职责不仅仅有一个,就应该将本来不属于该类真正的职责分离出去。
  • 不仅仅是类,函数(方法)也要遵循单一职责原则,即:一个函数(方法)只做一件事情。如果发现一个函数(方法)里面有不同的任务,则需要将不同的任务以另一个函数(方法)的形式分离出去。

优点

如果类与方法的职责划分得很清晰,不但可以提高代码的可读性,更实际性地更降低了程序出错的风险,因为清晰的代码会让bug无处藏身,也有利于bug的追踪,也就是降低了程序的维护成本。

代码讲解

单一职责原则的demo比较简单,通过对象(属性)的设计上讲解已经足够,不需要具体的客户端调用。我们先看一下需求点:

需求点

初始需求:需要创造一个员工类,这个类有员工的一些基本信息。
新需求:增加两个方法:

  • 判定员工在今年是否升职
  • 计算员工的薪水

先来看一下不好的设计:

不好的设计

//================== 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

由上面的代码可以看出:

  • 在初始需求下,我们创建了Employee这个员工类,并声明了3个员工信息的属性:员工姓名,地址,员工ID
  • 在新需求下,两个方法直接加到了员工类里面.

新需求的做法看似没有问题,因为都是和员工有关的,但却违反了单一职责原则:因为这两个方法并不是员工本身的职责

  • calculateSalary这个方法的职责是属于会计部门的:薪水的计算是会计部门负责。
  • willPromotionThisYear这个方法的职责是属于人事部门的:考核与晋升机制是人事部门负责。

而上面的设计将本来不属于员工自己的职责强加进了员工类里面,而这个类的设计初衷(原始职责)就是单纯地保留员工的一些信息而已。因此这么做就是给这个类引入了新的职责,故此设计违反了单一职责原则

我们可以简单想象一下这么做的后果是什么:如果员工的晋升机制变了,或者税收政策等影响员工工资的因素变了,我们还需要修改当前这个类。
那么怎么做才能不违反单一职责原则呢?- 我们需要将这两个方法(责任)分离出去,让本应该处理这类任务的类来处理。

较好的设计

我们保留员工类的基本信息:

//================== Employee.h ==================

@interface Employee : NSObject

//初始需求
@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *address;
@property (nonatomic, copy) NSString *employeeID;

接着创建新的会计部门类

//================== 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

通过创建了两个分别专门处理薪水和晋升的部门,会计部门和人事部门的类:FinancialApartmentHRApartment,把两个任务(责任)分离了出去,让本该处理这些职责的类来处理这些职责。

这样一来,不仅仅在此次新需求中满足了单一职责原则,以后如果还要增加人事部门和会计部门处理的任务,就可以直接在这两个类里面添加即可。

下面来看一下这两个设计的UML 类图,可以更形象地看出两种设计上的区别:

UML 类图对比

未实践单一职责原则:

avatar


实践了单一职责原则:

avatar

可以看到,在实践了单一职责原则的UML 类图中,不属于Employee的两个职责被分类了FinancialApartment类 和HRApartment类。(在 UML 类图中,虚线箭头表示依赖关系,常用在方法参数等,由依赖方指向被依赖方)

上面说过除了类要遵循单一职责设计原则之外,在函数(方法)的设计上也要遵循单一职责的设计原则。因函数(方法)的单一职责原则理解起来比较容易,故在这里就不提供Demo和UML 类图了。

可以简单举一个例子:

APP的默认导航栏的样式是这样的:

  • 白色底
  • 黑色标题
  • 底部有阴影
    那么创建默认导航栏的伪代码可能是这样子的:
//默认样式的导航栏
- (void)createDefaultNavigationBarWithTitle:(NSString *)title{

    //create white color background view

    //create black color title

    //create shadow bottom
}

现在我们可以用这个方法统一创建默认的导航栏了。 但是过不久又有新的需求来了,有的页面的导航栏需要做成透明的,因此需要一个透明样式的导航栏:

  • 透明底
  • 白色标题
  • 底部无阴影
    针对这个需求,我们可以新增一个方法:
//透明样式的导航栏
- (void)createTransParentNavigationBarWithTitle:(NSString *)title{

    //create transparent color background view

    //create white color title
}

看出问题来了么?在这两个方法里面,创造background view和 title color title的方法的差别仅仅是颜色不同而已,而其他部分的代码是重复的。 因此我们应该将这两个方法抽出来:

//根据传入的颜色参数设置导航栏的背景色
- (void)createBackgroundViewWithColor:(UIColor)color;

//根据传入的标题字符串和颜色参数设置标题
- (void)createTitlewWithColorWithTitle:(NSString *)title color:(UIColor)color;

而且上面的制造阴影的部分也可以作为方法抽出来:

- (void)createShadowBottom;

这样一来,原来的两个方法可以写成:

//默认样式的导航栏
- (void)createDefaultNavigationBarWithTitle:(NSString *)title{

    //设置白色背景
    

[self createBackgroundViewWithColor:[UIColor whiteColor]

]; //设置黑色标题

[self createTitlewWithColorWithTitle:title color:[UIColor blackColor]

]; //设置底部阴影

[self createShadowBottom]

; } //透明样式的导航栏 - (void)createTransParentNavigationBarWithTitle:(NSString *)title{ //设置透明背景

[self createBackgroundViewWithColor:[UIColor clearColor]

]; //设置白色标题

[self createTitlewWithColorWithTitle:title color:[UIColor whiteColor]

]; }

而且我们也可以将里面的方法拿出来在外面调用也可以:

设置默认样式的导航栏:

//设置白色背景
[navigationBar createBackgroundViewWithColor:[UIColor whiteColor]];

//设置黑色标题
[navigationBar createTitlewWithColorWithTitle:title color:[UIColor blackColor]];

//设置阴影
[navigationBar createShadowBottom];

设置透明样式的导航栏:

//设置透明色背景
[navigationBar createBackgroundViewWithColor:[UIColor clearColor]];

//设置白色标题
[navigationBar createTitlewWithColorWithTitle:title color:[UIColor whiteColor]];

这样一来,无论写在一个大方法里面调用或是分别在外面调用,都能很清楚地看到导航栏的每个元素是如何生成的,因为每个职责都分配到了一个单独的方法里面。而且还有一个好处是,透明导航栏如果遇到浅色背景的话,使用白色字体不如使用黑色字体好,所以遇到这种情况我们可以在createTitlewWithColorWithTitle:color:方法里面传入黑色色值。

而且今后可能还会有更多的导航栏样式,那么我们只需要分别改变传入的色值即可,不需要有大量的重复代码了,修改起来也很方便。

如何实践

对于上面的员工类的例子,或许是因为我们先入为主,知道一个公司的合理组织架构,觉得这么设计理所当然。但是在实际开发中,我们很容易会将不同的责任揉在一起,这点还是需要开发者注意的。

自己看法

单一职责原则其实就是尽可能使或者方法的职责单一,我们不能因为方便把他们融合在一起,不同的模块负责不同的职能。当模块需要变更时我们只需要变更其所属职能,这样,逻辑就会清晰许多。不难发现,单一职能原则很大程度上降低了代码的耦合度,当我们遇到不停变更的业务需求时也能从容面对,不用提着刀去跟产品理论了。。。

原则三:依赖倒置原则(Dependency Inversion Principle)

定义

  • 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

这两个开发者分别对外提供了自己开发的方法:writeJavaScriptCodewriteJavaCode

接着创建一个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类的构造器方法里面传入iOSDeveloperAndroidDeveloper两个类。而且按照现有的设计,还要分别向外部提供writeSwiftCodewriteKotlinCode
    -Project类的startDeveloping方法里面需要再多两个elseif判断,专门判断iOSDeveloperAndroidDeveloper这两个类。

开发安卓的代码也可以用Java,但是为了和后台的开发代码区分一下,这里用了同样可以开发安卓的Kotlin语言。

很显然,在这两种假设的场景下,高层模块(Project)都依赖了低层模块(BackEndDeveloper)或(FrondEndDeveloper)的改动,因此上述设计不符合依赖倒置原则

那么该如何设计才可以符合依赖倒置原则呢?
答案是将开发者写代码的方法抽象出来,让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和安卓的开发者)

在这个新场景下,我们只需要将新创建的两个开发者类:iOSDeveloperAndroidDeveloper分别实现DeveloperProtocol接口的writeCode方法即可。

同样,Project的接口和实现代码都不用修改:客户端只需要在Project的构建方法的数组参数里面添加这两个新类的实例即可,不需要在startDeveloping方法里面添加类型判断,原因同上。
我们可以看到,新设计很好地在高层类(Project)与低层类(各种developer类)中间加了一层抽象,解除了二者在旧设计中的耦合,使得在低层类中的改动没有影响到高层类。
同样是抽象,新设计同样也可以用抽象类的方式:创建一个Developer的抽象类并提供一个writeCode方法,让不同的开发者类继承与它并按照自己的方式实现writeCode方法。这样一来,在Project类的构造方法就是传入已Developer类型为元素的数组了。
下面来看一下这两个设计的UML 类图,可以更形象地看出两种设计上的区别:

UML 类图对比

avator
avator

未实践依赖倒置原则:

实践了依赖倒置原则:

在实践了依赖倒置原则的UML 类图中,我们可以看到Project仅仅依赖于新的接口;而且低层的FrondEndDevelopeBackEndDevelope类按照自己的方式实现了这个接口:通过接口解除了原有的依赖。(在UML 类图中,虚线三角箭头表示接口实线,由实现方指向接口

如何实践

今后在处理高低层模块(类)交互的情景时,尽量将二者的依赖通过抽象的方式解除掉,实现方式可以是通过接口也可以是抽象类的方式。

自己看法

这里原作者是采用接口的方式来解除高位到低位的依赖,这里有点类似我们老生常谈的工厂模式。工厂是通过继承的方式子类实现父类声明的方法来进行数据的解耦,其设计理念和本理念很类似,感兴趣的读者可以自行了解工厂模式

原则四:接口分离原则(Interface Segregation Principle)

定义

Many client specific interfaces are better than one general purpose interface.

即:多个特定的客户端接口要好于一个通用性的总接口。

定义解读

  • 客户端不应该依赖它不需要实现的接口。
  • 不建立庞大臃肿的接口,应尽量细化接口,接口中的方法应该尽量少。

需要注意的是:接口的粒度也不能太小。如果过小,则会造成接口数量过多,使设计复杂化。看来接口的设计既不能太大也不能太小。

优点

避免同一个接口里面包含不同类职责的方法,接口责任划分更加明确,符合高内聚低耦合的思想。

代码讲解

下面通过一个餐厅服务的例子讲解一下接口分离原则。

需求点

现在的餐厅除了提供传统的店内服务,多数也都支持网上下单,网上支付功能。写一些接口方法来涵盖餐厅的所有的下单及支付功能。

不好的设计

//================== RestaurantProtocol.h ==================

@protocol RestaurantProtocol <NSObject>

- (void)placeOnlineOrder;         //下订单:online
- (void)placeTelephoneOrder;      //下订单:通过电话
- (void)placeWalkInCustomerOrder; //下订单:在店里

- (void)payOnline;                //支付订单:online
- (void)payInPerson;              //支付订单:在店里支付

@end

在这里声明了一个接口,它包含了下单和支付的几种方式:

  • 下单
  • online下单
  • 电话下单
  • 店里下单(店内服务)
  • 支付
    • online支付(适用于online下单和电话下单的顾客)
    • 店里支付(店内服务)

这里先不讨论电话下单的顾客是用online支付还是店内支付。

对应的,我们有三种下单方式的顾客:

1.online下单,online支付的顾客
//================== OnlineClient.h ==================

#import "RestaurantProtocol.h"

@interface OnlineClient : NSObject<RestaurantProtocol>
@end



//================== OnlineClient.m ==================

@implementation OnlineClient

- (void)placeOnlineOrder{
    NSLog(@"place on line order");
}

- (void)placeTelephoneOrder{
    //not necessarily
}

- (void)placeWalkInCustomerOrder{
    //not necessarily
}

- (void)payOnline{
    NSLog(@"pay on line");
}

- (void)payInPerson{
    //not necessarily
}
@end

2.电话下单,online支付的顾客

//================== TelephoneClient.h ==================

#import "RestaurantProtocol.h"

@interface TelephoneClient : NSObject<RestaurantProtocol>
@end



//================== TelephoneClient.m ==================

@implementation TelephoneClient

- (void)placeOnlineOrder{
    //not necessarily
}

- (void)placeTelephoneOrder{
    NSLog(@"place telephone order");
}

- (void)placeWalkInCustomerOrder{
    //not necessarily
}

- (void)payOnline{
    NSLog(@"pay on line");
}

- (void)payInPerson{
    //not necessarily
}

@end

3.在店里下单并支付的顾客:

//================== WalkinClient.h ==================

#import "RestaurantProtocol.h"

@interface WalkinClient : NSObject<RestaurantProtocol>
@end



//================== WalkinClient.m ==================

@implementation WalkinClient

- (void)placeOnlineOrder{
   //not necessarily
}

- (void)placeTelephoneOrder{
    //not necessarily
}

- (void)placeWalkInCustomerOrder{
    NSLog(@"place walk in customer order");
}

- (void)payOnline{
   //not necessarily
}

- (void)payInPerson{
    NSLog(@"pay in person");
}

@end

我们发现,并不是所有顾客都必须要实现RestaurantProtocol里面的所有方法。由于接口方法的设计造成了冗余,因此该设计不符合接口隔离原则。

较好的设计

要符合接口隔离原则,只需要将不同类型的接口分离出来即可。我们将原来的RestaurantProtocol接口拆分成两个接口:下单接口和支付接口。

下单接口:
//================== RestaurantPlaceOrderProtocol.h ==================

@protocol RestaurantPlaceOrderProtocol <NSObject>

- (void)placeOrder;

@end
支付接口:
//================== RestaurantPaymentProtocol.h ==================

@protocol RestaurantPaymentProtocol <NSObject>

- (void)payOrder;

@end

现在有了下单接口和支付接口,我们就可以让不同的客户来以自己的方式实现下单和支付操作了:

首先创建一个所有客户的父类,来遵循这个两个接口:

//================== Client.h ==================

#import "RestaurantPlaceOrderProtocol.h"
#import "RestaurantPaymentProtocol.h"

@interface Client : NSObject<RestaurantPlaceOrderProtocol,RestaurantPaymentProtocol>
@end

接着另online下单,电话下单,店内下单的顾客继承这个父类,分别实现这两个接口的方法:

1.online下单,online支付的顾客
//================== OnlineClient.h ==================

#import "Client.h"
@interface OnlineClient : Client
@end



//================== OnlineClient.m ==================

@implementation OnlineClient

- (void)placeOrder{
    NSLog(@"place on line order");
}

- (void)payOrder{
    NSLog(@"pay on line");
}

@end
2.电话下单,online支付的顾客
//================== TelephoneClient.h ==================
#import "Client.h"
@interface TelephoneClient : Client
@end



//================== TelephoneClient.m ==================
@implementation TelephoneClient

- (void)placeOrder{
    NSLog(@"place telephone order");
}

- (void)payOrder{
    NSLog(@"pay on line");
}

@end
3.在店里下单并支付顾客:
//================== WalkinClient.h ==================

#import "Client.h"
@interface WalkinClient : Client
@end



//================== WalkinClient.m ==================

@implementation WalkinClient

- (void)placeOrder{
    NSLog(@"place walk in customer order");
}

- (void)payOrder{
    NSLog(@"pay in person");
}

@end

因为我们把不同职责的接口拆开,使得接口的责任更加清晰,简洁明了。不同的客户端可以根据自己的需求遵循所需要的接口来以自己的方式实现。
而且今后如果还有和下单或者支付相关的方法,也可以分别加入到各自的接口中,避免了接口的臃肿,同时也提高了程序的内聚性。
下面来看一下这两个设计的UML 类图,可以更形象地看出两种设计上的区别:

UML 类图对比

未实践接口分离原则:

avator


实践了接口分离原则:

avator

通过遵守接口分离原则,接口的设计变得更加简洁,而且各种客户类不需要实现自己不需要实现的接口。

如何实践

在设计接口时,尤其是在向现有的接口添加方法时,我们需要仔细斟酌这些方法是否是处理同一类任务的:如果是则可以放在一起;如果不是则需要做拆分。
做iOS开发的朋友对UITableViewUITableViewDelegateUITableViewDataSource这两个协议应该会非常熟悉。这两个协议里的方法都是与UITableView相关的,但iOS SDK的设计者却把这些方法放在不同的两个协议中。原因就是这两个协议所包含的方法所处理的任务是不同的两种:
UITableViewDelegate:含有的方法是UITableView的实例告知其代理一些点击事件的方法,即事件的传递,方向是从UITableView的实例到其代理。UITableViewDataSource:含有的方法是UITableView的代理传给UITableView一些必要数据供UITableView展示出来,即数据的传递,方向是从UITableView的代理到UITableView

很显然,UITableView协议的设计者很好地实践了接口分离的原则,值得我们大家学习。

我的看法

接口分离原则我之前真的没有注意过,开发中也没有刻意去关注这些。对此不敢多做评价,作者已经在描述中很清楚的讲解了。读者若存在疑问可以多读几遍。


最后:后面的两大设计原则将继续更新,再次感谢原作者掘金大神J_Knight_的文章,转载注明出处,原文



你远道而来这人世间,想必也是因为热爱吧 ||