面向对象设计的六大原则 : 单一职责原则, 里氏替换原则, 依赖倒置原则, 接口隔离原则, 迪米特法则, 开闭原则;
一. 单一职责原则
1. 单一职责简介
单一职责定义 : 有且只有一个原因引起类的变化, 一个接口 或者 类 只有一个职责;
单一职责的好处 :
-- 复杂性 : 降低类的复杂性, 对类或接口的职责有清晰明确定义;
-- 可读性 : 提高可读性;
-- 维护 : 提高可维护性;
-- 变更风险 : 降低变更引起的风险, 接口改变只影响相应的实现类, 不影响其他类;
2. 单一职责示例
(1) 反面示例
示例要求 : 创建一个绘图系统
-- 绘图 : 可以绘制圆形, 矩形;
-- 显示 : 显示绘制好的图形;
UML 图 : 明显下图不符合单一职责原则, 绘制圆形, 矩形, 显示圆形, 矩形 都集成在了一个接口中, 明显不符合单一职责原则;
(2) 正面示例
修改一下上面的示例, 我们可以得到下面的 UML 图 :
-- 说明 : 将 绘制 和 显示 分别封装在两个接口中, 让一个实现类同时继承这两个接口, 此时实现了接口的单一职责;
3. 单一职责实践
单一职责适用 : 单一职责同时适用于接口, 类, 方法;
-- 接口 : 接口一定要做到单一职责;
-- 类 : 类的单一职责比较难以实现, 尽量做到只有一个原因引起变化;
-- 方法 : 一个方法尽可能做一件事, 能分解就分解, 分解到原子级别;
二. 里氏替换原则
1. 里氏替换简介
(1) 里氏替换定义
里氏替换 : 所有 引用基类的地方 必须能 透明地使用其子类的对象;
-- 子类替换父类 : 只要 父类出现的地方子类就可以出现, 替换为子类也不会产生任何错误, 使用者不需要知道父类还是子类;
(2) 继承的优缺点
继承优点 :
-- 代码共享 : 共享代码, 子类都拥有父类的方法和属性, 将 父类的代码共享给了子类;
-- 重用性 : 提高代码的重用性, 子类重用父类的代码;
-- 子父类异同 : 子类形似父类, 异于父类, 父子都不同;
-- 扩展性 : 提高代码的可扩展性, 子类就可以为所欲为了, 子类可以随意扩展父类;
-- 开放性 : 提高产品或项目的开放性, 父类随意扩展, 开放性随之增加了;
继承缺点 :
-- 侵入性 : 继承是侵入性的, 子类 强制继承 父类的方法和属性;
-- 灵活性 : 降低代码的灵活性, 子类必须拥有父类的属性和方法, 子类收到了父类的约束, 这是从子类的角度讲得;
-- 耦合性 : 增强了耦合性, 父类的属性和方法被修改时, 还需要顾及其子类, 可能会带来大量的重构, 这是从父类的角度讲的;
2. 里氏替换规范含义
(1) 子类完全实现父类方法
替换方法 : 定义一个接口或抽象类, 编码实现一个子类继承或实现该接口或抽象类, 调用类直接传入接口或抽象类;
示例说明 :
-- UML 图 :
-- Client 场景类 : 在该场景中创建一个 Player 玩家, 然后为 Player 设置枪, 之后开枪 kill;
-- Player 类 : 玩家类, 该类有一个 IGun 抽象类成员变量;
-- IGun : 所有枪的基类, 所有的枪都要继承该类;
-- 说明 : 在 Player 中 IGun 类型的成员变量都可以使用 IGun 的三个实现类来替代;
(2) 子类个性
里氏替换单向性 : 子类可以有自己的方法和属性, 里氏替换可以正着用, 使用子类替换子类, 但是反过来不可以, 子类出现的地方, 父类不能使用;
(3) 覆盖方法参数放大
前置后置条件 : 子类方法中得前置条件必须与超类中被覆写的前置条件相同或者更宽松;
-- 前置条件 : 关于参数, 输入的参数必须符合要求, 才会执行, 必须满足的条件;
-- 后置条件 : 关于返回值, 方法执行完之后, 需要返回一个结果, 这个结果的标准;
重写与多态 : 如果子类的前置条件与父类相同, 那么是重写方法, 如果子类更宽松, 那么就是多态, 生成了新的方法;
(4) 覆盖方法返回值缩小
返回值 : 父类方法返回值类型 F, 子类方法返回值类型 S, 里氏替换原则是 S 范围必须小于 F;
-- 重写 : 父类子类参数相同, S 范围小于 F;
-- 重载 : 父类 子类 方法参数类型或者数量不同, 如果要符合里氏替换要求的话, 子类参数必须 >= 父类参数, 即不能让子类自己定义的方法被调用;
3. 里氏替换注意点
避免子类个性 : 如果想要使用里氏替换, 尽量避免让子类拥有自己单独的成员变量 或者 方法, 如果子类个性多了, 子类父类关系很难调和;
-- 里氏替换缺点 : 将子类当做父类用, 抹杀了子类的个性;
-- 里氏替换优点 : 将子类单独作为一个业务来使用, 会让代码间的耦合关系都复杂, 缺乏类替换标准;
三. 依赖倒置原则
1. 依赖倒置简介
(1) 依赖倒置定义
模块与抽象 :
-- 低层模块 : 不可分割的原子逻辑是低层模块;
-- 高层模块 : 原子逻辑组合成高层模块;
-- 抽象 : 接口或者抽象类, 不能被实例化;
-- 细节 : 实现类, 实现接口或继承抽象类就是细节, 可以被实例化;
依赖倒置定义 :
-- 模块依赖 : 高层模块不应该依赖底层模块, 两者都依赖其抽象, 实现类之间不发生依赖关系, 依赖关系通过接口或抽象类产生;
-- 抽象不依赖细节 : 抽象不依赖细节, 接口或抽象类不依赖与实现类;
-- 细节依赖抽象 : 实现类依赖接口或抽象类;
(2) 依赖倒置好处
依赖倒置好处 : 依赖倒置原则可以 减少类之间的耦合, 提高系统稳定性, 降低并发风险, 提高代码可读性 和 可维护性;
-- 耦合 :
-- 并发风险 :
-- 可读性 :
-- 可维护性 :
2. 依赖倒置注入实现
(1) 构造函数依赖对象
注入方法 : 通过 构造函数参数 声明依赖对象, 即构造函数注入;
(2) Setter 方法依赖对象
注入方法 : 通过 Setter 函数 参数 声明依赖对象, 即构造函数注入;
(3) 接口注入依赖对象
注入方法 : 在接口方法的参数中声明依赖对象, 即接口注入;
3. 依赖倒置遵循规则
依赖倒置本质 : 通过 抽象 即 接口或者抽象类, 使 各个类 和 模块实现彼此独立, 实现模块间 松耦合;
(1) 类有抽象
抽象所有的类 : 每个类尽量都有接口或者抽象类, 最好接口和抽象类都有;
(2) 变量类型抽象
变量类型抽象 : 变量的表面类型尽量都定义成抽象类;
-- 注意 : 不是绝对的, 一些工具类, 组件不必定义抽象;
(3) 类派生控制
派生控制 : 任何类不能从具体类派生;
-- 开发阶段 : 在开发阶段, 从具体类派生类是不允许的, 这样会增加继承的层次, 加大后期维护难度, 尽量将继承层次控制在 2 层以内;
-- 维护阶段 : 维护阶段可以出现从具体类派生的情况, 这样更有利于系统的稳定性;
(4) 不要覆盖方法
不要覆盖方法 : 尽量不要覆盖方法, 如果方法在抽象类中已经实现, 子类不要覆盖;
-- 覆盖缺点 : 会对系统稳定性产生影响;
(5) 结合里氏替换
里氏替换 : 父类出现的地方子类就能出现,
接口,抽象类,实现类规则 :
-- 接口 : 负责定义 public 属性和方法, 并声明与其它对象的依赖关系;
-- 抽象类 : 负责公共构造部分实现;
-- 实现类 : 准确地实现业务逻辑, 适当时候对父类进行细化;
4. 依赖倒置注意点
(1) 依赖正置
依赖正置 : 类之间的依赖是实体类之间的依赖, 即面向现实编程;
-- 例如 : 我开宝马车, 我 是 人类型, 宝马车 是车 类型, 依赖倒置就是 人 依赖 车, 依赖正置就是 我 依赖 宝马车;
(2) 依赖倒置使用场合
依赖倒置使用场景 :
-- 小项目 : 依赖倒置在小项目中得有点很难体现出来, 是否采用依赖倒置原则影响不大;
-- 大项目 : 项目越大, 需求改变越多, 采用依赖倒置原则设计的接口或抽象类 对 实现类的约束, 会大大减少维护成本;
(3) 依赖倒置与其它原则
依赖倒置与开闭原则联系 : 依赖倒置原则是实现开闭原则的重要途径, 依赖倒置原则没有实现, 无法对扩展开放, 对修改关闭;
四. 接口隔离原则
1. 接口隔离原则简介
(1) 接口分类
接口分类 :
-- 实例接口 : Java 中得一个类, 对一个类型的描述, 例如 Student xiaoming; 其中的 Student 类就是实例接口, 这不是我们这里所关心的;
-- 类接口 : Java 中得 Interface 接口, 这是我们所说的接口隔离原则中得接口;
(2) 接口隔离定义
接口隔离定义 : 建立单一的接口, 功能尽量细化, 不要建立臃肿的接口;
-- 不需要的接口 : 客户端尽量不依赖其不需要的接口, 客户端需要什么接口就提供什么接口, 剔除不需要的接口, 对接口进行细化, 保持接口方法最少;
-- 最小接口 : 类间的依赖关系应该建立在最小接口上, 细化接口;
(3) 接口隔离 与 单一职责区别
单一职责 与 接口隔离 区别 :
-- 单一职责 : 注重职责, 注重业务逻辑上得划分;
-- 接口隔离 : 注重的是接口的方法尽量少;
(4) 模块 与 接口
模块与接口 : 每个模块尽量都提供一个单一接口, 有几个模块, 就由几个接口, 避免建立一个庞大臃肿的接口对应所有的模块;
2. 接口隔离原则实现
(1) 接口尽量小
拆分接口 : 接口隔离的核心定义, 不出现臃肿的接口;
-- 限制 : 接口小有限度, 不能违反单一职责原则, 不要将一个业务逻辑拆分成两个接口;
-- 要求 : 根据接口隔离原则拆分接口时, 必须满足单一职责原则;
(2) 接口高内聚
高内聚 : 提高接口, 类, 模块的处理能力, 减少对外界交互;
-- 具体方法 : 接口中尽量少公布public 方法, 对外公布的 public 方法越少, 变更的风险就越小, 有利于后期的维护;
(3) 定制服务
定制服务 :
-- 起源 : 系统模块间的耦合需要有相互访问的接口, 这里就需要为各个 客户端 的访问提供定制的服务接口;
-- 要求 : 只提供访问者需要的方法, 不需要的就不提供;
(4) 接口隔离限度
接口隔离局限性 :
-- 粒度小 : 接口粒度越小, 系统越灵活, 但是同时使系统结构复杂, 开发难度增加, 降低了系统的可维护性;
-- 粒度大 : 灵活性降低, 无法提供定制服务, 增大项目风险;
3. 原子接口划分原则
原子接口或类 : 接口隔离原则 即适用于 接口的定义, 也适用于对类的定义, 那么这样的接口和类就是 原子接口 和 原子类;
(1) 接口模块一一对应
接口模块对应关系 : 一个接口只服务于一个子模块 或 业务逻辑;
(2) 压缩方法
方法压缩 : 通过业务逻辑, 压缩接口中得 public 方法, 减少接口的方法的数量;
(3) 接口修改
修改适配 : 尽量去修改已经污染的接口, 如果变更风险较大, 采用适配器模式进行转化处理;
五. 迪米特法则
1. 迪米特法则定义
迪米特法则 : 最少知识原则, 一个对象应该对其它对象有最少的了解, 即一个类对自己需要耦合或者调用的类知道的最少;
2. 低耦合要求
(1) 只和朋友交流
只与朋友通信 :
-- 朋友形成 : 一个对象与其它对象有耦合关系, 两个对象间的耦合使两个对象成为朋友关系;
-- 朋友定义 : 出现在 类 成员变量 , 方法参数返回值 中的类是朋友, 其它位置的不是朋友, 在方法体内出现的其它类不是朋友;
(2) 朋友距离
朋友间必须保持距离 :
-- 距离太近示例 : 朋友间不能无话不说, 无所不知, 类 A 与 B 耦合, B 将很多方法暴露给 A, 两个类之间的的耦合关系非常牢固, 这明显违反设计原则;
-- 保持距离方法 : 将 类 B 暴露给 A 的方法封装, 暴露的方法越少越好, 类 B 高内聚, 与 A 低耦合;
-- 设计方法 : 一个类的 public 方法越多, 修改时涉及的范围也就越大, 变更引起的风险也就越大, 在系统设计时要注意, 能用 private 就用private , 能用 protected 就用 protected, 能少用 public 就少用 public, 能加上 final 就加上 final;
(3) 类方法位置确定
原则 : 如果一个方法放在本类, 不增加类间关系, 不对类产生负面影响, 就放在本类中;
3. 迪米特法则注意事项
迪米特法则核心原则 : 类间解耦, 弱耦合, 耦合降低, 复用率提高;
-- 局限性 : 类间的耦合性太低, 会产生大量的中转或跳转类, 会导致系统的复杂性提高, 加大维护难度;
六. 开闭原则
1. 开闭原则定义
开闭原则定义 : 软件的实体 类, 模块, 函数 应该对扩展开放, 对修改关闭; 即 软件实体 应该 通过扩展实现变化, 不是通过 修改已有的代码实现变化;
-- 软件实体 : 软件产品中得 逻辑 划分模块, 抽象 和 类, 方法;
2. 变化
各种变化 :
-- 逻辑变化 : 变化一个逻辑模块, 其它模块不改变, 所有的依赖 或 关联关系都按照相同的逻辑处理;
-- 子模块变化 : 一个逻辑模块变化, 会影响其它模块, 低层次模块 (原子逻辑模块) 变化会引起高层次模块 (原子模块组合) 的变化, 通过扩展完成变化时, 高层次模块需要修改;
3. 开闭原则好处
开闭原则好处 :
-- 利于测试 : 如果改变软件内容, 需要将所有的测试流程都执行一遍, 如 单元测试, 功能测试, 集成测试等, 如果只是扩展, 只单独测试扩展部分即可;
-- 提高复用性 : 所有逻辑都从原子逻辑组合, 原子逻辑粒度越小, 复用性越大; 这样避免相同逻辑存在, 修改时需要修改多个此相同逻辑;
-- 提高可维护性 : 维护一个类最好的方式是 扩展一个类, 而不是修改一个类, 如果需要修改需要读懂源码才能修改, 扩展的话只需要了解即可, 直接继承扩展;