学习初衷与讲解方式
笔者想在 iOS 从业第三年结束之前系统学习一下关于设计模式方面的知识。而在学习设计模式之前,觉得更有必要先学习面向对象设计(OOD:Object Oriented Design)的几大设计原则,为后面设计模式的学习打下基础。
本篇分享的就是笔者近阶段学习和总结的面向对象设计的六个设计原则:
缩写 | 英文名称 | 中文名称 |
---|---|---|
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 | 依赖倒置原则 |
注意,通常所说的
SOLID
(上方表格缩写的首字母,从上到下)设计原则没有包含本篇介绍的迪米特法则,而只有其他五项。另外,本篇不包含合成/聚合复用原则(CARP),因为笔者认为该原则没有其他六个原则典型,而且在实践中也不容易违背。有兴趣的同学可以自行查资料学习。
在下一章节笔者将分别讲解这些设计原则,讲解的方式是将概念与代码及其对应的UML 类图结合起来讲解的方式。
代码的语言使用的是笔者最熟悉的Objective-C语言。虽然是一个比较小众的语言,但是因为有 UML 类图的帮助,而且主流的面向对象语言关于类,接口(Objective-C里面是协议)的使用在形式上类似,所以笔者相信语言的小众不会对知识的理解产生太大的阻力。
另外,在每个设计模式的讲解里,笔者会首先描述一个应用场景(需求点),接着用两种设计的代码来进行对比讲解:先提供相对不好的设计的代码,再提供相对好的设计的代码。而且两种代码都会附上标准的 UML 类图来进行更形象地对比,帮助大家来理解。同时也可以帮助不了解 UML 类图的读者先简单熟悉一下 UML 类图的语法。
本篇文章所展示的Demo和UML 类图都在笔者维护的一个专门的GitHub库中:object-oriented-design。
六大设计原则
本篇讲解六大设计原则的顺序大致按照难易程序排列。在这里最先讲解开闭原则,因为其在理解上比较简单,而且也是其他设计原则的基石。
注意:
- 六个原则的讲解所用的例子之间并没有关联,所以阅读顺序可以按照读者的喜好来定。
- Java语言里的接口在Objective-C里面叫做协议。虽然Demo是用Objective-C写的,但是因为协议的叫法比较小众,故后面一律用接口代替协议这个说法。
原则一:开闭原则(Open Close Principle)
定义
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
类声明了最初的在线课程所需要包含的数据:
- 课程名称
- 课程介绍
- 讲师姓名
- 文字内容
接着按照上面所说的需求变更:增加了视频,音频,直播课程:
//================== 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
里面。
我们可以看到,正是由于最初程序设计合理,所以对后面需求的增加才会处理得很好。
下面来看一下这两个设计的UML 类图,可以更形象地看出两种设计上的区别:
UML 类图对比
未实践开闭原则:
实践了开闭原则:
在实践了开闭原则的 UML 类图中,四个课程类继承了
Course
类并添加了自己独有的属性。(在 UML 类图中:实线空心三角箭头代表继承关系:由子类指向其父类)
如何实践
为了更好地实践开闭原则,在设计之初就要想清楚在该场景里哪些数据(或行为)是一定不变(或很难再改变)的,哪些是很容易变动的。将后者抽象成接口或抽象方法,以便于在将来通过创造具体的实现应对不同的需求。
原则二:单一职责原则(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; //