1. UML 类图基础
  2. 设计模式
  3. 设计原则

0. UML 类图基础

UML 类图示例:

UML 类图中,依赖,关联,聚合,组合都表示两个类之间的关系
其中四个关系的强烈程度为:组合>聚合>关联>依赖

  1. 依赖:
    就是一个类 A 使用到了另一个类 B,而这种使用关系是具有偶然性的,临时性的,非常弱的,但是 B 类的变化会影响到 A,比如某人要过河,需要借用一条船,此时人与船之间的关系就是依赖;表现在代码层面,为类 B 作为参数被类 A 在某个 method 方法中使用

  2. 关联:
    他体现的是两个类、或者类与接口之间语义级别的一种强依赖关系,比如我和我的朋友;这种关系比依赖更强、不存在依赖关系的偶然性、关系也不是临时性的,一般是长期性的,而且双方的关系一般是平等的、关联可以是单向、双向的;表现在代码层面,为被关联类 B 以类属性的形式出现在关联类 A 中,也可能是关联类 A 引用了一个类型为被关联类 B 的全局变量

  3. 聚合:
    聚合是关联关系的一种特例,他体现的是整体与部分、拥有的关系,即 has-a 的关系,此时整体与部分之间是可分离的,他们可以具有各自的生命周期,部分可以属于多个整体对象,也可以为多个整体对象共享;比如计算机与 CPU、公司与员工的关系等;表现在代码层面,和关联关系是一致的,只能从语义级别来区分;可以看作雁群和大雁的关系,二者可以互相分离,有各自独立的生命周期

  4. 组合:
    组合也是关联关系的一种特例,他体现的是一种 contains-a 的关系,这种关系比聚合更强,也称为强聚合;他同样体现整体与部分间的关系,但此时整体与部分是不可分的,整体的生命周期结束也就意味着部分的生命周期结束;比如你和你的大脑;表现在代码层面,和关联关系是一致的,只能从语义级别来区分;可以看作大雁和翅膀的关系,二者互相依存,无法脱离对方而存在

1. 设计模式

1.1 简单工厂模式

简单工厂模式中
考虑使用一个专门的类(工厂类)来实现实例的创造过程
工厂类的静态方法,根据传入的参数,返回一个抽象类/接口的具体实现对象

简单工厂模式使得对象的创建和使用分离,实现了解耦

1.2 策略模式

超市在不同时候需要不同的计价策略,想要尽可能少地改动代码

同一个问题,可能有多种算法来解决
策略模式把不同的算法封装在同一个类中

策略类中保存一个算法抽象类的对象,在调用算法类时调用该算法对象的方法
在策略初始化的时候可以传入抽象算法类的具体对象,也可以与工厂模式相结合,传入特殊定义的参数

策略类(与工厂模式结合)可以通过 ifelse 或者 switch 来进行内部算法类对象的实例化,也可以通过反射来进行其初始化

1.3 装饰器模式

可能为一个对象的某个功能添加其他步骤
如在给一个人穿衣服的时候,可能以不同的顺序穿不同的衣服
希望减少代码和耦合,增加复用

一个装饰器类和一个被装饰的对象实现相同的接口
同时装饰器内部维护一个被装饰器类的对象
装饰器在调用本对象方法的时候,会自动调用其内部维护的被装饰对象的对应方法,并增加一些其他步骤

1.4 代理模式

  1. 远程代理
    为一个对象在不同的地址空间提供局部代表,这样可以隐藏一个对象处于不同地址空间的事实(通过代理访问另一个地址空间的对象的一部分)
  2. 虚拟代理
    根据需求创建开销很大的对象,通过代理来存放实例化需要很长时间的真实对象(HTML 网页中使用代理取代未被加载/无需加载的图片等资源)
  3. 安全代理
    控制真实对象的访问权限
  4. 智能指引
    调用对象时,进行一些其他操作(比如为对象进行引用计数,以实现对某个对象的 GC)

代理模式与装饰器模式很相似(都是具体对象和额外对象实现相同接口,额外对象持有具体对象的引用并调用其方法)
其边界比较模糊,但是其侧重点却很不一样
装饰器模式的重点在于通过额外对象增强具体对象的功能,具体对象对于用户来说是透明的
代理模式的重点在于通过额外对象实现对具体对象的控制,而不是增加功能,具体对象对用户来说是不可见的

转自知乎 | 果冻.Lee
1、装饰器模式强调的是增强自身,在被装饰之后你能够在被增强的类上使用增强后的功能。增强后你还是你,只不过能力更强了而已;代理模式强调要让别人帮你去做一些本身与你业务没有太多关系的职责(记录日志、设置缓存)。代理模式是为了实现对象的控制,因为被代理的对象往往难以直接获得或者是其内部不想暴露出来。
2、装饰模式是以对客户端透明的方式扩展对象的功能,是继承方案的一个替代方案;代理模式则是给一个对象提供一个代理对象,并由代理对象来控制对原有对象的引用;
3、装饰模式是为装饰的对象增强功能;而代理模式对代理的对象施加控制,但不对对象本身的功能进行增强;
1、装饰器模式强调的是增强自身,在被装饰之后你能够在被增强的类上使用增强后的功能。增强后你还是你,只不过能力更强了而已;代理模式强调要让别人帮你去做一些本身与你业务没有太多关系的职责(记录日志、设置缓存)。代理模式是为了实现对象的控制,因为被代理的对象往往难以直接获得或者是其内部不想暴露出来。2、装饰模式是以对客户端透明的方式扩展对象的功能,是继承方案的一个替代方案;代理模式则是给一个对象提供一个代理对象,并由代理对象来控制对原有对象的引用;3、装饰模式是为装饰的对象增强功能;而代理模式对代理的对象施加控制,但不对对象本身的功能进行增强

1.5 工厂模式

简单工厂模式违背了对修改封闭原则,因为在需要添加新的类的时候,需要修改简单工厂类的定义

为了解决这个问题,工厂模式把创建对象的选择延迟到了工厂类的子类中完成

工厂模式克服了简单工厂模式的缺点,同时保持了简单工厂模式的封装对象的创建过程的优点

但是工厂模式在增加新的创建对象的时候也需要增加新的子类
后面可以通过反射解决这个问题

1.6 原型模式

需要大量某个模板的复制

用原型示例指定创建对象的种类,并且通过拷贝这些原型创建新的对象

1.7 模板方法模式

要编写的多个类中有多个非常类似的函数(包括类名,参数,函数体等)
而仅在函数的局部有细微不同,希望增加复用,减少重复

把要编写的类抽象出来一个基类,基类中有两个函数,一个是所有类都类似的函数部分 F1,另一个是各个函数不同的部分 F2
F1 中会调用 F2,而 F2 需要由子类进行实现

这样一来就可以使得子类只需要重载 F2 以实现不同的部分,并由模板部分 F1 自动调用

1.8 外观模式

散户为了避免风险,降低耦合,选择通过基金机构进行投资(来自<<大话设计模式>>的奇妙比喻)

或者是一个复杂系统中存在很多复杂的系统调用,通过一套简介的 API 实现对其的封装

外观模式:为子系统中的一组接口提供一个一致的界面,此模式定义了一个高层接口,这个接口使得这一子系统更加容易使用

1.9 建造者模式

需要通过一套标准的流程来生产一个对象或其他东西
流程的步骤是一定的,但是标准流程的每个步骤中可以定制化

比如画一个小人,以固定的顺序画不同的部位就是标准流程
但是画不同部位的方法和风格就是可定制化的步骤

建造者模式:将一个复杂对象的构建和它的表示分离,使得同样的构建过程可以创建不同的表示

使用建造者模式,用户只需要指定建造的类型就可以得到它们了,而不用关注建造的过程和具体细节

1.10 观察者模式

在 IDE 中,不同的事件会触发不同控件的不同反应
如点击调试按键和运行按键多个控件会做出不同的反应

形象的例子:
一个公司里老板娘脾气好,老板脾气坏,老板娘来时需要关闭游戏,老板来时需要关闭游戏和股市
此时两个观察者在不同的事件发生时向全体员工发送通知

同时为了使得不同类型的员工都能收到消息(还能增加新员工的类型),员工采用面向抽象编程
为了使得能增加不同的观察者,观察者也面向抽象编程

观察者模式定义了一种一对多的依赖关系,让多个观察者对象同时监听一个主题对象.这个主题对象在状态发生变化时,会通知所有观察者模式,使它们能及时更新自己

同时观察者模式还有一个地方是耦合的:观察者需要实现指定的接口
解决方法:在主体类中增加一个 Handler 集合,其中存放不同的对象的函数(相当于对象+函数指针)
在发生事件的时候调用 Handler 集合中的函数

1.11 抽象工厂模式

在更换底层数据库的时候,需要更换一系列的跟具体数据库相关的类
希望能通过一个类来控制切换一系列类对象的实例

抽象工厂模式:提供一个创建一系列相关或相互依赖对象的接口,而无需指定它们具体的类

原本的每个工厂类之能生产一个产品,而抽象工厂可以生产一系列有耦合关系的产品
同时抽象工厂本身是一个接口,不同系列的产品由不同的具体抽象工厂类实现

但是抽象工厂本身也有一些问题:创建一个新系列的产品需要创建和修改的类太多
可以用简单工厂模式来对抽象工厂模式进行改进

原本创建不同系列的产品需要实现不同的抽象工厂具体类
通过简单工厂模式改进:在一个抽象工厂类中的所有的方法中都用简单工厂模式的方法通过 switch 或 if 进行判断,这样一来增加新的系列只需要改动这唯一的一个抽象工厂类就行了

但是使用简单工厂模式进行改进后依然存在问题,最大的问题就是违背了开闭原则
进一步改进可以使用反射进行改进

不再把创建产品的语句写为固定代码(new 语句),而是用反射动态创建产品
进一步改进可以通过配置文件+反射进行改进(完美)

1.12 状态模式

比如上班过程中,不同的时间有不同的状态
如果把所有时间的工作写在一个函数中,则会使得这整个函数非常臃肿且耦合度很高
可以使用状态模式把工作分为不同部分,交给不同的类/函数负责以减少耦合

状态模式:当一个对象的内部状态改变时允许改变其行为,这个对象看起来显示改变了类

状态模式类似有限状态机,每个状态有自己的状态,同时在满足一定条件的时候转向下一个状态

1.13 适配器模式

但一个系统的数据和行为都正确,但是接口不对的时候,可以使用适配器模式进行接口转换
就行手机适配器把 220v 的交流电压转换为手机需要的电压一样

适配器模式:将一个类的接口转换为客户希望看到的另一个接口,使得原本由于接口不同而不能一起工作的类能在一起工作

但是适配器模式本身还是属于补救的措施而不是优秀的构架模式
更好的处理方式是在设计初期就统一接口,或者在错误刚显现的时候进行重构来方式问题扩大
就像是扁鹊说的,治病于病发之前和病情初起时,而不是病重时

适配器模式的使用时机是原系统很复杂,重构难度大,新的需求简单且不会继续扩大,可以使用适配器模式进行备用补救措施

1.14 备忘录模式

需要保存程序运行中某个对象的内部状态(全部状态/局部状态)

备忘录模式:在不破坏封装性的情况下,捕获一个对象的内部状态,并在该对象之外保存这个状态,以后可以恢复到之前的状态

1.15 组合模式

总公司下有分公司,分公司下有分分公司…
同时所有公司下面有财务部和人力资源部

希望编写一个系统,处理所有公司及其两个部门
把部门和子公司都视为父公司的等价部分

组合模式:将对象组合起来,以树型结构存储表示”部分-整体”的层次结构.组合模式使得用户对单个对象和组合对象的使用具有一致性

1.15 迭代器模式

就是各种语言中都有的那个迭代器…

迭代器:提供一种方法,按照一定顺序访问呢一个聚合对象中的所有元素,而不暴露该对象的内部表示

1.16 单例模式

有的地方某个类只需要一个实例
如 Unity 中游戏控制器虽然很多地方都会用到,但是只应该有一个实例

单例模式:保证一个类仅有一个实例,并提供一个访问它的全局访问点

单例模式中的单例有两种方式实现实例化:饿汉式(在静态代码块中实例化),懒汉式(在初次使用的时候初始化)
其中在静态代码中实例化无需考虑多线程安全而在初次使用的时候使用需要处理多线程安全

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 普通锁
// 这种锁会频繁使用锁,影响性能
private static reaadonly Object syncRoot = new Object();
public static Singleton(){
lock(syncRoot){
if(instance == null){
instance = new Singleton();
}
}
return instance;
}
// 双重锁定(double-check locking)
private static reaadonly Object syncRoot = new Object();
public static Singleton(){
// 检查实例是否实例化
if(instance == null){
// 没有实例化的时候处理多线程的访问
lock(syncRoot){
if(instance == null){
instance = new Singleton();
}
}
}
return instance;
}

1.17 桥接模式

一个功能手机有多个功能,有多种品牌
不同的手机品牌不通功能不互通
想要以低耦合的方式实现多个手机类

桥接模式:将抽象部分和其实现部分分离,使其能各自独立变化

合成/聚合复用原则:尽量使用合成/聚合,而不要使用类继承

对象的继承关系是在编译时期确定的,无法在运行的时候改变从父类得到的实现,子类的实现与父类密切相关,耦合度很高
桥接模式是合成/聚合复用模式的具体表现形式之一

1.18 命令模式

相比于烧烤摊的用户直接与厨师交互
烧烤店通过服务员以命令的方式与厨师交互,使用了命令模式

命令模式:将一个请求封装为一个对象,从而使你可用不通的请求对客户端进行参数化,对请求排队或是记录请求日志,以及支持可撤销的操作.

但是不要在不一定需要命令模式的情况下盲目使用,敏捷开发原则告我我们:,实际上通过重构实现命令模式并不复杂

命令模式好处:

  1. 比较容易设计一个命令队列
  2. 在需要的情况下,比较容易记录日志
  3. 允许接受请求方决定是否要否决请求
  4. 比较容易实现请求的撤销与重做
  5. 命令模式把请求和执行分离了,实现了解耦

1.19 责任链模式

就像公司中的不同等级的管理者有不同的权限,一个请求(如请假,加薪)会逐级上传,直到有权限的管理者进行处理

责任链模式:使多个对象都有机会处理请求,从而避免请求的发送者和接受者之间的耦合关系.将这个对象连成一条链,并沿着这条链传递请求,直到一个对象处理它.

责任链中接收者和发送者都没有对方的信息,两种的对象本身也不知道链的存在,实现了降低耦合的目的.

1.20 中介者模式

一系列对象之间互相有关系(一个计算器中,不同的按键之间,屏幕之间互相有关联)
为每两个有关联的对象之间设计通信会使得整个系统耦合度极高

中介者模式:用一个中介对象来封装一系列的对象交互,中介者使对象不需要显示地互相引用,从而使其耦合松散,并且可以独立地改变它们之间的交互

中介者模式虽然降低了对象之间的耦合度,但是却大大增加了中介对象内部逻辑的复杂

中介者模式一般用于”一组对象以定义良好但是复杂的方式进行通信”的场合

1.21 享元模式

多个项目使用的代码和数据类似,分别在不同的环境中运行他们会浪费很多资源,使用享元模式可以大量节约资源
一个应用程序使用了大量的对象,这些对象造成了很大的存储开销,还有就是对象的大量状态可以是外部状态,如果删除对象的外部对象,那么可以用相对较少的共享对象代替很多组的对象,此时可以使用享元模式

享元模式:运用共享模式有效地支持大量细粒度的对象

1.22 解释器模式

简单的需要一种语言的情况可以使用解释器模式
复杂的可以分别用词法分析和语法分析等编译手段

解释器模式:给定一个语言,定义其文法的一种表示和一个解释器,可以使用这个解释器解释语言中的句子

如果一种特定类型的问题发生的频率足够高,那么可能就值得将该问题的各个实例表述为一个简单语言中的句子
同时构建一个解释器,该解释器通过解释句子来解决问题

1.23 访问者模式

适用于数据结构相对稳定,但是功能可能会发生改变的系统,访问者模式把数据结构及其操作之间的耦合解开了

如人有男女两种(数据结构稳定),但是男女对不同情况的反应不同
使用访问者模式相当于可以动态为数据结构添加功能
同时访问者模式使得增加数据结构变得复杂(需要修改已有的操作类型),故而适合数据结构稳定的系统

访问者模式:表示一个作用于某对象结构中的各元素的操作,它使你可以在不改变各元素的类的前提下定义作用于这些元素的新操作

2. 设计原则

2.1 单一职责原则

对于一个俄罗斯方块游戏来说,游戏逻辑和 UI 界面时两个不同的职责,可以封装在两个类中
这样做的好处是在迁移到不同的地方的时候,如果 UI 界面需要变动,游戏逻辑部分依然可以复用
实现了代码逻辑的解耦

单一职责原则(Single Responsibility Principle,SRP):就一个类而言,应该仅有一个引起它变化的原因.

2.2 开放封闭原则

比如做一个计算器类,我们可能在最开始直接把四则运算放在同一个类中
但是这种设计不仅对修改封闭,对扩展也是封闭的(无法以较小的代价添加新的运算功能)

在需要进行重构的时候,我们可以把运算类抽象为一个接口,该接口有 GetResult()方法
并使用工厂模式/策略模式进行重构
这样一来,在未来需要添加行功能的时候,就可以以较小的代价达到目的了
即实现了对扩展开放

开放-封闭原则是面向对象设计的核心所在
即面向更改封闭,面向扩展开放

我们应该尽可能地预见到可能发生的变化,并允许相应的扩展(做出相应的抽象)
当时我们无论如何都无法预见到所有的变化,此时就进入了一个不得不进行’更改’的境地

但是我们在不得不进行修改的同时,也需要对这种变化做出抽象(不要在同一个地方摔倒两次)

2.3 依赖反转原则

  1. 高层模块不应该依赖模块,二者应该依赖接口
    显示屏不应该依赖显卡
    显卡和显示屏应该依赖标准的接口

  2. 抽象不应该依赖细节,细节应该依赖抽象
    显示屏的接口不应该依赖具体型号的显示屏
    不同信号的显示屏应该依赖标准的接口

正是由于计算机硬件的设计实现了这种原则,其内部的不同模块互不影响(CPU,内存,显卡互相独立)
而收音机内部则是强耦合的
所以复杂的计算机比简单的收音机更容易维修

依赖反转:Dependence Inversion Principle
控制反转:Inversion of Control

前一个是设计原则,后一个是 Spring 框架的特性,二者有异曲同工之妙(或者 Spring 就是按照 DIP 原则设计的)

两句话总结就是:

  • 高层模块不应该依赖低层模块,二者应该依赖接口
  • 抽象不应该依赖细节,细节应该依赖抽象

同时,DIP 原则可以使得不同的模块耦合度降低,即互不影响

2.4 迪米特法则

在处理事务的时候,被各个部门踢皮球就是违反这一法则的体现.
因为不同的部门之间没有严格遵守迪米特法则,使得耦合度很高,一个事务需要牵扯到很多部门,且无法动态调整

迪米特法则:如果两个类不必彼此直接通信,则两个类就不应该发生直接的相互作用.如果其中一个类需要调用另一个类的功能,可以通过第三方转发这个调用

迪米特法则强调在类的结构设计上,每个类都应该尽可能地降低成员的访问权限.换句话说就是强调类之间的松耦合
类之间的耦合越弱,越有利于复用,一个类的修改对其他类的影响越小