定义
在面向对象的设计中有很多流行的思想,比如说 "所有的成员变量都应该设置为私有(Private)","要避免使用全局变量(Global Variables)","使用运行时类型识别(RTTI:Run Time Type Identification,例如 dynamic_cast)是危险的" 等等。那么,这些思想的源泉是什么?为什么它们要这样定义?这些思想总是正确的吗?本篇文章将介绍这些思想的基础:开放封闭原则(Open Closed Principle)。
早在1988年Bertrand Meyer 就给出了指导建议,他创造了当下非常著名的开放封闭原则。套用他的原话:"软件实体(类、模块、函数等)应对扩展开放,但对修改封闭。"。
当一个需求变化导致程序中多个依赖模块都发生了级联的改动,那么这个程序就展现出了我们所说的 "坏设计(bad design)" 的特质。应用程序也相应地变得脆弱、僵化、无法预期和无法重用。开放封闭原则(Open Closed Principle)即为解决这些问题而产生,它强调的是你设计的模块应该从不改变。当需求变化时,你可以通过添加新的代码来扩展这个模块的行为,而不去更改那些已经存在的可以工作的代码。
开放封闭原则(Open Closed Principle)描述
1. 它们 "面向扩展开放(Open For Extension)"。
也就是说模块的行为是能够被扩展的。当应用程序的需求变化时,我们可以使模块表现出全新的或与以往不同的行为,以满足新的需求。
2. 它们 "面向修改封闭(Closed For Modification)"。
模块的源代码是不能被侵犯的,任何人都不允许修改已有源代码。
案例-小故事
我们在做工程,定义一个矩形类,有一天甲方需要我们编写一个应用,能够计算一系列长方形的总面积,那么我们的代码看上去可能是这样的:
class Rectangle:
def __init__(self, width, height):
# 一个简单的矩形类,拥有长和高
self.width = width
self.height = height
def area_calculate(shape: Rectangle):
return shape.width * shape.height
if __name__ == '__main__':
r = Rectangle(3, 4)
print(f'矩形面积为{area_calculate(r)}')
我们将方案给到甲方,甲方很满意,但是甲方说:我们还需要一个计算圆形面积的方案。
事情开始变得有些棘手了,但是经过一番思考, 我们带来了新的方案,那就是将 area 方法进行改造,不止能够处理长方形集合,还能处理其他对象的集合。通过类型检查的方式,将对象转换为相应的类型,再进行面积计算以保证计算时使用了正确的公式。
现在代码看起来是这样的:
import math
from typing import Union
class Rectangle:
def __init__(self, width, height):
# 一个简单的矩形类,拥有长和高
self.width = width
self.height = height
class Circle:
def __init__(self, radius):
self.radius = radius
def area_calculate(shape: Union[Rectangle, Circle]):
if isinstance(shape, Rectangle):
return shape.width * shape.height
if isinstance(shape, Circle):
return math.pi * shape.radius ** 2
if __name__ == '__main__':
r = Rectangle(3, 4)
print(f'矩形面积为{area_calculate(r)}')
r = Circle(3)
print(f'圆形面积为{area_calculate(r)}')
甲方很开心,这个方案没问题
好景不长,一周后甲方打电话给我们并要求:“扩展一下支持三角形面积的计算,我想这并不困难,对吧”。
…
可以料见的是,如果甲方的需求无止尽的话,所有的代码都需要一改再改。如果本例中的的area_calculate
函数并不是简单地计算个面积,而是上个程序员已经封装好的复杂函数,每次需求变动我们都需要翻动其他人的代码然后改个不停,这相当灾难!
所以本例中怎么办最好?这就回到了开放封闭原则上,我们需要area_calculate
对所有的shape都有一个普适的面积输出,那么我们就定义一个基类shape,规定好所有shape需要实现的特性,以应对复杂的需求场景。
现在,我们的代码看起来是这样的
import math
from abc import abstractmethod, ABC
from typing import Union
class Shape(ABC):
def __init__(self):
pass
@abstractmethod
def cal_area(self) -> Union[float, int]:
pass # 接下来规定所有的shape类都应该实现一个计算自身面积的方法
class Rectangle(Shape):
def __init__(self, width, height):
# 一个简单的矩形类,拥有长和高
super().__init__()
self.width = width
self.height = height
def cal_area(self) -> Union[float, int]:
return self.width * self.height
class Circle(Shape):
def __init__(self, radius):
super().__init__()
self.radius = radius
def cal_area(self) -> Union[float, int]:
return math.pi * self.radius ** 2
def area_calculate(shape: Shape):
return shape.cal_area()
if __name__ == '__main__':
r = Rectangle(3, 4)
print(f'矩形面积为{area_calculate(r)}')
r = Circle(3)
print(f'圆形面积为{area_calculate(r)}')
看上去就是把方法挪了个位置,这有什么用?这只是个简单的案例,实际工作中工程函数所要实现的特性是非常多的,这么改完成了一项壮举:再也不用修改area_calculate
函数了,也就是说area_calculate
函数”实现达到了对扩展开放而对修改关闭的效果“。
那么想加个三角形面积的计算,已经不需要修改源码,定义一个继承Shape类的Triangle
类,实现规定的接口即可。引物足够简单,这里就不给代码了。
启示
一句话概括:如果预料到未来有足够多的扩展需求,那么就应尽可能在开放封闭原则上下功夫,但是如果代码不需要未来扩展,则以实现业务为核心开发即可。
回顾一下应用开闭原则之前的场景,是哪里出了问题呢?很明显,对 area 方法的实现不是对扩展开放的。那么问题来了,这是必须的吗?我只能说,这完全取决于上下文场景。设想,如果我们从一开始就对甲方的需求(稳定性)表示怀疑,并且预料到后续会有对其他图形进行面积计算的要求,那也许就可以在第一时间着手准备了。然而,在需求发生变化前试图揣测并进行预判,显然以我现在的第六感能力还做不到 ;-D,并且对未发生的变化进行预设很容易踏入过渡设计的陷阱,俗语有云,过犹不及。那么,我会建议更多关注如何将代码实现得容易扩展以应对变化的需求。
一旦需求开始发生变化,那它很可能会以相似的方式一变再变。正如之前场景,Aldford 希望我们支持第二种图形的面积计算后,很快就提出需要支持第三种,甚至第四种。
换句话说,一旦需求开始发生变化,我们就应该在代码循环开放封闭原则上多下功夫。在大多数场景中,在需求发生变化之前,我建议避免过度设计,使代码能够支持业务即可,从而在需求开始变化之初,能够更容易地进行重构。
参考
https://www.cnblogs.com/gaochundong/p/open_closed_principle.html
文章评论