设计模式-开闭原则

开闭原则

概述

在软件领域,我们有时候写代码的过程中,有时候会遇到一些问题,这些问题已经被前人进行总结 归纳。 帮助我们在遇到问题的时候,如何写出更加优雅的代码,更加简洁的代码,更加没有重复的代码等。 于是就出现了很多设计原则,通过这些设计原则可以帮助我们写出更加优雅的代码,以及可以应对以后 代码的可能变动。实现低耦合,高内聚,代码复用的特点。 今天我们就来学习软件设计原则之一,开闭原则 这一原则看起来 简单,但是有的时候也会产生过度设计的臭味。

废话不多说,进入正题。

开闭原则,英文缩写OCP,全称Open-Closed Principle

原始定义:Software entities (classes, modules, functions) should be open for extension but closed for modification

如果程序中有一处改动就会有连锁 的反应,导致一系列的相关模块的改动,那么设计就具有僵化的臭味。 OCP建议我们应该对系统重构,这样以后对系统在进行那样的改动的时候,就不会导致更多的修改。 如果可以正确的应用 OCP ,每次系统修改时候,只是扩展代码,而不是修改原来的源代码。这样已经运行良好的代码 就不必要改动了。这样可以避免了很多的测试,只要测试新的类,模块,函数即可。

当然理想是美好的,现实是残酷的。
## OCP主要两个特征

对于扩展是开放的 Open for extension

模块的行为是可以扩展的,当应用的需求 发生了变化,我们可以对模块进行扩展,使其满足新的需求。 换句话说我可以改变模块的功能。

对于更改是封闭的 Closed for modification

对于模块扩展的时候,不必改动模块的源码或者二进制代码。 模块的二进制可执行版本,无论是可链接库,DLL或者Java 的jar文件,无需修改。

其实如何保证上面的两个原则呢? 看似有点矛盾的原则,其实不然。 关键点是 我们如果可以知道将来的变化,我们就可以提前抽象这个将来可能发生的变化,而进行抽象,从而隔离变化。 这样当未来需求发生了变化,只需要从抽象层派生子类即可。原来的代码就无需改动,就可以添加新的功能,以满足需求。

举例

有不同的形状,然后 每个形状都有绘制自己的方法,有一个 draw_all_shapes 这个函数 用来绘制所有的形状

# -*- coding: utf-8 -*-
"""
@Time    : 2022/1/22 11:20
@Author  : Frank
@File    : ocp.py
"""
from enum import Enum

from typing import List, Union


class ShapeType(Enum):
    Circle = 0
    Square = 1


class Circle:
    """
    圆类
    """

    def __init__(self, its_type):
        self.its_type = its_type

    def draw(self):
        print("draw circle ...")
        pass


class Square:
    """
    正方形
    """

    def __init__(self, its_type):
        self.its_type = its_type

    def draw(self):
        print("draw square ...")


def draw_square(square):
    print("==draw square begin==")
    square.draw()
    print("==draw square done==")


def draw_cirle(circle):
    print("==draw circle begin==")
    circle.draw()
    print("==draw circle done==")


def draw_all_shapes(shapes: List[Union[Circle, Square]], n: int):
    """

    :param shapes: 形状列表
    :param n:  形状的长度
    :return:
    """
    for i in range(n):
        cur_type = shapes[i].its_type
        if cur_type == ShapeType.Circle:
            draw_cirle(shapes[i])
            pass
        elif cur_type == ShapeType.Square:
            draw_square(shapes[i])
        else:
            print('warning:unknown shape')


def main():
    s1 = Square(ShapeType.Square)
    s2 = Square(ShapeType.Square)
    c1 = Circle(ShapeType.Circle)
    c2 = Circle(ShapeType.Circle)

    shapes = [s1, s2, c1, c2]

    draw_all_shapes(shapes, len(shapes))

if __name__ == '__main__':
    main()

我们来看下 这段代码 有没有什么问题呢?

这是典型面向过程的思考方式。 首先如何 需要添加新的形状, 就要重新修改代码 draw_all_shapes 这个函数里面进行 if语句的判断, 然后还要添加 ShapeType 新的类型。 这样就要改动源码情况下,来扩展程序。 假设有一个 triangle 形状,还要为 三角形 写一个 函数draw_triangle 这明显 是违法了 我们所说的 OCP 原则。

如何修改呢? 如何让程序更容易在添加新的形状的情况下, 可以更好的扩展程序呢? 如果这个改变是我们预期到的, 并且将来可能还有很多中的形状要添加进来, 扇形,五边形,六边形等等 。。 那么我们就要重新进行设计一个更好的方式来解决这个改变的问题。

首先先想一想 这个 改变的方向 是什么?

改变的方向是 将来有不断的新的形状添加进来,都要进行绘制。

改进后的代码

我们最主要的方法就是创建一个抽象层,这个抽象层是什么呢? 就是形状需要添加进来,并且 需要绘制出来 ,那么我可以抽象一个类,定义绘制的方法 即可。

这里抽象一个 Shape 基类, 然后有一个接口 draw 然后通过 派生出不同的子类就可以啦。这样每次 添加新的形状 ,我们就可以创建一个新的子类,而不需要在进行改动之前的代码。

from abc import abstractmethod
from enum import Enum

from typing import List


class ShapeType(Enum):
    Circle = 0
    Square = 1


class Shape:
    """
    形状类

    """

    def __init__(self, its_type):
        self.its_type = its_type

    @abstractmethod
    def draw(self):
        pass

派生类 ,这里就是不同的派生类, Circle, Square


class Circle(Shape):
    """
    圆类
    """
    def __init__(self, its_type):
        super().__init__(its_type)

    def draw(self):
        print("begin draw circle")
        pass


class Square(Shape):
    """
    正方形
    """

    def __init__(self, its_type):
        super().__init__(its_type)

    def draw(self):
        print("begin draw square")
        pass

还有一个 draw_all_shapes 方法

def draw_all_shapes(shapes: List[Shape], n: int):
    for i in range(n):
        shapes[i].draw()

        
        
def main():
    s1 = Square(ShapeType.Square)
    s2 = Square(ShapeType.Square)
    c1 = Circle(ShapeType.Circle)
    c2 = Circle(ShapeType.Circle)

    shapes = [s1, s2, c1, c2]

    draw_all_shapes(shapes, len(shapes))
    

好了,这样就完成了。这样改动过之后,每次如果 有新的形状添加进来,我们只需要继承 一个Shape 基类,实现 draw 方法即可。 在main 函数中,添加形状即可。 这种修改之后功能可以扩展,并且 修改只是添加了新的子类而已 就符合了OCP. 对它的改动是通过增加 新代码 进行的,而不是改现有的代码. 因此它就不会引起程序那样的连锁改动. 所需的改动仅仅是增加新的模块.

这是完美的代码吗?

太好了,非常完美,这样就解决了未来新增不同的形状. 他真的解决了问题了吗?

这个是程序是100% 封闭的吗? 然而我说谎了, 并不是完全封闭的!

假设 现在有这样的一个需求,要求 圆形必需在正方形前面绘制, 要实现这个需求, 我们必须修改draw_all_shapes实现, 使它先绘制 圆形 在绘制 正方形. 如果我们预测到了这种变化,我们就可以设计一个抽象来隔离它. 对于上面的抽象,突然要求 绘制图形的顺序, 这种抽象反而成了一种障碍. 你可能觉得 还有什么定义一个 Shape 类 并派生 Circle, Square 类 更为贴切呢?

这个设计 ,对于一个形状的顺序 比 形状类型 更具有意义的系统来说, 就不是那么贴切了.

这就导致了一个麻烦的结果, 一般而言 无论模块设计的有多么的 封闭,都会存在一些无法对之封闭的变化, 没有对所有变化都贴切的设计. 既然不能应对所有的变化, 我们 就必须做好 对哪些变化封闭做出选择. 或者在进行预测出最有可能发生的变化, 然后 构造抽象来进行隔离这种变化.

那么你可能会问, 我如何提前能知道这种变化呢? 我怎么知道 用户的需求的变化的方向呢?

我的想法就是 把变化交给市场或者说交给用户. 首先先做出0.1beta 版本发布, 看市场的反映,或者用户的反应. 尽早地让用户使用软件,尽可能频繁地 给客户,用户演示软件.用户就可能会提出各种需求, 这种需求 就是你要做设计的隔离变化的点. 其实 这个和 敏捷开发 有点类似. 使用小步快跑的原则, 多向用户寻求反馈. 及时发现软件变化的方向.

有句老话 愚弄我一次,应该感到羞耻的是你, 再次愚弄我,应感到羞愧的是我.

软件设计也是遵守这样的原则. 提前发现,尽早发现用户的变化.就可以提前构建抽象层来隔离变化.

现在第一颗子弹已经击中了我们.

好了,现在用户要求绘制图形 先绘制正方形,在绘制圆形 . 那么我们如何隔离这种变化呢? 防止被第二颗子弹击中呢?

现在我假设已经预测到用户可能对顺序有要求, 那么我们重新来设计一下 Shape 类, 在 Shape 类中增加一个weight 的变量,用来表示 将来被画出来的优先级,数字越大,越优先被画出来.

然后在 Shape 类中添加 魔术方法 __gt____eq__ 这两个魔术方法. 这样就可以自定义了排序了.

之后在shapes 的列表中调用 sort 操作 就可以按照我们的规定 进行排序了. 之后进入 draw_all_shapes函数.

# -*- coding: utf-8 -*-
from abc import abstractmethod
from enum import Enum
from functools import total_ordering
from typing import List


class ShapeType(Enum):
    Circle = 0
    Square = 1


@total_ordering
class Shape:
    """
    形状类

    """

    def __init__(self, its_type, weight=0):
        """

        :param its_type:
        :param weight: int 类型,数字大小代表将来 绘画的顺序, 0,1,2,3.. 数字越大越优先被画出来.
        """
        self.its_type = its_type
        self.weight = weight

    @abstractmethod
    def draw(self):
        pass

    # 实现 自定义比较的魔术方法
    def __gt__(self, other):
        return self.weight < other.weight

    def __eq__(self, other):
        return self.weight == other.weight


class Circle(Shape):
    """
    圆类
    """

    def __init__(self, its_type, weight: int):
        super().__init__(its_type)
        self.weight = weight

    def draw(self):
        print(f"begin draw circle,{self.weight}")
        pass


class Square(Shape):
    """
    正方形
    """

    def __init__(self, its_type, weight: int):
        super().__init__(its_type)
        self.weight = weight

    def draw(self):
        print(f"begin draw square,{self.weight}")
        pass


def draw_all_shapes(shapes: List[Shape], n: int):
    for i in range(n):
        shapes[i].draw()


def main():
    s1 = Square(ShapeType.Square, 0)
    s2 = Square(ShapeType.Square, 0)
    s3 = Square(ShapeType.Square, 0)

    c1 = Circle(ShapeType.Circle, 1)
    c2 = Circle(ShapeType.Circle, 1)

    shapes: list = [s1, s2, c1, c2, s3]
    ordered_shapes = sorted(shapes)
    draw_all_shapes(ordered_shapes, len(shapes))


if __name__ == '__main__':
    main()

结果如下:

mage-20220124131145080

可以看出 现在 已经可以实现 先画圆,再绘制 正方形了. 并且如果后续有新的形状添加进来 我们只需要继承 Shape 然后指定一个 weight 属性,就可以很好的实现扩展, 而不对原来的代码有修改,只是添加了一个类. 这样的就符合OCP.

OCP核心思想

​ OCP核心思想 就是 希望可以如果将来 软件发生变化, 或者有新的需求,我们希望 扩展原来的模块, 而不是 修改源码,来进行扩展代码的功能. 对于扩展是开放的, 对于更改是封闭的 .

总结

​ 在很多的方面 , OCP都是面向对象设计的核心所在, 遵守这个原则可以带来面向对象技术所声称的极大的好处. (灵活性,可重用性,以及可维护性) 然而并不是说 只要进行面向对象 语言 就要遵守这个原则, 对于应用程序中每个部分都进行 肆意的 抽象 同样是一个不好的主意. 当变化发生时,才进行抽象,防止被第二颗子弹击中,这个时候才抽象 才是一个好主意. 或者说 只对频繁发生变化的哪些部分做出抽象处理. 我们要拒绝不成熟的抽象,或者过早的抽象,防止代码变成了过度设计的臭味.

参考文档

敏捷软件开发:原则,模式,实践

小话设计模式原则之:开闭原则OCP

分享快乐,留住感动. '2022-01-24 18:49:53' --frank
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值