05.开闭原则(Open Closed Principle)

2023-12-13 06:02:19

“你这个人怎么这么轴?让你改改以前的代码怎么和要了你命似的?难道你的能力仅限于此吗?”
“你懂什么?我有我的原则!我有我的信仰!”

一言

开闭原则即:对扩展开放,对修改关闭,它是所有设计的核心。


概述

大家潜意识里普遍遵循的原则,往往是最关键、最重要的原则。法律或许偶有狂徒触碰,但雷池从不放走一个活人。

科幻小说《三体》中有这样一个桥段,面壁者比尔·希恩斯为了“坚定”地球人抵抗三体人侵略的决心,发明了可以改变人类潜意识的机器,并取名思想钢印。甚至当在机器中输入“水是有毒的”这样的理论也可以灌注到人类的基本意志中,比尔希尔斯本人也险些因此拒绝喝水,脱水而亡。

小说的描述颇具浪漫主义气质,事实上,开闭原则就是软件设计中难以推翻的“思想钢印”。
如果单说开闭原则,或许有些同学会一时发懵不知道是在说什么。但如果换种通俗的说法:之前的需求要你实现了30个工具方法,随着项目的丰满,这30个工具方法在各个实现中调用了差不多100次。现在有个狼人过来悄咪咪告诉你,“兄弟,这30个工具方法你把实现细节全改一改,我有大用!” 你会有什么反应?
相信大多数同学都能心领神会的口径一致:“GUN!
“呀?还会说英文呢?”
“拼音或者英文都可以表达我的态度,具体看你理解!”
狼人不理解了,为什么这么大反应?其实广大程序员这种刻在骨子里的反应就是开闭原则的思想钢印,大多数人潜意识里都在恪守。


何为开闭原则

一个软件实体,模块和函数应该对扩展开放,对修改关闭。用抽象构建框架,用实现扩展细节。当软件需要变化时,尽量通过扩展软件实体的行为来实现变化,而不是通过修改已有的代码来实现优化。

  • 对扩展开放: 对提供功能的一方扩展开放,可以添加功能或类;
  • 对修改关闭: 对使用方而言,修改是关闭的(比如新增了一个类,不会修改之前使用方的代码/功能);

三寸反骨

碰了璧的狼人黑夜化身反骨仔,挑灯加班决定突破开闭原则的思想钢印,在不遵守开闭原则的前提下,写了一段代码。

需求很简单,实现一个画图形的工具类。

反例代码

public class Graph {
    class Editor{
        public void draw(Shape shape){
            if (shape.type==1)
                drawCircle(shape);
            else if (shape.type==2)
                drawBlock(shape);
        }
        public void drawCircle(Shape shape){
            System.out.println("绘制圆形具体实现");
        }
        public void drawBlock(Shape shape){
            System.out.println("绘制方形具体实现");
        }
    }
    class Shape{
        int type;
    }
    class Circle extends Shape{
        Circle(){
            super.type =1;
        }
    }
    class Block extends Shape{
        Block(){
            super.type = 2;
        }
    }
}

反骨仔开始志得意满,这也太简单了,丝毫没有压力啊。于是此方法投入使用了半年,借助这个工具类,同事们搭建了一个又一个“完美且稳定”的系统,牵扯调用三百多次。
然后,好消息来了,绘图工具需求增加,要求在原有基础上支持对三角形、平行四边形、梯形、五角星、六芒星、小波浪、大波浪等等一百二十多个图形的绘制。
反骨仔凌乱了,因为他的绘制方法实现要做大量修改姑且不论,同事们调用这个方法高达三百余处,现在底层方法修改了,上下文逻辑是不是都需要再推敲,回归测试的工作量有多少。这一些列操作下来的成本又由谁来承担?
所以说,雷池就是雷池,轻易不要头铁,开闭原则能被广大同学在潜意识中遵守一定有他的道理。在软件开发的过程中,也许我不比所有人聪明,但是我一定要是最稳的那个。


优化设计

打开时光机,反骨仔脱胎换骨回到了半年前那个倔强的夜晚,默默捧起了《程序员的自我修养》,开始遵循OCP原则,对那个“简单的需求”进行设计。
优化代码

public class Ocp {
    public static void main(String[] args) {
        //使用,看看存在的问题
        GraphEditor graphEditor = new GraphEditor();
        graphEditor.drawShape(new Block());
        graphEditor.drawShape(new Circle());
        graphEditor.drawShape(new Star());
    }
}

//绘图类
class GraphEditor{
    //接收Shape对象,根据type绘制不同图形
    public void drawShape(Shape s){
        s.draw();
    }
}

//图形基类
abstract class Shape{
    public abstract void draw();//抽象方法
}

//矩形类
class Block extends  Shape{
   @Override
    public void draw() {
        System.out.println("绘制方块");
    }
}

//圆形类
class Circle extends  Shape{
    @Override
    public void draw() {
        System.out.println("绘制圆形");
    }
}

//圆形类
class Star extends  Shape{
    @Override
    public void draw() {
        System.out.println("绘制五角星");
    }
}

把创建Shape类做成抽象类,并提供一个抽象的draw方法,让子类实现即可,这样我们有新的图形种类时,只需要新的图形类继承Shape,并实现draw方法即可,使用的代码就不需要修改,这就满足了OCP原则。
果然,命运的齿轮在半年后再一次转到了曾经的至暗时刻,海量的图形扩展需求蜂拥而至,而此时的那个少年成竹在胸,微微一笑,说到:
“大家放心,已经在用的方法不会有任何变更。扩展的问题交给我,相关模块的回归测试理论上可以不用做,当然,具体的施行还是看各部门的落地要求。”


开闭原则是所有设计的核心,更是所有优雅扩展的集中表现形式。从广义上讲,它其实是在寻求一种变中求不变的平衡。开闭原则并不是拒绝需求的扩张,相反,它恰恰是为了以最小代价满足需求扩张而形成的捷径。从另一个角度看,它恰恰印证了软件开发生命周期中,设计阶段的重要性。
在上文的论述中,为了更好的表述OCP原则,我们一直尝试着从正向解决问题。其实我们忽略了一个角度,“反骨仔”遇到的难题一方面是由于他没有遵循OCP原则,那么其他同事有没有问题呢?一个工具类牵扯到几百个模块的调用是否真的合理呢?
这里其实就又牵出了耦合度的问题,当然这是另外一个话题了。
笔者认为,软件设计的本质之一在于对耦合关系的优化处理,通常我们讲“高内聚、低耦合”的系统架构是值得学习和借鉴的(比如springframework中的IOC理论就极大的降低了体系内的耦合度)。
那么在软件设计中,是否有一个原则描述了耦合关系呢?
有的,那就是是 迪米特法则 , 下篇博客我们再展开聊聊耦合处理的二三事。


关注我,共同进步,每周至少一更。——Wayne

文章来源:https://blog.csdn.net/WayneSlytherin/article/details/134825175
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。