定义
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 类图中:实线空心三角箭头代表继承关系:由子类指向其父类)
如何实践
为了更好地实践开闭原则,在设计之初就要想清楚在该场景里哪些数据(或行为)是一定不变(或很难再改变)的,哪些是很容易变动的。将后者抽象成接口或抽象方法,以便于在将来通过创造具体的实现应对不同的需求。