1.什么是开闭原则

开闭原则的英文是Open Closed Principle,缩写就是OCP。其定义如下:

软件实体(模块、类、方法等)应该“对扩展开放、对修改关闭”。

从定义上看,这个原则主要包含两部分:

  • 对扩展开放:“ 这意味着模块的行为是可以扩展的。当应用程序的需求改变时,我们可以对其模块进行扩展,使其具有满足那些需求变更的新行为。换句话说,我们可以改变模块的功能。

  • 对修改关闭:“ 对模块行为进行扩展时,不必改动该模块的源代码或二进制代码。模块的二进制可执行版本,无论是可链接的库、DLL或Java的.jar文件,都无需改动。

通俗解释就是,添加一个新的功能,应该通过在已有代码(模块、类、方法)的基础上进行扩展来实现,而不是修改已有代码。

之前的一篇文章《何谓高质量代码?》中,我们总结了高质量代码的几个衡量标准。

而开闭原则解决的就是代码的扩展性问题。如果某段代码在应对未来需求变化的时候,能够做到“对扩展开放、对修改关闭”,那就说明这段代码的扩展性比较好。

2.如何做到对扩展开放、对修改关闭

那么应该怎样写出扩展性好的代码呢?

在思想上我们要具备扩展意识、抽象意识、封装意识。这些意识的培养要比一些具体的方法更为重要,这依赖我们对面向对象的理解、对业务的掌握度,以及长期的经验积累...... 这要求我们在写代码的时候后,要多花点时间往前多思考一下,未来可能有哪些需求变更,识别出代码的易变部分与不易变部分,合理设计代码结构,事先留好扩展点,以便在未来不需要改动代码整体结构、做到最小代码改动的情况下,新的代码能够很灵活地插入到扩展点上。

在方法上,我们主要可以通过多态、依赖注入、面向接口编程等方式来实现代码的可扩展性。做到“对扩展开放、对修改关闭”。我们要将可变部分抽象出来以隔离变化,提供抽象化的不可变接口,给上层系统使用。当具体的实现发生变化的时候,我们只需要基于相同的抽象接口,扩展一个新的实现,替换掉老的实现即可,上游系统的代码几乎不需要修改。

比如,我们的项目中通常会用到一些第三方组件,消息中间件,缓存中间件......消息中间件我们可能一开始使用RabbitMQ,但是可能后来会换成Kafka,缓存中间件可能会从Memcache换成Redis。这种情况,如果我们的上层应用直接依赖这些中间件调用代码,那么更换的成本就会更高,这种代码就不利于扩展。

  1. public class MemcacheClient {
  2. public boolean set(String key, String value) {
  3. return false;
  4. }
  5. public String get(String key) {
  6. return null;
  7. }
  8. public boolean remove(String key) {
  9. return false;
  10. }
  11. }
  12. public class OcpApplication {
  13. public void test() {
  14. // 业务代码
  15. //...
  16. //...
  17. //写缓存
  18. MemcacheClient client = new MemcacheClient();
  19. client.set("testKey", "testValue");
  20. }
  21. }

如上示例,我们的上层应用OcpApplication直接依赖了MemcacheClient,如果未来有需要把Memcache换成Redis,我们就需要替换掉所有调用了MemcacheClient的上层应用方法,这严重违背了开闭原则。

在这种情况下,通常我们会把这种中间件的调用设计成可插拔的。我们提供一个这些中间件的抽象接口出来,让所有上层系统都依赖这组抽象的接口编程,并且通过依赖注入的方式来调用。当我们要替换新的中间件的时候,比如将 Memcache替换成 Redis,就可以可以很方便地拔掉老的Memecache实现,插入新的Redis实现。

  1. /**
  2. * 缓存中间件的使用抽象出接口
  3. */
  4. public interface ICacheClient {
  5. boolean set(String key, String value);
  6. String get(String key);
  7. boolean remove(String key);
  8. }
  9. /**
  10. * MemcacheClient
  11. */
  12. public class MemcacheClient implements ICacheClient {
  13. public boolean set(String key, String value) {
  14. return false;
  15. }
  16. public String get(String key) {
  17. return null;
  18. }
  19. public boolean remove(String key) {
  20. return false;
  21. }
  22. }
  23. /**
  24. * RedisClient
  25. */
  26. public class RedisClient implements ICacheClient {
  27. @Override
  28. public boolean set(String key, String value) {
  29. return false;
  30. }
  31. @Override
  32. public String get(String key) {
  33. return null;
  34. }
  35. @Override
  36. public boolean remove(String key) {
  37. return false;
  38. }
  39. }
  40. public class OcpApplication {
  41. /**
  42. * 依赖注入cacheClient
  43. */
  44. ICacheClient cacheClient;
  45. public OcpApplication(ICacheClient cacheClient){
  46. this.cacheClient=cacheClient;
  47. }
  48. public void test() {
  49. // 业务代码
  50. //...
  51. //...
  52. //写缓存
  53. cacheClient.set("testKey", "testValue");
  54. }
  55. }

3.如何灵活运用开闭原则

开闭原则看似简单,但我却认为是SOLID 中最难掌握的一条原则。其难点就在于如何在真正的项目中去灵活运动开闭原则。而且OCP同样存在着一些陷阱,怎么才算满足或违反开闭原则,修改代码就一定意味着违反开闭原则吗,扩展点设计的越多越好吗......

3.1 灵活设计扩展点

对于业务系统,要想识别出尽可能多的扩展点,就要求你对业务有足够的了解,能够预见一些未来可能的变化。

对于偏技术的系统,比如,框架、组件、类库等,就需要充分了解它的使用场景?以及今后想要扩展点功能?使用者未来会有哪些更多的诉求......

但即便我们对业务、对系统有足够的了解,也不可能识别出所有的扩展点,即便可以,并为这些地方都预留扩展点,也是没有必要的。同样有一条原则叫KISS原则,那就是尽量保持简单,不要进行过度设计,实际上很多人都会陷入这样一个误区,我们常常为了一些很可能不存在的扩展而绞尽脑汁!

最合理的做法就是,对于一些比较确定的、短期内可能就会扩展,或者需求改动对代码结构影响比较大的情况,或者实现成本不高的扩展点,可以事先做些扩展性设计。但对于一些不确定未来是否要支持的需求,或者实现起来比较复杂的扩展点,我们可以等到有需求驱动的时候,再通过重构代码的方式来支持扩展的需求。

3.2 修改代码一定违反开闭原则吗

开闭原则中对于修改是封闭的并非是一个绝对的概念。

1.修复缺陷所做的改动

缺陷在软件中很常见,是不可能完全消除的。当缺陷出现时,就需要我们修复现有的代码。软件修复明显倾向于实用主义而不是坚持开放封闭原则。

2.客户端无法感知到的改动

如果一个类的改动会引起另一个类的改动,那么这两个类就是紧密耦合的。相反,如果一个类的修改总是独立的,并不会引起其他类的改动,那么这些类就是松散耦合的。我们要记住,任何情况下,松散耦合都比紧密耦合要好。如果我们对现有代码的修改不会影响客户端代码,那么也就谈不上违背开放封闭原则。

3.修改还是扩展?

从开闭原则定义中,我们可以看出,开闭原则可以应用在不同粒度的代码中,可以是模块,也可以类,还可以是方法(及其属性)。同样一个代码改动,在粗代码粒度下,可以被认定为“修改”,但在细代码粒度下,又可以被认定为“扩展”。

比如,在类这个层面添加属性和方法相当于修改类,这个代码改动可以被认定为“修改”;但这个改动并没有修改已有的属性和方法,在方法(及其属性)这一层面,它又可以被认定为“扩展”。

实际上,当纠结于某个代码改动是“修改”还是“扩展”的时候,我们就已经背离了设计原则的初衷,开闭原则的本质目的就是为了让我们的代码更具有扩展性,更容易维护,如果我们可以很容易的完成修改,又不会影响到既有的代码与单测,就可以认为这是一个合理的改动。

3.3 扩展性与可读性的平衡

在有些情况下,代码的扩展性会跟可读性相冲突。为了更好地支持扩展性,我们对代码进行了重构,重构之后的代码要比之前的代码复杂很多,理解起来也更加有难度。实际上很多时候,我们都要结合具体的场景在扩展性和可读性之间做权衡。在某些场景下,扩展性很重要,我们就可以适当地牺牲一些可读性;而在另一些场景下,可读性更加重要,那我们就适当地牺牲一些扩展性。

小结

绝大多数情况下,我们的系统都不是一锤子买卖,通常随着需求的迭代,我们需要不断地对其进行维护与扩展。而开闭原则的思想可以很好的解决扩展性的问题,因此理解并掌握开闭原则至关重要,但这需要我们充分的理解面向对象的思想,合理的利用封装、多态等方法以及长期大量的积累!

系列文章

设计原则:单一职责(SRP)

设计原则:开闭原则(OCP)

设计原则:里式替换原则(LSP)

设计原则:接口隔离原则(ISP)

设计原则:依赖倒置原则(DIP)

何谓高质量代码?

理解RESTful API

关注下方公众号,回复“代码的艺术”,可免费获取重构、设计模式、代码整洁之道等提升代码质量等相关学习资料

设计原则:开闭原则(OCP)的更多相关文章

  1. 设计模式之六大原则——开闭原则(OCP)

    转载于: http://www.cnblogs.com/muzongyan/archive/2010/08/05/1793454.html 开闭原则(Open Closed Principle)是Ja ...

  2. [转]设计模式之六大原则——开闭原则(OCP)

    原文地址:http://www.cnblogs.com/muzongyan/archive/2010/08/05/1793454.html 开闭原则(Open Closed Principle)是Ja ...

  3. 设计模式值六大原则——开闭原则(OCP)

    开闭原则(Open Closed Principle)是Java世界里最基础的设计原则,它指导我们如何建立一个稳定的.灵活的系统. 定义: 一个软件实体如类.模块和函数应该对扩展开放,对修改关闭. S ...

  4. java设计原则---开闭原则

    开闭原则:指的是一个软件实体应对对扩展开发,对修改关闭(Software entities should be open for extension, but closed for modificat ...

  5. Java设计原则—开闭原则(转)

    原文出自:http://www.cnblogs.com/muzongyan/archive/2010/08/05/1793454.html 开闭原则(Open Closed Principle)是Ja ...

  6. 设计模式 第一天 UML图,设计模式原则:开闭原则、依赖倒转原则、接口隔离原则、合成复用原则、迪米特法则,简单工厂模式

    1 课程大纲 2 UML的概述 总结: UML unified model language 统一建模语言 一共有十种图: 类图 用例图 时序图 * 对象图 包图 组件图 部署图 协作图 状态图 (最 ...

  7. 第2章 面向对象的设计原则(SOLID):6_开闭原则

    6. 开闭原则(Open Closed Principle,OCP) 6.1 定义 (1)一个类应该对扩展开放,对修改关闭.要求通过扩展来实现变化,而且是在不修改己有的代码情况下进行扩展,也不必改动己 ...

  8. 聊一聊开闭原则(OCP).

    目录 简述 最早提出(梅耶开闭原则) 重新定义(多态开闭原则) 深入探讨 OCP的两个特点 对外扩展开放(Open for extension) 对内修改关闭 抽象 关闭修改.对外扩展? 简述 在面向 ...

  9. 设计模式原则(6)--Open-Closed Principle(OCP)--开闭原则

    作者QQ:1095737364    QQ群:123300273     欢迎加入! 1.定义: 一个软件实体应当对扩展开放,对修改关闭.即软件实体应尽量在不修改原有代码的情况下进行扩展. 2.使用场 ...

随机推荐

  1. taro ENV & NODE_ENV & process.env

    taro ENV & NODE_ENV & process.env https://github.com/NervJS/taro-ui/blob/dev/src/common/util ...

  2. PWA & TWA

    PWA & TWA https://www.bilibili.com/video/av68082979/ Service Worker workbox.js https://developer ...

  3. NGK Global伦敦路演:“区块链+能源”必将推动世界性能源革命

    随着区块链技术的发展和应用的不断完善深入,市场的热情也开始活跃高涨,在万众期待下,NGK Global在英国伦敦的路演于7月25日圆满举办. 此次伦敦路演会议中众多行业精英,各社区代表.星盟投资公司资 ...

  4. BGV劝早买内存

    12月3日,BGV全球首发,上线AOFEX交易所(A网),全球区块链爱好者震惊.很多人争相抢挖BGV,希望能够及早获取BGV带来的红利.有趣的是,随着BGV抢挖人数的增多,NGK内存也迎来了暴涨,在1 ...

  5. 为什么说NGK的去中心化预言机越来越受欢迎?

    2020年区块链市场非常火热,从年初的交易所杠杆,到Defi热潮,一波连着一波,风向不断切换,很多人无奈感叹跟不上时代,很多人欢欣雀跃登上了早班车.随着Defi的不断火热,预言机也进入了大众视野.NG ...

  6. 在next主题添加微信公众号二维码

    在侧边栏添加微信公众号二维码 首先,当然是准备一张微信公众号二维码.有两种添加方式,添加到侧边栏或者添加到推文的结尾处.我的next主题是7.x版本的,使用的主题是Gemini,设置的侧栏显示方式是一 ...

  7. Iterative learning control for linear discrete delay systems via discrete matrix delayed exponential function approach

    对于一类具有随机变迭代长度的问题,如功能性电刺激,用户可以提前结束实验过程,论文也是将离散矩阵延迟指数函数引入到状态方程中. 论文中关于迭代长度有三个定义值:\(Z^Ta\) 为最小的实验长度,\(Z ...

  8. ElementUI使用总结

    首先声明,我这总结的官网都有,只是将自己使用时遇到的问题,重新记录一下,官网地址:https://element.eleme.cn/ 1.表格内指定行数给定不同样式(类似于隔行变色,也能叫指定行数不同 ...

  9. 快速入门Redis调用Lua脚本及使用场景介绍

    Redis 是一种非常流行的内存数据库,常用于数据缓存与高频数据存储.大多数开发人员可能听说过redis可以运行 Lua 脚本,但是可能不知道redis在什么情况下需要使用到Lua脚本. 一.阅读本文 ...

  10. 【DB宝41】监控利器PMM的使用--监控MySQL、PG、MongoDB、ProxySQL等

    目录 一.PMM简介 二.安装使用 三.监控MySQL数据库 MySQL慢查询分析 四.监控PG数据库 五.监控MongoDB数据库 六.监控ProxySQL中间件 一.PMM简介 之前发布过一篇Pr ...