接口隔离原则
接口分为两种:
● 实例接口( Object Interface) , 在Java中声明一个类, 然后用new关键字产生一个实例, 它是对一个类型的事物的描述, 这是一种接口。 比如你定义Person这个类, 然后使用Person zhangSan=new Person()产生了一个实例, 这个实例要遵从的标准就是Person这个类, Person类就是zhangSan的接口。 疑惑? 看不懂? 不要紧, 那是因为让Java语言浸染的时间太长了, 只要知道从这个角度来看, Java中的类也是一种接口。
● 类接口( Class Interface) , Java中经常使用的interface关键字定义的接口。
主角已经定义清楚了, 那什么是隔离呢? 它有两种定义, 如下所示:
● Clients should not be forced to depend upon interfaces that they don't use.( 客户端不应该依赖它不需要的接口。 )
● The dependency of one class to another one should depend on the smallest possible interface.( 类间的依赖关系应该建立在最小的接口上。 )
我们可以把这两个定义概括为一句话: 建立单一接口, 不要建立臃肿庞大的接口。 再通俗一点讲: 接口尽量细化, 同时接口中的方法尽量少。 看到这里大家有可能要疑惑了, 这与单一职责原则不是相同的吗? 错, 接口隔离原则与单一职责的审视角度是不相同的, 单一职责要求的是类和接口职责单一, 注重的是职责, 这是业务逻辑上的划分, 而接口隔离原则要求接口的方法尽量少。 例如一个接口的职责可能包含10个方法, 这10个方法都放在一个接口中, 并且提供给多个模块访问, 各个模块按照规定的权限来访问, 在系统外通过文档约束“不使用的方法不要访问”, 按照单一职责原则是允许的, 按照接口隔离原则是不允许的,因为它要求“尽量使用多个专门的接口”。 专门的接口指什么? 就是指提供给每个模块的都应该是单一接口, 提供给几个模块就应该有几个接口, 而不是建立一个庞大的臃肿的接口, 容纳所有的客户端访问。
● 接口要尽量小
这是接口隔离原则的核心定义, 不出现臃肿的接口( Fat Interface) , 但是“小”是有限度的, 首先就是不能违反单一职责原则, 根据接口隔离原则拆分接口时, 首先必须满足单一职责原则。
● 接口要高内聚
什么是高内聚? 高内聚就是提高接口、 类、 模块的处理能力, 减少对外的交互。具体到接口隔离原则就是, 要求在接口中尽量少公布public方法, 接口是对外的承诺, 承诺越少对系统的开发越有利, 变更的风险也就越少, 同时也有利于降低成本。
● 定制服务
一个系统或系统内的模块之间必然会有耦合, 有耦合就要有相互访问的接口( 并不一定就是Java中定义的Interface, 也可能是一个类或单纯的数据交换) , 我们设计时就需要为各个访问者( 即客户端) 定制服务, 什么是定制服务? 定制服务就是单独为一个个体提供优良的服务。 我们在做系统设计时也需要考虑对系统之间或模块之间的接口采用定制服务。 采用定制服务就必然有一个要求: 只提供访问者需要的方法。
● 接口设计是有限度的
接口的设计粒度越小, 系统越灵活, 这是不争的事实。 但是, 灵活的同时也带来了结构的复杂化, 开发难度增加, 可维护性降低, 这不是一个项目或产品所期望看到的, 所以接口设计一定要注意适度, 这个“度”如何来判断呢? 根据经验和常识判断, 没有一个固化或可测量的标准。
接口隔离原则是对接口的定义, 同时也是对类的定义, 接口和类尽量使用原子接口或原子类来组装。 但是, 这个原子该怎么划分是设计模式中的一大难题, 在实践中可以根据以下几个规则来衡量:
● 一个接口只服务于一个子模块或业务逻辑;
● 通过业务逻辑压缩接口中的public方法, 接口时常去回顾, 尽量让接口达到“满身筋骨肉”, 而不是“肥嘟嘟”的一大堆方法;
● 已经被污染了的接口, 尽量去修改, 若变更的风险较大, 则采用适配器模式进行转化处理;
● 了解环境, 拒绝盲从。 每个项目或产品都有特定的环境因素, 别看到大师是这样做的你就照抄。 千万别, 环境不同, 接口拆分的标准就不同。 深入了解业务逻辑, 最好的接口设计就出自你的手中!
[Design Pattern is] a solution to a problem in a context.也就是说,设计模式是针对特定上下文的特定问题的解决方案,这种解决方案被抽象化、模版化,就是设计模式。
迪米特法则
迪米特法则( Law of Demeter, LoD) 也称为最少知识原则( Least Knowledge Principle, LKP) , 虽然名字不同, 但描述的是同一个规则: 一个对象应该对其他对象有最少的了解。 通俗地讲, 一个类应该对自己需要耦合或调用的类知道得最少, 你( 被耦合或调用的类) 的内部是如何复杂都和我没关系, 那是你的事情, 我就知道你提供的这么多public方法, 我就调用这么多, 其他的我一概不关心。
迪米特法则对类的低耦合提出了明确的要求, 其包含以下4层含义。1. 只和朋友交流
迪米特法则还有一个英文解释是: Only talk to your immediate friends( 只与直接的朋友通信。 ) 什么叫做直接的朋友呢? 每个对象都必然会与其他对象有耦合关系, 两个对象之间的耦合就成为朋友关系, 这种关系的类型有很多, 例如组合、 聚合、 依赖等。
人和人之间是有距离的, 太远关系逐渐疏远, 最终形同陌路; 太近就相互刺伤。 对朋友关系描述最贴切的故事就是: 两只刺猬取暖, 太远取不到暖, 太近刺伤了对方, 必须保持一个既能取暖又不刺伤对方的距离。 迪米特法则就是对这个距离进行描述, 即使是朋友类之间也不能无话不说, 无所不知。
在实际应用中经常会出现这样一个方法: 放在本类中也可以, 放在其他类中也没有错,那怎么去衡量呢? 你可以坚持这样一个原则: 如果一个方法放在本类中, 既不增加类间关系, 也对本类不产生负面影响, 那就放置在本类中。
4. 谨慎使用Serializable
在实际应用中, 这个问题是很少出现的, 即使出现也会立即被发现并得到解决。 是怎么回事呢? 举个例子来说, 在一个项目中使用RMI( Remote Method Invocation, 远程方法调用) 方式传递一个VO( Value Object, 值对象) , 这个对象就必须实现Serializable接口( 仅仅是一个标志性接口, 不需要实现具体的方法) , 也就是把需要网络传输的对象进行序列化, 否则就会出现NotSerializableException异常。 突然有一天, 客户端的VO修改了一个属性的访问权限, 从private变更为public, 访问权限扩大了, 如果服务器上没有做出相应的变更, 就会报序列化失败, 就这么简单。 但是这个问题的产生应该属于项目管理范畴, 一个类或接口在客户端已经变更了, 而服务器端却没有同步更新, 难道不是项目管理的失职吗?
开闭原则
开闭原则的定义:
Software entities like classes,modules and functions should be open for extension but closed formodifications.( 一个软件实体如类、 模块和函数应该对扩展开放, 对修改关闭。 )
软件实体包括以下几个部分:
● 项目或软件产品中按照一定的逻辑规则划分的模块。
● 抽象和类。
● 方法。
一个软件产品只要在生命期内, 都会发生变化, 既然变化是一个既定的事实, 我们就应该在设计时尽量适应这些变化, 以提高项目的稳定性和灵活性, 真正实现“拥抱变化”。 开闭原则告诉我们应尽量通过扩展软件实体的行为来实现变化, 而不是通过修改已有的代码来完成变化, 它是为软件实体的未来事件而制定的对现行开发设计进行约束的一个原则。
注意 开闭原则对扩展开放, 对修改关闭, 并不意味着不做任何修改, 低层模块的变更, 必然要有高层模块进行耦合, 否则就是一个孤立无意义的代码片段。
放弃修改历史的想法吧, 一个项目的基本路径应该是这样的: 项目开发、 重构、 测试、 投产、 运维, 其中的重构可以对原有的设计和代码进行修改,运维尽量减少对原有代码的修改, 保持历史代码的纯洁性, 提高系统的稳定性。
开闭原则是最基础的一个原则, 前五个原则都是开闭原则的具体形态,也就是说前五个原则就是指导设计的工具和方法, 而开闭原则才是其精神领袖。 换一个角度来理解, 依照Java语言的称谓, 开闭原则是抽象类, 其他五大原则是具体的实现类, 开闭原则在面向对象设计领域中的地位就类似于牛顿第一定律在力学、 勾股定律在几何学、 质能方程在狭义相对论中的地位, 其地位无人能及。
开闭原则是非常重要的, 可通过以下几个方面来理解其重要性:
1. 开闭原则对测试的影响
所有已经投产的代码都是有意义的, 并且都受系统规则的约束, 这样的代码都要经过“千锤百炼”的测试过程, 不仅保证逻辑是正确的, 还要保证苛刻条件( 高压力、 异常、 错误) 下不产生“有毒代码”( Poisonous Code) , 因此有变化提出时, 我们就需要考虑一下,原有的健壮代码是否可以不修改, 仅仅通过扩展实现变化呢? 否则, 就需要把原有的测试过程回笼一遍, 需要进行单元测试、 功能测试、 集成测试甚至是验收测试, 现在虽然在大力提倡自动化测试工具, 但是仍然代替不了人工的测试工作。
2. 开闭原则可以提高复用性
在面向对象的设计中, 所有的逻辑都是从原子逻辑组合而来的, 而不是在一个类中独立实现一个业务逻辑。 只有这样代码才可以复用, 粒度越小, 被复用的可能性就越大。 那为什么要复用呢? 减少代码量, 避免相同的逻辑分散在多个角落, 避免日后的维护人员为了修改一个微小的缺陷或增加新功能而要在整个项目中到处查找相关的代码, 然后发出对开发人员“极度失望”的感慨。 那怎么才能提高复用率呢? 缩小逻辑粒度, 直到一个逻辑不可再拆分为止。
3. 开闭原则可以提高可维护性
一款软件投产后, 维护人员的工作不仅仅是对数据进行维护, 还可能要对程序进行扩展, 维护人员最乐意做的事情就是扩展一个类, 而不是修改一个类, 甭管原有的代码写得多么优秀还是多么糟糕, 让维护人员读懂原有的代码, 然后再修改, 是一件很痛苦的事情, 不要让他在原有的代码海洋里游弋完毕后再修改, 那是对维护人员的一种折磨和摧残。
4. 面向对象开发的要求
万物皆对象, 我们需要把所有的事物都抽象成对象, 然后针对对象进行操作, 但是万物皆运动, 有运动就有变化, 有变化就要有策略去应对, 怎么快速应对呢? 这就需要在设计之初考虑到所有可能变化的因素, 然后留下接口, 等待“可能”转变为“现实”。
如何使用开闭原则
1. 抽象约束
抽象是对一组事物的通用描述, 没有具体的实现, 也就表示它可以有非常多的可能性,可以跟随需求的变化而变化。 因此, 通过接口或抽象类可以约束一组可能变化的行为, 并且能够实现对扩展开放, 其包含三层含义:
第一, 通过接口或抽象类约束扩展, 对扩展进行边界限定, 不允许出现在接口或抽象类中不存在的public方法;
第二, 参数类型、 引用对象尽量使用接口或者抽象类, 而不是实现类;
第三, 抽象层尽量保持稳定, 一旦确定即不允许修改。
2. 元数据( metadata) 控制模块行为
编程是一个很苦很累的活, 那怎么才能减轻我们的压力呢? 答案是尽量使用元数据来控制程序的行为, 减少重复开发。 什么是元数据? 用来描述环境和数据的数据, 通俗地说就是配置参数, 参数可以从文件中获得, 也可以从数据库中获得。
3. 制定项目章程
在一个团队中, 建立项目章程是非常重要的, 因为章程中指定了所有人员都必须遵守的约定, 对项目来说, 约定优于配置。
4. 封装变化
对变化的封装包含两层含义:
第一, 将相同的变化封装到一个接口或抽象类中;
第二,将不同的变化封装到不同的接口或抽象类中, 不应该有两个不同的变化出现在同一个接口或抽象类中。
封装变化, 也就是受保护的变化( protected variations) , 找出预计有变化或不稳定的点, 我们为这些变化点创建稳定的接口, 准确地讲是封装可能发生的变化, 一旦预测到或“第六感”发觉有变化, 就可以进行封装, 23个设计模式都是从各个不同的角度对变化进行封装的。
最佳实践
软件设计最大的难题就是应对需求的变化, 但是纷繁复杂的需求变化又是不可预料的。我们要为不可预料的事情做好准备, 这本身就是一件非常痛苦的事情, 但是大师们还是给我们提出了非常好的6大设计原则以及23个设计模式来“封装”未来的变化, 我们在前5章中讲过如下设计原则。
● Single Responsibility Principle: 单一职责原则
● Open Closed Principle: 开闭原则
● Liskov Substitution Principle: 里氏替换原则
● Law of Demeter: 迪米特法则
● Interface Segregation Principle: 接口隔离原则
● Dependence Inversion Principle: 依赖倒置原则
把这6个原则的首字母( 里氏替换原则和迪米特法则的首字母重复, 只取一个) 联合起来就是SOLID( solid, 稳定的) , 其代表的含义也就是把这6个原则结合使用的好处: 建立稳定、 灵活、 健壮的设计, 而开闭原则又是重中之重, 是最基础的原则, 是其他5大原则的精神领袖。 我们在使用开闭原则时要注意以下几个问题。
● 开闭原则也只是一个原则
开闭原则只是精神口号, 实现拥抱变化的方法非常多, 并不局限于这6大设计原则, 但是遵循这6大设计原则基本上可以应对大多数变化。 因此, 我们在项目中应尽量采用这6大原则, 适当时候可以进行扩充, 例如通过类文件替换的方式完全可以解决系统中的一些缺陷。大家在开发中比较常用的修复缺陷的方法就是类替换, 比如一个软件产品已经在运行中, 发现了一个缺陷, 需要修正怎么办? 如果有自动更新功能, 则可以下载一个.class文件直接覆盖原有的class, 重新启动应用( 也不一定非要重新启动) 就可以解决问题, 也就是通过类文件的替换方式修正了一个缺陷, 当然这种方式也可以应用到项目中, 正在运行中的项目发现需要增加一个新功能, 通过修改原有实现类的方式就可以解决这个问题, 前提条件是: 类必须做到高内聚、 低耦合, 否则类文件的替换会引起不可预料的故障。
● 项目规章非常重要
如果你是一位项目经理或架构师, 应尽量让自己的项目成员稳定, 稳定后才能建立高效的团队文化, 章程是一个团队所有成员共同的知识结晶, 也是所有成员必须遵守的约定。 优秀的章程能带给项目带来非常多的好处, 如提高开发效率、 降低缺陷率、 提高团队士气、 提高技术成员水平, 等等。
● 预知变化
在实践中过程中, 架构师或项目经理一旦发现有发生变化的可能, 或者变化曾经发生过, 则需要考虑现有的架构是否可以轻松地实现这一变化。 架构师设计一套系统不仅要符合现有的需求, 还要适应可能发生的变化, 这才是一个优良的架构。
开闭原则是一个终极目标, 任何人包括大师级人物都无法百分之百做到, 但朝这个方向努力, 可以非常显著地改善一个系统的架构, 真正做到“拥抱变化”。