开闭原则

开闭原则定义


开闭原则是编程世界里面最基础的设计原则,它指导我们如何建立一个稳定,灵活的系统。

一个软件实体,如类、模块和函数应该对拓展开放,对修改关闭

开闭原则是什么意思


开闭原则明确的告诉我们:软件实现应该对拓展开发,对修改关闭。

事实很容易理解,如果我们在维护一个项目,相比去改变代码,我们更愿意增加代码以实现功能;我认为原因有二:

  • 想要改变原来代码需要去理解原来的代码,不论项目大小这都是一项折磨人的事情
  • 改变原来的代码有很大的风险,很容易影响之前的逻辑,特别是对后来加入的人来说

一个软件只要在生命周期内,都会发生变化,既然变化是一个事实,我们就应该在设计是尽量适应这些变化,以提高项目的稳定性和灵活性,真正实现“拥抱变化”

我们举例说明什么是开闭原则,以书店销售书籍为例,其类图如下:

书籍接口:

1
2
3
4
5
6
// typescript 代码
interface IBook{
getName(): string;
getPrice(): string;
getAuthor(): string;
}

小说类书籍:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// typescript
class NovelBook implements IBook {
protected name: string
protected price: number
protected author: string

constructor(name: string, price: number, author: string) {
this.name = name
this.price = price
this.author = author
}

getAuthor() {
return this.author
}
getName() {
return this.name
}
getPrice() {
return this.price
}
}

调用小说类:

1
2
const book = new NovelBook('笑傲江湖',100,'金庸')
book.getAuthor() // 金庸

项目投入生产,书籍正常销售,但是我们经常因为各种原因,要打折销售书籍。

也就是说我们要改变我们的书籍价格,getPrice()

原文分析了三种方法:

  • 修改接口 - IBook:
    在IBook接口中,增加一个方法getOffrice(),专门用于进行打折处理,所有的实现类实现此方法。但是这样的一个修改方式,实现类NovelBook要修改,同时IBook接口应该稳定且可靠,不应该经常发生改变,否则接口作为契约的作用就失去了。因此,此方案否定。

  • 修改实现类 - NovelBook:
    修改NovelBook类的方法,直接在getPrice()方法中实现打折处理。此方法也是有问题的,例如我们还需要读取书籍打折前的价格呢?这不是问题吗?当然我们可以再增加getOffPrice()方法,这也是实现其需求的一种方法,但是这就有两个读取价格的方法。因此这不是最优的方案。

  • 通过拓展实现变化
    我们可以增加一个子类OffNovelBook,覆盖getPrice()方法。此方法修改少,对现有的代码没有影响,风险少,是个好办法。

打折类:

1
2
3
4
5
6
7
8
9
10
11
12
class OffNovelBook extends NovelBook {
constructor(name: string, price: number, author: string) {
super(name, price, author)
}
getPrice() {
if (this.price > 40) {
return this.price * 0.8
} else {
return this.price * 0.9
}
}
}

这样打折销售开发就完成了,我们只是增加了一个OffNovelBook类,我们修改的代码都是高层次的模块,没有修改底层模块,代码改变量少,可以有效的防止风险的扩散。

我们可以把变化归纳为两种类型:

  • 逻辑变化
    只改变了一个逻辑,而不涉及其他模块,比如一个算法是a*b*c,现在需要修改为a+b+c,可以直接通过修改原有类中方法的方式来完成,前提条件是所有一览或关联类都按照相同的逻辑处理
  • 子模块变化
    一人模块变化,会对其他的模块产生影响,特别是一个低层次的模块变化必然引起高层模块的变化,因此在通过拓展完成变化。

为什么使用开闭原则


第一:开闭原则非常有名,只要是面向对象编程,在开发时都会强调开闭原则

第二:开闭原则是最基础的设计原则,其它的五个设计原则都是开闭原则的具体形态,也就是说其它的五个设计原则是指导设计的工具和方法,而开闭原则才是其精神领袖。依照java语言的称谓,开闭原则是抽象类,而其它的五个原则是具体的实现类。

第三:开闭原则可以提高复用性
在面向对象的设计中,所有的逻辑都是从原子逻辑组合而来,不是在一个类中独立实现一个业务逻辑。只有这样的代码才可以复用,粒度越小,被复用的可能性越大。那为什么要复用呢?减少代码的重复,避免相同的逻辑分散在多个角落,减少维护人员的工作量。那怎么才能提高复用率呢?缩小逻辑粒度,直到一个逻辑不可以分为止。

第四:开闭原则可以提高维护性
一款软件量产后,维护人员的工作不仅仅对数据进行维护,还可能要对程序进行扩展,维护人员最乐意的事是扩展一个类,而不是修改一个类。让维护人员读懂原有代码,再进行修改,是一件非常痛苦的事情,不要让他在原有的代码海洋中游荡后再修改,那是对维护人员的折磨和摧残。

第五:面向对象开发的要求
万物皆对象,我们要把所有的事物抽象成对象,然后针对对象进行操作,但是万物皆发展变化,有变化就要有策略去应对,怎么快速应对呢?这就需要在设计之初考虑到所有可能变化的因素,然后留下接口,等待“可能”转变为“现实”。

开闭原则如何使用


第一:抽象约束
抽象是对一组事物的通用描述,没有具体的实现,也就表示它可以有非常多的可能性,可以跟随需求的变化而变化。因此,通过接口或抽象类可以约束一组可能变化的行为,并且能够实现对扩展开放,其包含三层含义:

通过接口或抽象类约束扩散,对扩展进行边界限定,不允许出现在接口或抽象类中不存在的public方法。
参数类型,引用对象尽量使用接口或抽象类,而不是实现类,这主要是实现里氏替换原则的一个要求
抽象层尽量保持稳定,一旦确定就不要修改
第二:元数据(metadata)控件模块行为
编程是一个很苦很累的活,那怎么才能减轻压力呢?答案是尽量使用元数据来控制程序的行为,减少重复开发。什么是元数据?用来描述环境和数据的数据,通俗的说就是配置参数,参数可以从文件中获得,也可以从数据库中获得。

第三:制定项目章程
在一个团队中,建立项目章程是非常重要的,因为章程是所有人员都必须遵守的约定,对项目来说,约定优于配置。这比通过接口或抽象类进行约束效率更高,而扩展性一点也没有减少。

第四:封装变化
对变化封装包含两层含义:
(1)将相同的变化封装到一个接口或抽象类中
(2)将不同的变化封装到不同的接口或抽象类中,不应该有两个不同的变化出现在同一个接口或抽象类中。
封装变化,也就是受保护的变化,找出预计有变化或不稳定的点,我们为这些变化点创建稳定的接口。

原文链接-六大设计原则之开闭原则