软件设计原则 Software Design Principles
软件设计 Software Design
- 需求 requirements
- 系统需要满足的目标
- 规约 specification
- 系统的外部可观察到的行为
- 架构 architecture
- 系统一级的主要组成部分
- 各部分的交互方法
- 使用的技术
- 设计 design
- 如何完成任务
- 需要写的代码
对于面向对象的软件系统设计来说,在支持可维护性的同时没需要提高系统的可复用性
软件的复用可以提高软件的开发效率,提高软件质量,节约开发成本,恰当的复用可以改善系统的可维护性
面向对象软件设计 Object-Oriented Software Design
- 将实现的约束条件应用到面向对象分析所产生的概念模型的过程
- 用方法和属性来描述用于构成系统的类
- 添加不明显属于领域的类,比如抽象类和接口
- 描述类是如何构成组件的
OOA 得到业务概念模型,OOD 在它基础上结合系统架构和实现约束,设计出类、接口、组件结构,最后 OOP 把这些设计写成代码
(A:Analysis 分析;D:Design 设计;P:Programming 编程)
面向对象设计原则 Object-Oriented Design Principles
软件的可维护性和可复用性
- 软件的复用优点众多,恰当的复用还可以改善系统的可维护性
- 面向对象设计复用的目标在于实现支持可维护性的复用
- 面向对象的设计里面,可维护性复用都是以面向对象设计原则为基础的,这些设计原则首先都是复用的的原则,遵循这些设计原则可以有效地提供系统的复用性,同时提高系统的可维护性
- 面向对象设计原则也是对系统进行合理重构的指南针,重构是在不改变软件现有功能的基础上,通过调整程序代码改善软件的质量、性能,使其程序的设计模式和架构更趋合理,提高软件的扩展性和维护性
常用面向对象设计原则
- 单一职责原则 SRP = Single Responsibility Principle
- 类的职责要单一,不能将太多的职责放到一个类中
- 开闭原则 OCP = Open-Closed Principle
- 软件实体对扩展是开放的,但对修改是关闭的,即在不修改一个软件实体的基础上去扩展其功能
- 里氏代换原则 LSP = Liskov Substitution Principle
- 在软件系统中,一个可以接受基类对象的地方必然可以接受一个子类对象
- 依赖倒转原则 DIP = Dependency Inversion Principle
- 要针对抽象层编程,而不要针对具体类编程
- 接口隔离原则 ISP = Interface Segregation Principle
- 使用多个专门的接口来取代一个统一的接口
- 合成复用原则 CRP = Composite Reuse Principle
- 在系统中应该尽量多使用组合和聚合关联关系,尽量少使用甚至不使用继承关系
- 迪米特法则 LoD = Law of Demeter
- 一个软件实体对其他实体的引用越少越好,或者说如果两个类不必彼此直接通信,那么这两个类就不应当发生直接的相互作用,而是通过引入一个第三者发生间接交互
单一职责原则 Single Responsibility Principle
- 一个对象应该只包含单一的职责,并且该职责被完整地封装在一个类中
- 就一个类而言,应该仅有一个引起它变化的原因
分析
- 一个类(或者大到模块,小到方法)承担的职责越多,它被复用的可能性越小,而且如果一个类承担的职责过多,就相当于将这些职责耦合在一起,当其中一个职责变化时,可能会影响其他职责的运作
- 类的职责主要包括两个方面:数据职责和行为职责,数据职责通过其属性来体现,行为职责通过其方法来体现
- 单一职责原则是实现高内聚、低耦合的指导方针

开闭原则 Open-Closed Principle
- 一个软件实体应当对扩展开放,对修改关闭。也就是说在设计一个模块的时候,应当使这个模块可以在不被修改的前提下被扩展,即实现在不修改源代码的情况下改变这个模块的行为
- 这里的软件实体可以指一个软件模块、一个由多个类组成的局部结构或一个独立的类
分析
- 抽象化是开闭原则的关键
- 开闭原则还可以通过一个更加具体的“对可变性封装原则”来描述,即要求找到系统的可变因素并将其封装起来

里氏代换原则 Liskov Substitution Principle
- 在软件中如果能够使用基类对象,那么一定能够使用其子类对象。把基类都替换成它的子类,程序将不会产生任何错误和异常,反过来则不成立,如果一个软件实体使用的是一个子类的话,那么它不一定能够使用基类
- 里氏代换原则是实现开闭原则的重要方式之一,由于使用基类对象的地方都可以使用子类对象,因此在程序中尽量使用基类类型来对对象进行定义,而在运行时确定其子类类型,用子类对象来替换父类对象

依赖倒转原则 Dependency Inversion Principle
- 高层模块不应该依赖低层模块,它们都应该依赖抽象。抽象不应该依赖于细节,细节应该依赖于抽象
- 要针对接口编程,不要针对实现编程
分析
-
代码要依赖于抽象的类,而不要依赖于具体的类;要针对接口或抽象类编程,而不是针对具体类编程
-
实现开闭原则的关键是抽象化,并且从抽象化导出具体实现,如果说开闭原则是面向对象设计的目标的话,那么依赖倒转原则就是面向对象设计的主要手段
-
常用实现方式是在代码中使用抽象类,而将具体类放在配置文件中
-
类之间的耦合
- 零耦合关系
- 具体耦合关系
- 抽象耦合关系 一个类不依赖具体实现类,而依赖接口或抽象类
-
依赖倒转原则要求客户端依赖于抽象耦合,以抽象方式耦合是依赖倒转原则的关键

接口隔离原则 Interface Segregation Principle
- 客户端不应该依赖哪些它不需要的接口
- 一旦一个接口太大,则需要将它分隔成一些更细小的接口,使用该接口的客户端仅需要知道与之相关的方法即可
分析
- 用多个专门的接口,而不使用单一的总接口
- 一个接口只代表一个角色
- 接口仅提高客户端需要的行为
- 使用接口隔离原则拆分接口时,首先必须满足单一职责原则,将一组相关的操作定义在一个接口中,且在满足高内聚的前提下,接口中的方法越少越好
- 可以在进行系统设计时采用定制服务的方式,即为不同的客户端提高宽窄不同的接口,只提高用户需要的行为,而隐藏用户不需要的行为

合成复用原则 Composite Reuse Principle
- 尽量使用对象组合,而不是继承来达到复用的目的
分析
- 在一个新的对象里通过关联关系(包括组合关系和聚合关系)来使用一些已有的对象,使之成为新对象的一部分;新对象通过委派调用已有对象的方法达到复用其已有功能的目的
- 要尽量使用组合/聚合关系,少用继承
- 组合/聚合可以使系统更加灵活,类与类之间的耦合度降低;使用继承时,需要严格遵循里氏代换原则,有效使用继承会有助于对问题的理解,降低复杂度,而滥用继承反而会增加系统构建和维护的难度以及系统的复杂度,因此需要慎重使用继承复用

两个DAO应该去聚合DBUtil而非继承,这样在后续换get Connection的方式时,才能在不修改代码的情况下便于扩展(写一个DBUtil的子类来注入到DAO里面)
最小知识原则(迪米特法则) Law of Demeter
迪米特法则定义
- 迪米特又称为最少知识原则,它有多种定义方法,其中几种典型定义如下:
- 不要和陌生人说话
- 只与你的直接朋友通信
- 每一个软件单位对其他的单位只有最少的知识,而且局限于那些与本单位密切相关的软件单位
- 迪米特法则就是指一个软件实体应当尽可能少的与其他实体发生相互作用。这样,当一个模块修改时,就会尽量少的影响其他模块,扩展会相对容易,这是对软件实体之间通信的限制,它要求限制软件实体之间的通信的宽度和深度
分析
-
在迪米特法则中,对于一个对象,其朋友包括以下几类:
- 当前对象本身
- 以参数形式传大到当前对象方法中的对象
- 当前对象的成员对象
- 如果当前对象的成员对象是一个集合,那么集合中的元素也都是朋友
- 当前对象所创建的对象
-
任何一个对象,如果满足上面的条件之一,就是当前对象的”朋友“,否则就是”陌生人“
-
迪米特法则可分为狭义法则和广义法则
- 在狭义的迪米特法则中,如果两个类之间不必彼此直接通信,那么这两个类就不应当发生直接的相互作用,如果其中的一个类需要调用另一个类的某一个方法的话,可以通过第三者转发这个调用
- 可以降低类之间的耦合,但是会在系统中增加大量的小方法并散落在系统的各个角落,它可以使一个系统的局部设计简化,因此每一个局部都不会和远距离的对象有直接的关联,但是也会造成系统的不同模块之间的通信效率降低,使得系统的不同模块之间不容易协调
- 广义的迪米特法则:指对对象之间的信息流量、流向以及信息的影响的控制,主要是对信息隐藏的控制。信息的隐藏可以使各个子系统之间脱耦,从而允许它们独立地开发、优化、使用和修改,同时可以促进软件的复用,由于每一个模块都不依赖于模块而存在,因此每一个模块都可以独立地在其他的地方使用。一个系统的规模越大,信息的隐藏就越重要,而信息隐藏的重要性也就越明显
- 迪米特法则的主要用途在于控制信息的过载
- 在类的划分上,应当尽量创建松耦合的类,类之间耦合度越低,就越有利于复用,一个处在松耦合中的类一旦被修改,不会对关联的类造成太大波及;
- 在类的结构设计上,每一个类都应当尽量降低其成员变量和成员函数的访问权限
- 在类的设计上,只要有可能,一个类型应当设计成不变类
- 在对其他类的引用上,一个对象对其他对象的引用应当降到最低

- 在狭义的迪米特法则中,如果两个类之间不必彼此直接通信,那么这两个类就不应当发生直接的相互作用,如果其中的一个类需要调用另一个类的某一个方法的话,可以通过第三者转发这个调用
在 JDK 中,java.util.Stack是java.util.Vector类的子类,该设计合理吗?若不合理,请分析解释该设计存在的问题
Stack 继承 Vector 是为了代码复用,但继承关系表达了错误的语义,暴露了不该暴露的方法,破坏了栈的封装;更合理的做法是用组合,而不是继承。
- 目标:开闭原则
- 指导:最小知识原则
- 基础:单一职责原则、可变性封装原则
- 实现:依赖倒转原则、合成复用原则、里氏代换原则、接口隔离原则
高频英文术语 Glossary
| English Term | 中文 | 备注 |
|---|---|---|
| Requirement | 需求 | 系统需要满足的目标 |
| Specification | 规约 | 系统外部可观察行为 |
| Architecture | 架构 | 系统级主要组成与交互 |
| Design | 设计 | 如何完成任务、如何组织代码 |
| OOA, Object-Oriented Analysis | 面向对象分析 | 得到业务概念模型 |
| OOD, Object-Oriented Design | 面向对象设计 | 把概念模型转为类、接口、组件 |
| OOP, Object-Oriented Programming | 面向对象编程 | 把设计落到代码 |
| SRP, Single Responsibility Principle | 单一职责原则 | 一个类只有一个引起变化的原因 |
| OCP, Open-Closed Principle | 开闭原则 | 对扩展开放,对修改关闭 |
| LSP, Liskov Substitution Principle | 里氏代换原则 | 子类应能替换父类 |
| DIP, Dependency Inversion Principle | 依赖倒转原则 | 高层和低层都依赖抽象 |
| ISP, Interface Segregation Principle | 接口隔离原则 | 不依赖不需要的接口 |
| CRP, Composite Reuse Principle | 合成复用原则 | 优先组合/聚合,而不是继承 |
| LoD, Law of Demeter | 迪米特法则 | 只与你的直接朋友通信 |
| Cohesion | 内聚 | 模块内部职责集中程度 |
| Coupling | 耦合 | 模块之间依赖程度 |
| Refactoring | 重构 | 不改变外部功能前提下改进结构 |
| Inheritance | 继承 | 子类继承父类属性和方法 |
| Composition | 组合 | 强 has-a,生命周期绑定 |
| Aggregation | 聚合 | 弱 has-a,部分可独立存在 |
| Abstraction | 抽象 | 面向接口/抽象类编程的基础 |