目录
设计模式
面向对象编程
设计原则
单一职责原则(Single Resbosibility Principle)
开闭原则(Open Closed Principle)
里式替换原则
接口隔离原则
依赖反转原则
KISS、YAGNI原则
DRY原则
迪米特法则(LOD, Law of Demeter)
规范与重构
单元测试
编程规范
设计模式与范式
单例模式
工厂模式
Dependency Injection框架
构造者模式
建造者模式
原型模式(不常用)
代理模式(Proxy Design Pattern)
桥接模式(Bridge Design Pattern)
装饰器模式
适配者模式(Adapter Design Pattern)
代理、桥接、装饰器、适配器 4 种设计模式的区别
门面模式
组合模式
享元模式(Flyweight Design Pattern)
观察者模式(Observe Design Pattern)/发布订阅者模式(Publish-Subscribe Design Pattern)——EventBus
模板模式(Template Method Design Pattern)
策略模式
理解 接口
一组API接口集合
举个例子。微服务用户系统提供了一组API,包括注册、登陆、获取用户信息等。现在后台管理系统需要用户系统需要删除用户的接口。但是这个接口只希望后台管理系统来调用,因此,可以将对应的api放在另一个单独的interface。
单个API接口或函数
单个函数的功能不要太复杂。比如
public class Statistics { private Long max; private Long min; private Long average; private Long sum; private Long percentile99; private Long percentile999; //...省略constructor/getter/setter等方法... } public Statistics count(Collection
现在的接口的count函数功能性太复杂,包含了很多不同的统计功能,按照接口隔离原则,应该把count()函数拆成几个更小粒度的函数。满足接口隔离原则之后,可以改进为:
public Long max(Collection
不过,这也需要结合具体场景。比如,在业务场景中,每个统计需求Statistics定义的那几个统计信息都有涉及,那原来的count()函数设计就是合理的;如果每个统计需求只涉及到其中一部分,而count()函数都需要把所有信息都算一遍,这就会影响代码性能。
OOP中的接口概念
可以理解为面向对象编程中的接口语法,那接口的设计要尽量单一,不要让接口的实现类或者调用者,依赖不需要的接口。
控制反转(IOC)
控制反转是一个比较笼统的设计思想,并不是一种具体的实现方法,一般用来指导框架层面的设计。我们按照框架设计的接口或者定义的规则编码,框架会自动调用我们的代码。比如Spring的初始化,我们实现一些Component/Configuration,框架会自动帮我们注册。
依赖注入(DI)
这是一种具体的编程技巧。简单来说就是,不通过new的方式在类的内部创建依赖类的对象,而是在外部创建好,然后通过构造函数/函数参数等方式传递(注入)给类来使用。
依赖注入框架(DI Framework)
我们通过依赖注入框架提供的扩展点,简单配置一下所需要的类及其类与类之间依赖关系,就可以实现由框架来自动创建对象、管理对象的生命周期、依赖注入等原本需要程序员来做的事情。
依赖反转原则(DIP)
高层模块不依赖底层模块,他们共同依赖同一个抽象。抽象不要依赖具体的实现细节,具体实现细节依赖抽象。
KISS(Keep It Simple and Stupid)
- 不要使用同事可能不懂的技术来实现代码;
- 不要重复造轮子,要善于使用已经有的工具类库
- 不要过度优化
YAGNI(You Ain't Gonna Need It)
不要做过度设计。不要去设计当前用不到的功能,不要编写当前用不到的代码,可以留好扩展点。
DRY(Don't Repeat Yourself)
代码重复的三种情况:实现逻辑重复、功能语义重复、代码执行重复。
高内聚,松耦合
- 单一职责原则
- 适用对象:模块,类,接口
- 侧重点:高内聚,低耦合
- 思考角度:自身
- 接口隔离原则
- 适用对象:接口,函数
- 侧重点:低耦合
- 思考角度:调用者
- 基于接口而非实现编程
- 适用对象:接口,抽象类
- 侧重点:低耦合
- 思考角度:调用者
- 迪米特法则
- 适用对象:模块,类
- 侧重点:低耦合
- 思考角度:类关系
单元测试 VS 集成测试
集成测试的测试对象是整个系统或者某个功能模块,比如测试用户注册、登录功能是否正常,是一种端到端(end to end)的测试。
单元测试的测试对象是类或者函数,用来测试一个类和函数是否都按照预期的逻辑执行。这是代码层级的测试
命名
name
,而不是userName
;函数参数也可以借助函数这个上下文来简化命名,比如:public void uploadUserAvatarImageToAliyun(String userAvatarImageUri); //利用上下文简化为: public void uploadUserAvatarImageToAliyun(String imageUri);
注释
注释的内容主要包含这样三个方面:做什么、为什么、怎么做。对于一些复杂的类和接口,我们可能还需要写明“如何用”。
https://static001.geekbang.org/resource/image/f3/d3/f3262ef8152517d3b11bfc3f2d2b12d3.png?wh=5013*3903
设计模式要干的事情就是解耦,创建型模式是将创建和使用代码解耦,结构型模式是将不同功能代码解耦,行为型模式是将不同的行为代码解耦 。
单例设计模式。一个类只允许创建一个对象(或者实例),这个类就是一个单例类,这种设计模式就叫单例设计模式,简称单例模式。
简单工厂模式
工厂模式
当创建逻辑比较复杂,是一个“大工程”的时候,我们就考虑使用工厂模式,封装对象的创建过程,将对象的创建和使用相分离。
何为创建逻辑比较复杂?
- 第一种:类似规则配置解析的例子,代码中存在if-else分支判断,动态地根据不同的类型创建不同的对象。可以将这一大坨if-else创建对象的代码抽离出来,放到工厂类中。
- 第二种:尽管我们不需要根据不同的类型创建不同的对象,但是,单个对象本身的创建过程比较复杂,比如前面提到的要组合其他类对象,做各种初始化操作。在这种情况下我们也可以考虑使用工厂模式,将对象的创建过程封装到工厂类中。
对于第一种情况,如果创建对象过程不复杂,用简单工厂模式,如果创建对象过程比较复杂,为了避免设计一个过于庞大的简单工厂类,推荐使用工厂模式,将创建逻辑拆分得更细,每个对象的创建逻辑独立到各自的工厂类中。对于第二种,工厂模式。
工厂模式
- 封装变化:创建逻辑有可能变化,封装成工厂类之后,创建逻辑的变更对调用者透明
- 代码复用:创建逻辑的代码 抽离到独立的工厂类之后可以复用
- 隔离复杂性:封装复杂的创建逻辑,调用者无需了解如何创建对象
- 控制复杂度:将创建代码抽离出来,让本来的函数或职责更单一,代码更简洁
DI容器底层最基本的设计思路就是基于工厂模式。DI容器相当于一个大的工厂类
什么情况下使用建造者模式?
工厂模式 VS 建造者模式 工厂模式是用来创建不同但是相关类型的对象(继承同一父类或者接口的一组子类),根据给定参数来决定创建哪种类型的对象。 建造者模式是用来创建一种类型的复杂对象,可以通过设置不同的可选参数,“定制化”地创建不同的对象。
什么情况下使用建造者模式?
工厂模式 VS 建造者模式
工厂模式是用来创建不同但是相关类型的对象(继承同一父类或者接口的一组子类),根据给定参数来决定创建哪种类型的对象。
建造者模式是用来创建一种类型的复杂对象,可以通过设置不同的可选参数,“定制化”地创建不同的对象。
原型模式:如果对象的创建成本比较大,而同一个类的不同对象之间差别不大(大部分字段都相同),在这种情况下,我们可以利用对已有对象(原型)进行复制(拷贝)的方式来创建新对象,以达到节省创建时间的目的。
原型模式的两种实现方法:
虽然深拷贝比浅拷贝更加耗时,耗内存空间,但是对于可变对象来说,浅拷贝得到的对象和原始对象会共享部分数据,有可能出现数据被修改的风险,所以除非深拷贝非常耗时,不推荐浅拷贝。
代理模式在不改变原石类(或叫被代理类)代码的情况下,通过引入代理类来给原始类附加功能。
应用场景:常用在业务系统中开发一些非功能性需求:监控、统计、鉴权、限流、事务、幂等、日志。除此之外,代理模式还可以用在RPC、缓存等应用场景。
将抽象和实现解耦,让它们可以独立变化。比较难理解 可以结合JDBC驱动理解桥接模式。
装饰器模式和静态代理模式很像。主要作用是给原始类增强功能,装饰器类需要跟原始类继承相同的抽象类或者接口。
适配者模式的原理和与实现
这个模式就是用来做接口适配的,他将不兼容的接口转换为可兼容的接口,类比USB转换器。它有两种实现方式:类适配器 & 对象适配器
类适配器:使用继承关系来实现
对象适配器:使用组合方式来实现
怎么选择实现方式?
// 类适配器: 基于继承
public interface ITarget {
void f1();
void f2();
void fc();
}
public class Adaptee {
public void fa() { //... }
public void fb() { //... }
public void fc() { //... }
}
public class Adaptor extends Adaptee implements ITarget {
public void f1() {
super.fa();
}
public void f2() {
//...重新实现f2()...
}
// 这里fc()不需要实现,直接继承自Adaptee,这是跟对象适配器最大的不同点
}
// 对象适配器:基于组合
public interface ITarget {
void f1();
void f2();
void fc();
}
public class Adaptee {
public void fa() { //... }
public void fb() { //... }
public void fc() { //... }
}
public class Adaptor implements ITarget {
private Adaptee adaptee;
public Adaptor(Adaptee adaptee) {
this.adaptee = adaptee;
}
public void f1() {
adaptee.fa(); //委托给Adaptee
}
public void f2() {
//...重新实现f2()...
}
public void fc() {
adaptee.fc();
}
}
适配器的使用场景
尽管四种模式的代码结构相似,但是他们要解决的问题、应用场景完全不同。
代理模式
代理模式在不改变原始类接口的条件下,为原始类定义一个代理类,主要目的是控制访问,而非加强功能,这是它跟装饰器模式最大的不同。
桥接模式
桥接模式的目的是将接口部分和实现部分分离,从而让它们可以较为容易、也相对独立地加以改变。
装饰器模式
装饰者模式在不改变原始类接口的情况下,对原始类功能进行增强,并且支持多个装饰器的嵌套使用。
适配器模式
适配器模式是一种事后的补救策略。适配器提供跟原始类不同的接口,而代理模式、装饰器模式提供的都是跟原始类相同的接口。
门面模式为子系统提供一组统一的接口,定义一组高层接口让子系统更易用。
门面模式 VS 适配器模式
适配器模式:接口转换,解决的是原接口和目标接口不匹配的问题。
门面模式:接口整合,解决的是多接口调用带来的问题。
组合模式的设计思路,与其说是一种设计模式,倒不如说是对业务场景的一种数据结构和算法的抽象。其中,数据可以表示成树这种数据结构,业务需求可以通过在树上的递归遍历算法来实现。
享元,顾名思义就是被共享的单元。享元模式的意图就是复用对象,节省内存,前提是享元对象是不可变对象。
例子
棋牌游戏(象棋),在游戏大厅里,有成千上万个房间,对应成千上万个棋局,每个棋局需要保存几十个棋子的信息,包括棋子的类型(象、车、士等),棋子的颜色,棋子的位置,如果不用享元模式,那保存这么多棋局对象就会消耗大量的内存。在上述的实现方式中,在内存中会有大量的相似对象,这些相似对象的id/text/color都是一样的,只是positionX/positionY不一样,那我们可以把前面的属性抽出来,设计成独立的类,并且作为享元供多个棋局复用。
// 享元类
public class ChessPieceUnit {
private int id;
private String text;
private Color color;
public ChessPieceUnit(int id, String text, Color color) {
this.id = id;
this.text = text;
this.color = color;
}
public static enum Color {
RED, BLACK
}
// ...省略其他属性和getter方法...
}
public class ChessPieceUnitFactory {
private static final Map pieces = new HashMap<>();
static {
pieces.put(1, new ChessPieceUnit(1, "車", ChessPieceUnit.Color.BLACK));
pieces.put(2, new ChessPieceUnit(2,"馬", ChessPieceUnit.Color.BLACK));
//...省略摆放其他棋子的代码...
}
public static ChessPieceUnit getChessPiece(int chessPieceId) {
return pieces.get(chessPieceId);
}
}
public class ChessPiece {
private ChessPieceUnit chessPieceUnit;
private int positionX;
private int positionY;
public ChessPiece(ChessPieceUnit unit, int positionX, int positionY) {
this.chessPieceUnit = unit;
this.positionX = positionX;
this.positionY = positionY;
}
// 省略getter、setter方法
}
public class ChessBoard {
private Map chessPieces = new HashMap<>();
public ChessBoard() {
init();
}
private void init() {
chessPieces.put(1, new ChessPiece(
ChessPieceUnitFactory.getChessPiece(1), 0,0));
chessPieces.put(1, new ChessPiece(
ChessPieceUnitFactory.getChessPiece(2), 1,0));
//...省略摆放其他棋子的代码...
}
public void move(int chessPieceId, int toPositionX, int toPositionY) {
//...省略...
}
}
享元 vs 单例、缓存、对象池
单例模式下,一个类只能创建一个对象;在享元模式下,一个类可以创建多个对象,每个对象被多处代码引用共享,有点类似于单例的变体:多例。从设计意图上看,应用享元模式是为了对象复用,节省内存,而应用多例模式是为了限制对象的个数。
缓存主要是为了提高访问效率,而非复用。
池化技术中的“复用”理解为重复使用,主要是为了节省时间。
观察者模式是为了接耦观察者和被观察者代码。比如用户注册的例子,当一个新的用户注册后,我们需要给用户下发消费券,同时还要发送一封欢迎信,那我们可以定义一个接口,接口包含一个f()方法,对于新用户注册,每当我们需要新增一个事件,可以看成需要新增一个观察者,比如赠送积分,那可以新增一个该接口的实现,在用户注册(调用方法register())时,这些观察者都做出对应的操作,即f()。
实现方式
这个模式在不同场景下可以有不用的实现方式,可以是同步阻塞,观察者和被观察者代码在同一个线程内执行,被观察者一直阻塞,直到所有的观察者代码都执行完成之后,才执行后续的代码。也可以是异步非阻塞,可以在执行f()时,都创建一个新的线程去执行,或者更优雅的方法,比如EventBus。
模板模式的作用:复用和扩展。模板方法模式在一个方法中定义一个算法骨架,并将某些步骤推迟到子类中实现。模板方法模式可以让子类在不改变算法整体结构的情况下,重新定义算法中的某些步骤。
其中,复用指的是,所有的子类可以复用父类中提供的模板方法的代码。扩展指的是,框架通过模板模式提供功能扩展点,让框架用户可以在不修改框架源码的情况下,基于扩展点定制化框架的功能。
策略模式定义一族算法类,将每个算法分别封装起来,让他们可以互相替换。策略模式可以使算法的变化独立于使用他们的客户端(使用算法的代码)。
策略模式用来解耦策略的定义、创建、使用。实际上,一个完整的策略模式就会是由这三个部分组成的。
(mark:公司代码里,handler继承或者扩展一个接口/类,同时实现/重写ableHandle()就是使用了策略模式)
职责链模式将请求和接受解耦,让多个接受对象都有机会处理这个请求。将这些接受对象串成一条链,并沿着这条链传递这个请求,直到链上的某个接受对象能够处理它为止。
有两种实现方式,例子:Handler是所有处理器类的抽象父类,handle()是抽象方法。每个具体的处理器类(HandlerA\HandlerB)的handle()函数的代码结构类似,如果它能处理该请求,就不继续往下传递,如果不能,则交给后面的处理器来处理。
第一种是HandlerChain是处理器链,记录了head和tail,记录tail是为了方便添加处理器。
第二种是HandlerChain类用数组而非链表来保存所有的处理器,在HandlerChain的handle()函数中,一次调用每个处理器的handle()函数。
还有一种变体是,请求会被所有的处理器都处理一遍,不存在中途终止的情况。
应用场景
是状态机的一种实现方式。
状态机又叫有限状态机,它有3个部分组成:状态、事件、动作。
一个完整的迭代器模式,一般会涉及容器和容器迭代器两部分内容。为了达到基于接口而非实现编程的目的,容器又包含容器接口、容器实现类,迭代器又包含迭代器接口、迭代器实现类。容器中需要定义 iterator() 方法,用来创建迭代器。迭代器接口中需要定义 hasNext()、currentItem()、next() 三个最基本的方法。容器对象通过依赖注入传递到迭代器类中。
遍历集合一般有三种方式:for 循环、foreach 循环、迭代器遍历。后两种本质上属于一种,都可以看作迭代器遍历。相对于 for 循环遍历,利用迭代器来遍历有下面三个优势:
使用Iterator遍历集合的同时,如果增删集合元素会发生什么?
在java的Iterator实现里面,当调用next()方法时会去判断modCount和expectedModCount是否相等,如果不相等,会抛出ConcurrentModificationException
异常。