设计原则:开闭原则 OCP(ocp是什么意思)

1.什么是开闭原则
开闭原则的英文是Open Closed Principle,缩写就是OCP 。其定义如下:
软件实体(模块、类、方法等)应该“对扩展开放、对修改关闭” 。
从定义上看 , 这个原则主要包含两部分:
对扩展开放:“ 这意味着模块的行为是可以扩展的 。当应用程序的需求改变时 , 我们可以对其模块进行扩展 , 使其具有满足那些需求变更的新行为 。换句话说 , 我们可以改变模块的功能 。
对修改关闭:“ 对模块行为进行扩展时 , 不必改动该模块的源代码或二进制代码 。模块的二进制可执行版本 , 无论是可链接的库、DLL或Java的.jar文件 , 都无需改动 。
通俗解释就是 , 添加一个新的功能 , 应该通过在已有代码(模块、类、方法)的基础上进行扩展来实现 , 而不是修改已有代码 。
之前的一篇文章《 何谓高质量代码? 》中 , 我们总结了高质量代码的几个衡量标准 。
而开闭原则解决的就是代码的扩展性问题 。如果某段代码在应对未来需求变化的时候 , 能够做到“对扩展开放、对修改关闭” , 那就说明这段代码的扩展性比较好 。
2.如何做到对扩展开放、对修改关闭
那么应该怎样写出扩展性好的代码呢?
在思想上我们要具备扩展意识、抽象意识、封装意识 。这些意识的培养要比一些具体的方法更为重要 , 这依赖我们对面向对象的理解、对业务的掌握度 , 以及长期的经验积累…… 这要求我们在写代码的时候后 , 要多花点时间往前多思考一下 , 未来可能有哪些需求变更 , 识别出代码的易变部分与不易变部分 , 合理设计代码结构 , 事先留好扩展点 , 以便在未来不需要改动代码整体结构、做到最小代码改动的情况下 , 新的代码能够很灵活地插入到扩展点上 。
在方法上 , 我们主要可以通过多态、依赖注入、面向接口编程等方式来实现代码的可扩展性 。做到“对扩展开放、对修改关闭” 。我们要将可变部分抽象出来以隔离变化 , 提供抽象化的不可变接口 , 给上层系统使用 。当具体的实现发生变化的时候 , 我们只需要基于相同的抽象接口 , 扩展一个新的实现 , 替换掉老的实现即可 , 上游系统的代码几乎不需要修改 。
比如 , 我们的项目中通常会用到一些第三方组件 , 消息中间件 , 缓存中间件……消息中间件我们可能一开始使用RabbitMQ , 但是可能后来会换成Kafka , 缓存中间件可能会从Memcache换成Redis 。这种情况 , 如果我们的上层应用直接依赖这些中间件调用代码 , 那么更换的成本就会更高 , 这种代码就不利于扩展 。
ocp是什么意思(设计原则:开闭原则(OCP))
public class MemcacheClient { public boolean set(String key, String value) { return false; } public String get(String key) { return null; } public boolean remove(String key) { return false; }}public class OcpApplication { public void test() { // 业务代码 //... //... //写缓存 MemcacheClient client = new MemcacheClient(); client.set("testKey", "testValue"); }}
如上示例 , 我们的上层应用OcpApplication直接依赖了MemcacheClient , 如果未来有需要把Memcache换成Redis,我们就需要替换掉所有调用了MemcacheClient的上层应用方法 , 这严重违背了开闭原则 。
在这种情况下 , 通常我们会把这种中间件的调用设计成可插拔的 。我们提供一个这些中间件的抽象接口出来 , 让所有上层系统都依赖这组抽象的接口编程 , 并且通过依赖注入的方式来调用 。当我们要替换新的中间件的时候 , 比如将Memcache替换成Redis , 就可以可以很方便地拔掉老的Memecache实现 , 插入新的Redis实现 。
ocp是什么意思(设计原则:开闭原则(OCP))
/** * 缓存中间件的使用抽象出接口 */public interface ICacheClient { boolean set(String key, String value); String get(String key); boolean remove(String key);}/** * MemcacheClient */public class MemcacheClient implements ICacheClient { public boolean set(String key, String value) { return false; } public String get(String key) { return null; } public boolean remove(String key) { return false; }}/** * RedisClient */public class RedisClient implements ICacheClient { @Override public boolean set(String key, String value) { return false; } @Override public String get(String key) { return null; } @Override public boolean remove(String key) { return false; }}public class OcpApplication { /** * 依赖注入cacheClient */ ICacheClient cacheClient; public OcpApplication(ICacheClient cacheClient){ this.cacheClient=cacheClient; } public void test() { // 业务代码 //... //... //写缓存 cacheClient.set("testKey", "testValue"); }}
3.如何灵活运用开闭原则
【设计原则:开闭原则 OCP(ocp是什么意思)】开闭原则看似简单 , 但我却认为是SOLID 中最难掌握的一条原则 。其难点就在于如何在真正的项目中去灵活运动开闭原则 。而且OCP同样存在着一些陷阱 , 怎么才算满足或违反开闭原则 , 修改代码就一定意味着违反开闭原则吗 , 扩展点设计的越多越好吗……
3.1 灵活设计扩展点
对于业务系统 , 要想识别出尽可能多的扩展点 , 就要求你对业务有足够的了解 , 能够预见一些未来可能的变化 。
对于偏技术的系统 , 比如 , 框架、组件、类库等 , 就需要充分了解它的使用场景?以及今后想要扩展点功能?使用者未来会有哪些更多的诉求……
但即便我们对业务、对系统有足够的了解 , 也不可能识别出所有的扩展点 , 即便可以 , 并为这些地方都预留扩展点 , 也是没有必要的 。同样有一条原则叫KISS原则 , 那就是尽量保持简单 , 不要进行过度设计 , 实际上很多人都会陷入这样一个误区 , 我们常常为了一些很可能不存在的扩展而绞尽脑汁!
最合理的做法就是 , 对于一些比较确定的、短期内可能就会扩展 , 或者需求改动对代码结构影响比较大的情况 , 或者实现成本不高的扩展点 , 可以事先做些扩展性设计 。但对于一些不确定未来是否要支持的需求 , 或者实现起来比较复杂的扩展点 , 我们可以等到有需求驱动的时候 , 再通过重构代码的方式来支持扩展的需求 。
3.2 修改代码一定违反开闭原则吗
开闭原则中对于修改是封闭的并非是一个绝对的概念 。
1.修复缺陷所做的改动
缺陷在软件中很常见 , 是不可能完全消除的 。当缺陷出现时 , 就需要我们修复现有的代码 。软件修复明显倾向于实用主义而不是坚持开放封闭原则 。
2.客户端无法感知到的改动
如果一个类的改动会引起另一个类的改动 , 那么这两个类就是紧密耦合的 。相反 , 如果一个类的修改总是独立的 , 并不会引起其他类的改动 , 那么这些类就是松散耦合的 。我们要记住 , 任何情况下 , 松散耦合都比紧密耦合要好 。如果我们对现有代码的修改不会影响客户端代码 , 那么也就谈不上违背开放封闭原则 。
3.修改还是扩展?
从开闭原则定义中 , 我们可以看出 , 开闭原则可以应用在不同粒度的代码中 , 可以是模块 , 也可以类 , 还可以是方法(及其属性) 。同样一个代码改动 , 在粗代码粒度下 , 可以被认定为“修改” , 但在细代码粒度下 , 又可以被认定为“扩展” 。
比如 , 在类这个层面添加属性和方法相当于修改类 , 这个代码改动可以被认定为“修改”;但这个改动并没有修改已有的属性和方法 , 在方法(及其属性)这一层面 , 它又可以被认定为“扩展” 。
实际上 , 当纠结于某个代码改动是“修改”还是“扩展”的时候 , 我们就已经背离了设计原则的初衷 , 开闭原则的本质目的就是为了让我们的代码更具有扩展性 , 更容易维护 , 如果我们可以很容易的完成修改 , 又不会影响到既有的代码与单测 , 就可以认为这是一个合理的改动 。
3.3 扩展性与可读性的平衡
在有些情况下 , 代码的扩展性会跟可读性相冲突 。为了更好地支持扩展性 , 我们对代码进行了重构 , 重构之后的代码要比之前的代码复杂很多 , 理解起来也更加有难度 。实际上很多时候 , 我们都要结合具体的场景在扩展性和可读性之间做权衡 。在某些场景下 , 扩展性很重要 , 我们就可以适当地牺牲一些可读性;而在另一些场景下 , 可读性更加重要 , 那我们就适当地牺牲一些扩展性 。
小结
绝大多数情况下 , 我们的系统都不是一锤子买卖 , 通常随着需求的迭代 , 我们需要不断地对其进行维护与扩展 。而开闭原则的思想可以很好的解决扩展性的问题 , 因此理解并掌握开闭原则至关重要 , 但这需要我们充分的理解面向对象的思想 , 合理的利用封装、多态等方法以及长期大量的积累!

    推荐阅读