[设计原则] 六大设计原则之“开闭原则”-程序员宅基地

技术标签: Java  代码设计  设计原则  开闭原则  OCP  

[设计原则] 六大设计原则之“开闭原则”

手机用户请横屏获取最佳阅读体验,REFERENCES中是本文参考的链接,如需要链接和更多资源,可以关注其他博客发布地址。

平台 地址
CSDN https://blog.csdn.net/sinat_28690417
简书 https://www.jianshu.com/u/3032cc862300
个人博客 https://yiyuery.github.io/NoteBooks/

定义

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

Software entities like classes,modules and functions should be open for extension but closed for modifications.

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

软件实体应该是可以扩展的,但不能因修改而改变它在抽象层次上的确定性。

什么是开闭原则

开闭原则明确的告诉我们:软件实现应该对扩展开放,对修改关闭,其含义是说一个软件实体应该通过扩展来实现变化,而不是通过修改已有的代码来实现变化的。那什么是软件实体呢?软件实体包括以下几个部分:

  • 项目或软件产品中按照一定的逻辑规则划分的模块
  • 抽象和类
  • 方法

一个软件产品只要在生命周期内,都会发生变化,即然变化是一个事实,我们就应该在设计时尽量适应这些变化,以提高项目的稳定性和灵活性,真正实现“拥抱变化”。开闭原则告诉我们应尽量通过扩展软件实体的行为来实现变化,而不是通过修改现有代码来完成变化,它是为软件实体的未来事件而制定的对现行开发设计进行约束的一个原则。

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

在这里插入图片描述
书籍接口

/*
 * @ProjectName: 编程学习
 * @Copyright:   2018 HangZhou xiazhaoyang Dev, Ltd. All Right Reserved.
 * @address:     http://xiazhaoyang.tech
 * @date:        2018/11/28 22:34
 * @email:       [email protected]
 * @description: 本内容仅限于编程技术学习使用,转发请注明出处.
 */
package com.example.chapter3.design.ocp;

/**
 * <p>
 *
 * </p>
 *
 * @author xiazhaoyang
 * @version V1.0
 * @date 2018/11/28 22:34
 * @modificationHistory=========================逻辑或功能性重大变更记录
 * @modify By: {修改人} 2018/11/28
 * @modify reason: {方法名}:{原因}
 * ...
 */
public interface IBook {
    

    /**
     * 出售
     * @return 收益
     */
    double doSell();
}

小说类书籍

/*
 * @ProjectName: 编程学习
 * @Copyright:   2018 HangZhou xiazhaoyang Dev, Ltd. All Right Reserved.
 * @address:     http://xiazhaoyang.tech
 * @date:        2018/11/28 22:42
 * @email:       [email protected]
 * @description: 本内容仅限于编程技术学习使用,转发请注明出处.
 */
package com.example.chapter3.design.ocp;

import lombok.AllArgsConstructor;
import lombok.Data;

/**
 * <p>
 *
 * </p>
 *
 * @author xiazhaoyang
 * @version V1.0
 * @date 2018/11/28 22:42
 * @modificationHistory=========================逻辑或功能性重大变更记录
 * @modify By: {修改人} 2018/11/28
 * @modify reason: {方法名}:{原因}
 * ...
 */
@Data
@AllArgsConstructor
public class NovelBook implements IBook {
    

    private double price;

    private String name;

    private String author;

    @Override
    public double doSell() {
    
        return getPrice();
    }

    public static void main(String[] args) {
    
        System.out.println(new NovelBook(10.5,"天龙八部","金庸").doSell());
        //10.5
    }
}

项目投产生,书籍正常销售,但是我们经常因为各种原因,要打折来销售书籍,这是一个变化,我们要如何应对这样一个需求变化呢?

我们有下面三种方法可以解决此问题:

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

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

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

下面是修改后的类图:

在这里插入图片描述

打折类

/*
 * @ProjectName: 编程学习
 * @Copyright:   2018 HangZhou xiazhaoyang Dev, Ltd. All Right Reserved.
 * @address:     http://xiazhaoyang.tech
 * @date:        2018/11/28 22:44
 * @email:       [email protected]
 * @description: 本内容仅限于编程技术学习使用,转发请注明出处.
 */
package com.example.chapter3.design.ocp;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;

/**
 * <p>
 *
 * </p>
 *
 * @author xiazhaoyang
 * @version V1.0
 * @date 2018/11/28 22:44
 * @modificationHistory=========================逻辑或功能性重大变更记录
 * @modify By: {修改人} 2018/11/28
 * @modify reason: {方法名}:{原因}
 * ...
 */
@EqualsAndHashCode(callSuper = true)
@Data
public class DiscountNovelBook extends NovelBook {
    

    private double discount;

    public DiscountNovelBook(double discount) {
    
        super(12.5,"天龙八部","金庸");
        this.discount = discount;
    }

    @Override
    public double doSell() {
    
        return super.doSell() * discount;
    }

    public static void main(String[] args) {
    
        DiscountNovelBook discountNovelBook = new DiscountNovelBook(0.5);
        System.out.println(discountNovelBook.doSell());//6.25
    }
}

现在打折销售开发完成了,我们只是增加了一个DiscountNovelBook类,我们修改的代码都是高层次的模块,没有修改底层模块,代码改变量少,可以有效的防止风险的扩散。
我们可以把变化归纳为二种类型:

  • 逻辑变化
    只变化了一个逻辑,而不涉及其他模块,比如一个算法是abc,现在需要修改为a+b+c,可以直接通过修改原有类中的方法的方式来完成,前提条件是所有依赖或关联类都按照相同的逻辑处理

  • 子模块变化
    一个模块变化,会对其它的模块产生影响,特别是一个低层次的模块变化必然引起高层模块的变化,因此在通过扩展完成变化。

为什么使用开闭原则

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

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

  • 第三:开闭原则可以提高复用性

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

    • 保持软件产品的稳定性,开闭原则要求我们通过保持原有代码不变添加新代码来实现软件的变化,因为不涉及原代码的改动,这样可以避免为实现新功能而改坏线上功能的情况,避免老用户的流失。

  • 第四:开闭原则可以提高维护性

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

    • 使代码更具模块化,易于维护。开闭原则可以让代码中的各功能,以及新旧功能独立存在于不同的单元模块中,一旦某个功能出现问题,可以很快地锁定代码位置作出修改,由于模块间代码独立不相互调用,更改一个功能的代码也不会引起其他功能的崩溃。

    • 不影响原有测试代码的运行。软件开发规范性好的团队都会写单元测试,如果某条单元测试所测试的功能单元发生了变化,则单元测试代码也应做相应的断言变更,否则就会导致单元测试运行红条。如果每次软件的变化,除了变更功能代码之外,还得变更测试代码,书写测试代码同样需要消耗工时,这样在项目中引入单元测试就成了累赘。开闭原则可以让单元测试充分发挥作用而又不会成为后期软件开发的累赘。

  • 第五:面向对象开发的要求

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

    • 提高开发效率。在代码开发中,有时候阅读前人的代码是件很头疼的事,尤其项目开发周期比较长,可能三五年,再加上公司人员流动性大,原有代码的开发人员早就另谋高就,而代码写的更是一团糟,自带混淆,能走弯路不走直路。而现在需要在原有功能的基础上开发新功能,如果开闭原则使用得当的话,我们是不需要看懂原有代码实现细节便可以添加新代码实现新功能(例如示例中,我们不需要知道A产品是怎么生产的,便可以开发生产B产品的功能),毕竟有时候阅读一个功能的代码,比自己重新实现这个功能用的时间还要长。

如何使用开闭原则

  • 第一:抽象约束

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

  • 通过接口或抽象类约束扩散,对扩展进行边界限定,不允许出现在接口或抽象类中不存在的public方法。

  • 参数类型,引用对象尽量使用接口或抽象类,而不是实现类,这主要是实现里氏替换原则的一个要求

  • 抽象层尽量保持稳定,一旦确定就不要修改

  • 第二:元数据(metadata)控件模块行为

编程是一个很苦很累的活,那怎么才能减轻压力呢?答案是尽量使用元数据来控制程序的行为,减少重复开发。什么是元数据?用来描述环境和数据的数据,通俗的说就是配置参数,参数可以从文件中获得,也可以从数据库中获得。

  • 第三:制定项目章程

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

  • 第四:封装变化

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

注意事项

  • 开闭原则要求我们要尽可能的通过保持原有代码不变添加新代码而不是通过修改已有的代码来实现软件产品的变化。

问题由来

  • 在软件的生命周期内,因为变化、升级和维护等原因需要对软件原有代码进行修改时,可能会给旧代码中引入错误,也可能会使我们不得不对整个功能进行重构,并且需要原有代码经过重新测试。

解决方案

当软件需要变化时,尽量通过扩展软件实体的行为来实现变化,而不是通过修改已有的代码来实现变化。

开闭原则是面向对象设计中最基础的设计原则,它指导我们如何建立稳定灵活的系统。开闭原则可能是设计模式六项原则中定义最模糊的一个了,它只告诉我们对扩展开放,对修改关闭,可是到底如何才能做到对扩展开放,对修改关闭,并没有明确的告诉我们。以前,如果有人告诉我"你进行设计的时候一定要遵守开闭原则",我会觉的他什么都没说,但貌似又什么都说了。因为开闭原则真的太虚了。

其实开闭原则无非就是想表达这样一层意思:用抽象构建框架,用实现扩展细节。因为抽象灵活性好,适应性广,只要抽象的合理,可以基本保持软件架构的稳定。而软件中易变的细节,我们用从抽象派生的实现类来进行扩展,当软件需要发生变化时,我们只需要根据需求重新派生一个实现类来扩展就可以了。当然前提是我们的抽象要合理,要对需求的变更有前瞻性和预见性才行。

总结

至此开闭原则的示例就基本完了。细心的朋友可能觉察到了,以面向抽象编程的方式去达到对扩展开放的目的,这和我们的依赖倒置原则(高层模块不应该依赖低层模块,两者都应该依赖其抽象)怎么这么像。可能有朋友就要不满了,不就是一个面向抽象编程,玩这么多花样干嘛,将简单的问题复杂化。博主的理解是这些设计原则没有绝对的界限,它们只是从不同的侧重点去约束我们的软件架构,使其能使用各种不同的需求。

说到这里,再简要介绍下其他5项原则,它们主要是告诉我们用抽象构建框架,用实现扩展细节的注意事项而已:单一职责原则告诉我们实现类要职责单一;里氏替换原则告诉我们不要破坏继承体系;依赖倒置原则告诉我们要面向接口编程;接口隔离原则告诉我们在设计接口的时候要精简单一;迪米特法则告诉我们要降低耦合。而开闭原则是总纲,他告诉我们要对扩展开放,对修改关闭。

最后说明一下如何去遵守这六个原则。对这六个原则的遵守并不是是和否的问题,而是多和少的问题,也就是说,我们一般不会说有没有遵守,而是说遵守程度的多少。任何事都是过犹不及,设计模式的六个设计原则也是一样,制定这六个原则的目的并不是要我们刻板的遵守他们,而需要根据实际情况灵活运用。对他们的遵守程度只要在一个合理的范围内,就算是良好的设计。

REFRENCES

  1. 六大设计原则之开闭原则
  2. 小话设计模式原则之:开闭原则OCP
  3. 设计模式心法总则:开闭原则
  4. 极客学院 开闭原则

更多

扫码关注架构探险之道,回复文章标题,获取本文相关源码和资源链接

在这里插入图片描述

知识星球(扫码加入获取历史源码和文章资源链接)

在这里插入图片描述

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/sinat_28690417/article/details/84711426

智能推荐

从零开始搭建Hadoop_创建一个hadoop项目-程序员宅基地

文章浏览阅读331次。第一部分:准备工作1 安装虚拟机2 安装centos73 安装JDK以上三步是准备工作,至此已经完成一台已安装JDK的主机第二部分:准备3台虚拟机以下所有工作最好都在root权限下操作1 克隆上面已经有一台虚拟机了,现在对master进行克隆,克隆出另外2台子机;1.1 进行克隆21.2 下一步1.3 下一步1.4 下一步1.5 根据子机需要,命名和安装路径1.6 ..._创建一个hadoop项目

心脏滴血漏洞HeartBleed CVE-2014-0160深入代码层面的分析_heartbleed代码分析-程序员宅基地

文章浏览阅读1.7k次。心脏滴血漏洞HeartBleed CVE-2014-0160 是由heartbeat功能引入的,本文从深入码层面的分析该漏洞产生的原因_heartbleed代码分析

java读取ofd文档内容_ofd电子文档内容分析工具(分析文档、签章和证书)-程序员宅基地

文章浏览阅读1.4k次。前言ofd是国家文档标准,其对标的文档格式是pdf。ofd文档是容器格式文件,ofd其实就是压缩包。将ofd文件后缀改为.zip,解压后可看到文件包含的内容。ofd文件分析工具下载:点我下载。ofd文件解压后,可以看到如下内容: 对于xml文件,可以用文本工具查看。但是对于印章文件(Seal.esl)、签名文件(SignedValue.dat)就无法查看其内容了。本人开发一款ofd内容查看器,..._signedvalue.dat

基于FPGA的数据采集系统(一)_基于fpga的信息采集-程序员宅基地

文章浏览阅读1.8w次,点赞29次,收藏313次。整体系统设计本设计主要是对ADC和DAC的使用,主要实现功能流程为:首先通过串口向FPGA发送控制信号,控制DAC芯片tlv5618进行DA装换,转换的数据存在ROM中,转换开始时读取ROM中数据进行读取转换。其次用按键控制adc128s052进行模数转换100次,模数转换数据存储到FIFO中,再从FIFO中读取数据通过串口输出显示在pc上。其整体系统框图如下:图1:FPGA数据采集系统框图从图中可以看出,该系统主要包括9个模块:串口接收模块、按键消抖模块、按键控制模块、ROM模块、D.._基于fpga的信息采集

微服务 spring cloud zuul com.netflix.zuul.exception.ZuulException GENERAL-程序员宅基地

文章浏览阅读2.5w次。1.背景错误信息:-- [http-nio-9904-exec-5] o.s.c.n.z.filters.post.SendErrorFilter : Error during filteringcom.netflix.zuul.exception.ZuulException: Forwarding error at org.springframework.cloud..._com.netflix.zuul.exception.zuulexception

邻接矩阵-建立图-程序员宅基地

文章浏览阅读358次。1.介绍图的相关概念  图是由顶点的有穷非空集和一个描述顶点之间关系-边(或者弧)的集合组成。通常,图中的数据元素被称为顶点,顶点间的关系用边表示,图通常用字母G表示,图的顶点通常用字母V表示,所以图可以定义为:  G=(V,E)其中,V(G)是图中顶点的有穷非空集合,E(G)是V(G)中顶点的边的有穷集合1.1 无向图:图中任意两个顶点构成的边是没有方向的1.2 有向图:图中..._给定一个邻接矩阵未必能够造出一个图

随便推点

MDT2012部署系列之11 WDS安装与配置-程序员宅基地

文章浏览阅读321次。(十二)、WDS服务器安装通过前面的测试我们会发现,每次安装的时候需要加域光盘映像,这是一个比较麻烦的事情,试想一个上万个的公司,你天天带着一个光盘与光驱去给别人装系统,这将是一个多么痛苦的事情啊,有什么方法可以解决这个问题了?答案是肯定的,下面我们就来简单说一下。WDS服务器,它是Windows自带的一个免费的基于系统本身角色的一个功能,它主要提供一种简单、安全的通过网络快速、远程将Window..._doc server2012上通过wds+mdt无人值守部署win11系统.doc

python--xlrd/xlwt/xlutils_xlutils模块可以读xlsx吗-程序员宅基地

文章浏览阅读219次。python–xlrd/xlwt/xlutilsxlrd只能读取,不能改,支持 xlsx和xls 格式xlwt只能改,不能读xlwt只能保存为.xls格式xlutils能将xlrd.Book转为xlwt.Workbook,从而得以在现有xls的基础上修改数据,并创建一个新的xls,实现修改xlrd打开文件import xlrdexcel=xlrd.open_workbook('E:/test.xlsx') 返回值为xlrd.book.Book对象,不能修改获取sheett_xlutils模块可以读xlsx吗

关于新版本selenium定位元素报错:‘WebDriver‘ object has no attribute ‘find_element_by_id‘等问题_unresolved attribute reference 'find_element_by_id-程序员宅基地

文章浏览阅读8.2w次,点赞267次,收藏656次。运行Selenium出现'WebDriver' object has no attribute 'find_element_by_id'或AttributeError: 'WebDriver' object has no attribute 'find_element_by_xpath'等定位元素代码错误,是因为selenium更新到了新的版本,以前的一些语法经过改动。..............._unresolved attribute reference 'find_element_by_id' for class 'webdriver

DOM对象转换成jQuery对象转换与子页面获取父页面DOM对象-程序员宅基地

文章浏览阅读198次。一:模态窗口//父页面JSwindow.showModalDialog(ifrmehref, window, 'dialogWidth:550px;dialogHeight:150px;help:no;resizable:no;status:no');//子页面获取父页面DOM对象//window.showModalDialog的DOM对象var v=parentWin..._jquery获取父window下的dom对象

什么是算法?-程序员宅基地

文章浏览阅读1.7w次,点赞15次,收藏129次。算法(algorithm)是解决一系列问题的清晰指令,也就是,能对一定规范的输入,在有限的时间内获得所要求的输出。 简单来说,算法就是解决一个问题的具体方法和步骤。算法是程序的灵 魂。二、算法的特征1.可行性 算法中执行的任何计算步骤都可以分解为基本可执行的操作步,即每个计算步都可以在有限时间里完成(也称之为有效性) 算法的每一步都要有确切的意义,不能有二义性。例如“增加x的值”,并没有说增加多少,计算机就无法执行明确的运算。 _算法

【网络安全】网络安全的标准和规范_网络安全标准规范-程序员宅基地

文章浏览阅读1.5k次,点赞18次,收藏26次。网络安全的标准和规范是网络安全领域的重要组成部分。它们为网络安全提供了技术依据,规定了网络安全的技术要求和操作方式,帮助我们构建安全的网络环境。下面,我们将详细介绍一些主要的网络安全标准和规范,以及它们在实际操作中的应用。_网络安全标准规范