一、七大基本原则
1.1 单一职责原则SRP
- 一个类只负责一项职责
- 如果一个类负责多个职责,当某个职责的需求发生变化,存在潜在的隐患(其他职责的代码无法正常工作)
- 降低类复杂度,提高代码可读性和维护性
- 可以违背单一职责,但要确保不会对已有的功能造成影响
1.2 开放关闭原则OCP
开放关闭解释:对扩展开放(类、模块、函数等可以被扩展),对修改关闭(但是不可以被修改)。
- 能扩展已经存在的系统,已经存在的系统,不需要修改
- 不是所有模块都能满足OCP,尽量满足。
- 提高扩展性和维护性
1.3 里氏替换原则LSP
- 子类继承父类的时候,除了添加新的功能,尽量不要影响父类方法的原来的预期行为
- 子类可以扩展父类功能,但不能改变父类原来的功能
1.4 依赖倒转原则DIP
- 高层模块不应该依赖于低层模块,而是依赖于抽象(而不是细节)
- 面向接口编程,而不是面向具体的类
- 降低类之间的耦合度
1.5 接口隔离原则ISP
- 客户端不应该依赖它不需要的接口,一个类对接口的依赖应该是最小最细的(适度)
- 接口定义太大,需要进行接口拆分
- 建立单一接口,细化接口
1.6 迪米特法则(最少知道原则)LOD
- 类只与直接朋友(出现在成员变量、方法参数和方法返回值中的类)通信,对朋友保持最少的依赖,对于非直接朋友通过第三个类实现信息的传递
- 减少对朋友的了解
- 实践:类最好不要以局部变量的形式出现在类的内部,尽量减少一个类对外暴露的方法
- 降低耦合
1.7 组合/聚合复用原则
- 在新的对象里通过组合/聚合已有的对象,让其成为新对象的一部分,尽量不使用继承
- 新的对象通过委派给原对象来达到复用原有的功能的目的
- 分清楚是Has-A/Contains-A还是Is-A,如果确定需要继承,看看是否满足迪米特原则
- 降低耦合
二、面向对象关系(与UML图解)
这部分按照耦合度递增的顺序进行介绍
耦合度:当一个类变化,对其他类造成影响的程度
5.1 依赖关系 Dependency
- 类A为了完成某个功能,引用类B
- 代码中的体现:类成员函数的返回值、函数参数、局部变量、静态方法调用
- UML表示:虚线+箭头
5.2 关联关系 Association
-
类A是类B的一个属性
-
代码中表现:成员变量
-
UML表示:实线+箭头
-
与依赖关系的区别:依赖关系是类方法调用产生,方法结束后结束;关联关系是类实例化产生,对象销毁时结束。
-
关联分:单向关联、双向关联、自身关联、多维关联
5.3 聚合关系 Aggregation
- 表示集合与个体的关联关系(班级和学生)Contains-A
- 代码中表现:成员变量的集合
- UML表示:空心菱形
5.4 组合(复合)关系 Composition
- 个体和组成部分的关联关系(人类和手臂)Has-A
- 代码中表现:成员变量(和关联关系一样)
- UML表示:实心菱形
- 聚合和组合的区别:聚合的成员可以独立的存在,复合的成员必须依赖于整体
5.5 泛化关系 Generalization
- 类与类的继承关系和类与接口的实现关系
- 代码中表现:接口、类
- UML表示:实线+空心三角形表示继承;虚线+空心三角形表示实现
三、创建型模式
创建型模式提供了创建对象的机制
3.1 单例模式Singleton
保证类只有一个实例,并且提供一个访问该实例的全局节点
3.1.1 解决的问题
- 保证一个类只有一个实例
- 提供一个访问该实例的全局节点
3.1.2 解决方案
- 单例实例保存在私有静态成员变量中
- 类的构造函数设为私有:防止其他对象new,使用公有静态方法用于获取单例实例
- 静态方法中延迟初始化,第一次创建新对象,以后都直接返回实例
- 客户端调用静态方法获取单例实例
3.1.3 结构
- 单例类声明了一个获取实例的静态方法
- 构造函数需要对客户端隐藏(private)
3.1.4 适用场景
- 某个类只能有一个实例
- 更加严格的控制全局变量
3.2 工厂方法模式Factory Method
父类中提供一个创建对象的方法,允许子类决定实例化对象的类型
3.2.1 解决问题
- 方便代码添加新的类,而不是将代码与具体的类耦合
- 将创建产品的代码和实际使用产品的代码分离,可以在不影响其他代码的情况下扩展产品创建部分代码
3.2.2 结构
- 产品接口:创建者所返回的类型
- 具体产品:产品接口的实现
- 创建者:声明返回产品的方法,返回类型与产品接口类型相同
- 具体创建者:重写创建者的方法,返回不同的产品
3.3.3 适用场景
- 无法预知对象的确切类别和依赖关系
- 希望用户能扩展软件库或框架的内部组件
- 希望复用现有对象,而不是每次重新创建对象
3.3 抽象工厂模式Abstract Factory
创建一系列相关的对象,无需指定具体类
3.3.1 解决问题
- 添加新产品或者添加新的产品(类别)风格的时候,不需要唏嘘改核心代码
- 通过同一个工厂创建的一系列产品不会是风格不一致的
3.3.2 解决方案
- 抽象产品接口:构成系列中的每个不同的产品声明单独的接口
- 具体产品:是抽象产品的不同类型的实现
- 抽象工厂:声明一组创建各种抽象产品的方法,包含制造所有产品的接口
- 具体工厂:实现抽象工厂,返回特定类别(风格)的产品,工厂内的产品风格相同
3.3.3 适用场景
- 代码需要与多个不同系列的相关产品交互
- 扩展性,不希望代码基于产品的具体类而构建
3.4 工厂模式比较
3.4.1 工厂(多个意义、根据上下文确定)
表示可以创建一些东西的函数、方法、类,工厂可以创建对象、文件、数据库记录
- 创建GUI的函数、方法
- 创建用户的类
- 调用类构造函数的静态方法
- 一种创建型设计模式
3.4.2 构建方法
- 构造函数调用的封装器
3.4.3 静态构建方法
- 被声明为static的构建方法
3.4.4 简单工厂模式
- 一个拥有大量条件语句的类,可以根据方法参数选择对何种产品初始化并返回
3.4.5 工厂方法模式
- 创建型设计模式,父类中提供创建对象的方法,子类来决定实例化的对象类型
3.4.6 抽象工厂模式
- 创建型设计模式,创建一系列相关的对象无需指定具体类
3.5 建造者模式Builder
分步骤创建复杂对象,可以使用相同的创建代码生成不同的类型和形式的对象
3.5.1 问题
- 对于复杂对象的构造,需要对许多成员变量和嵌套对象进行复杂的初始化,导致构造函数参数过多,很多又不使用
- 新增对象需要扩展原有的类,产生许多子类
3.5.2 解决办法
- 将对象构造代码从产品类中提取出来,放入到生成器中
- 不同形式的产品(构造过程需要不同的实现)对应不同的生成器
- 主管类定义创建步骤的执行顺序(非必需),隐藏产品构造细节
3.5.3 结构
- 生成器接口:声明通用的产品构造步骤
- 具体生成器:生成器接口构造步骤的具体实现,也可以构造不遵循通用接口的产品
- 产品:生成的对象
- 主管:定义构造步骤的顺序,隐藏特定产品的构造细节
- 客户端:调用生成器或者关联主管,获得产品(只有产品遵循相同接口,主管才能返回产品,否则都是通过生成器获取)。
3.5.4 适用场景
- 避免重叠构造函数(很多参数的构造函数)
- 创建不同形式的产品(石头或木头房屋)
- 构造对象树或复杂对象
3.6 原型模式Prototype
复制已有对象,无需依赖对象所属的类
3.6.1 问题
- 对象私有成员变量无法从对象外面进行复制给其他变量
3.6.2 解决办法
- 支持克隆的对象(称为原型)声明一个通用接口(包含克隆方法),使得能克隆对象
- 将克隆过程委派给被克隆的实际对象
- 还可以先创建一系列预生成对象,并对其配置,clone的时候与预生成对象对比,如果相同就直接克隆。
3.6.3 结构
- 原型接口:声明克隆clone方法
- 具体原型:实现克隆方法,将原始数据复制到克隆体、递归克隆等
- 客户端:实现了原型接口的对象,调用clone得到克隆对象
- 原型注册表:存储一系列可随时复制的预生成对象
3.6.4 适用场景
- 需要复制一些对象,又希望代码独立于所属的具体类
- 如果子类的区别知识对象初始化方式不同,可以使用原型模式减少子类数量
四、结构型模式
结构型模式将对象和类组装成较大的结构
4.1 适配器模式Adapter/Wrapper
使接口不兼容的对象可以相互合作
4.1.1 问题
将现有代码与不兼容的第三方库整合,需要修改现有代码(可能根本没法修改)
4.1.2 解决方法
- 使用适配器转换对象接口,适配器将复杂的转换过程隐藏
4.1.3 结构
对象适配器
- 客户端:包含当前业务逻辑的类,通过接口与适配器进行交互
- 客户端接口:描述其他类与客户端代码合作的时候必须遵循的协议
- 服务:与客户端不兼容的第三方程序或库,代码通常无法修改
- 适配器:同时和客户端和服务进行交互。实现了客户端接口,接受客户端发起的请求,然后封装转换为适用于服务对象的调用
类适配器
在多继承语言中(C++),同时继承了客户端和服务的行为,直接替代客户端使用
4.1.4 适用场景
- 需要使用的类(第三方类、库)与当前代码不兼容
- 在继承体系内的一些类,拥有一些共同的方法,但是却不是本继承体系内所有子类的共性
4.2 代理模式Proxy
提供对象的替代品。代理控制对原对象的访问,允许在请求提交给对象前后进行处理
4.2.1 问题
- 延迟初始化或缓存,所有的客户端都需要执行延迟初始化代码,导致代码重复。
- 希望将代码放入对象的类中,如果是三方封闭库泽无法实现
4.2.2 解决办法
- 新建一个与原服务对象接口相同的代理类
- 将代理对象传递给原始对象客户端
- 代理类收到客户端请求,然后创建实际的服务对象,并委派工作给他
4.2.3 结构
- 服务接口:代理必须遵循此接口才能伪装成服务对象
- 服务类:提供业务逻辑
- 代理类:包含服务对象的引用,代理完成了任务(缓存、延迟初始化、日志记录、访问控制)后将请求传递给服务对象
- 客户端:与服务对象或者代理对象交互
4.2.4 适用场景
- 虚拟代理:延迟初始化。对于偶尔使用的重量级服务对象,一直保持对象会消耗资源。
- 保护代理:访问控制。希望特定的客户端使用服务对象,代理在客户端经过认证后才传递给服务对象。
- 远程代理:本地执行远程服务,通过网络传递客户端请求,处理和网络有关的细节。
- 日志记录代理:保存服务对象的请求历史记录,在请求传递前进行记录
- 缓存代理:对请求结果进行缓存并管理缓存生命周期(特别是大体积结果)
- 智能引用:代理记录了所有正在使用服务对象的客户端,如果客户端列表为空,就销毁服务对象。还可以复用没有被修改的服务对象。
4.3 装饰器模式Decorator
将对象放入包含行为的特殊封装对象中来给原对象绑定新的行为
4.3.1 问题
-
需要改变类的行为时候,继承是静态的,而且子类只能有一个父类(编程语言限制)
-
通过子类可以扩展一个类的行为,但是子类组合数量将爆炸
4.3.2 解决办法
- 一个对象可以使用多个类的行为,包含了多个指向其他类的引用,并把工作委派给引用的对象。
- 装饰器包含与目标对象相同的一系列方法,将收到的请求委派给目标对象
4.3.3 结构
- 组件接口:声明装饰器和被装饰对象的公共接口
- 具体组件类:被封装对象的类,定义了基础的行为,装饰器可以改变这些行为
- 基础装饰器类:拥有一个被封装对象的引用,该对象类型是组件接口类型,这样就可以引用具体的组件类和装饰器
- 具体装饰器类:定义了动态添加到组件的额外动作
- 客户端:使用多层装饰器装饰组件
4.3.4 适用场景
- 无需修改代码的情况下使用对象,并且增加额外的行为
- 使用继承来扩展对象行为的方案难以实现(收到final关键字限制)
4.4 组合模式Composite
将对象组合成树状结构,能像独立对象一样使用他们
4.4.1 问题
当应用结构类似于一棵树,循环计算起来不方便,甚至不可行。(如复杂订单)
4.4.2 解决办法
- 内部以递归的方式处理对象树的所有项目
4.4.3 结构
- 组件接口:描述树中简单项目和复杂项目的共有操作
- 叶节点:树的基本结构,完成实际的工作
- 容器(或组合):包含叶节点或其他容器,委派工作给子项目
- 客户端:通过组件接口与所有项目交互
4.4.4 适用场景
- 需要实现树状的对象结构
- 客户端代码需要用相同的方式处理简单的和复杂的元素
4.5 桥接模式Bridge
将一个大类或一系列紧密相关的类拆分为抽象和实现两个独立的层次结构,能在开发时分别使用
4.5.1 问题
- 在多个独立的维度上扩展基础类,在处理继承时很常见
4.5.2 解决办法
- 将继承改为组合
- 抽取其中一个维度,变成独立的类层次,在初始的类引用这个新层次的对象
4.5.3 结构
- 抽象部分:提供高层逻辑,依赖于底层的实现对象
- 实现部分:为具体实现提供通用接口
- 具体实现:包括特定于平台的代码
- 精确抽象:控制逻辑的变体
- 客户端:将抽象对象与实现对象连起来
4.5.4 适用场景
- 拆分或重组一个具有多重功能的复杂类,例如与多个数据库进行交互的类
- 想要在几个独立的维度上扩展一个类
- 在运行时切换不同的实现方法
4.6 外观模式Facade
为程序可、框架和其他复杂类提供一个简单的接口
4.6.1 问题
- 当代码中使用某个复杂的库或者框架,业务逻辑与三方类紧密结合,后期理解维护困难
4.6.2 解决办法
外观类为包含许多活动部件的复杂子系统提供一个简单的接口,其中包含的功能少,使用方便
4.6.3 结构
- 外观:快速便捷的访问特定的子系统功能,可以操作一切活动部件和重定向用户请求
- 附加外观:避免多种不相关的功能污染单一外观,造成外观类又变成复杂的结构
- 复杂子系统:如果需要使用这些类,需要了解细节,需要正确初始化并提供正确的数据
- 客户端:使用外观类代替对子系统的直接调用
4.6.4 适用场景
- 需要一个指向复杂子系统的直接接口,且该接口功能有限
- 需要将子系统组织为多层结构
4.7 享元模式Flyweight(Cache)
共享多个对象所共有的相同状态,在有限的内存容量中载入更多的对象
4.7.1 问题
- 大量的独立对象导致内存不足
- 程序存在大量类似对象同时占用大量内存,并且无法使用更好的方式解决
4.7.2 解决办法
- 不再对象中存储外在状态(能被其他对象从外部改变),只在对象中保留内在状态(外部对象只能读取不能修改值),内部状态变体很少(可以缓存)
- 仅存储内在状态的对象叫做享元
4.7.3 结构
- 享元类:包含原始对象中部分能在多个对象中共享的状态(内在状态),传递给享元方法的状态是外在状态
- 上下文类(Context):包含原始对象中各不相同的外在状态,组合享元对象就能表示原始对象
- 客户端:计算或存储享元的外在状态
- 享元工厂:对已有的享元的缓存池进行管理
4.7.4 适用场景
- 程序必须支持大量对象且没有足够的内存容量