设计模式相关概念
11. 设计模式
11.0 设计原则
-
单一职责原则
不要存在多于一个导致类变更的原因。
总结:一个类只负责一项职责。 -
里氏替换原则
-
子类可以实现父类的抽象方法,但不能覆盖父类的非抽象方法。
-
子类中可以增加自己特有的方法。
-
当子类的方法重载父类的方法时,方法的前置条件(即方法的形参)要比父类方法的输入
参数更宽松。 -
当子类的方法实现父类的抽象方法时,方法的后置条件(即方法的返回值)要比父类更严
格。
总结:所有引用父类的地方必须能透明地使用其子类对象
-
-
依赖倒置原则/面向接口编程
- 高层模块不应该依赖低层模块,二者都应该依赖其抽象;
- 抽象不应该依赖细节;
- 细节应该依赖抽象。
-
接口隔离原则
- 使用多个专门的接口来替代一个统一的接口;
- 一个类对另一个类的依赖应建立在最小的接口上
-
迪米特法则
一个类对自己依赖的类知道的越少越好。也就是说,对于被依赖的类来说,无论逻辑多么复杂,都尽量地的将逻辑封装在类的内部 -
开闭原则
- 对扩展开放,对修改关闭
- 用抽象构建框架,用实现扩展细节
-
合成复用原则/组合优于继承
尽量多使用组合和聚合,尽量少使用甚至不使用继承关系
11.1 创建型模式(5种)
创建型模式的主要关注点是“怎样创建对象?”,它这样可以降低系统的耦合度,使⽤用者不不需要关注对象的创建细节,对象的创建由相关的⼯工⼚厂来完成。就像我们去商场购买商品时,不不需要知道商品是怎么⽣生产出来⼀一样,因为它们由专⻔门的⼚厂商⽣生产。
1. 单例模式:
- 定义
确保某一个类只有一个实例,并自行实例化向整个系统提供这个实例。
- 简介
- 单例模式理解起来不难,典型例子有一个公司只能有一个CEO。它主要是为了保证一个类仅有一个实例,这个类中自己提供一个返回实例的方法,方法中先判断系统是否已经有这个单例,如果有则返回,如果没有则创建。
- 如果创建多个实例会消耗过多的资源或者某种类型的对象只应该有且只有一个时,应该考虑使用单例模式。
写法一、懒汉式写法
public class Singleton {
private static Singleton instance;
//构造函数私有
private Singleton (){
}
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
写法二、DCL(Double Check Lock) 双重校验锁
public class Singleton {
private volatile static Singleton singleton; //使用 volatile 可以禁止 JVM 的指令重排,保证在多线程环境下也能正常运行。
private Singleton (){
}
public static Singleton getSingleton() {
if (singleton == null) { //一重if
synchronized (Singleton.class) {
if (singleton == null) { //两重if
singleton = new Singleton();
}
}
}
return singleton;
}
}
写法三、静态内部类单例模式
public class Singleton {
//当 Singleton 类被加载时,静态内部类 SingletonHolder 没有被加载进内存。只有当调用 getUniqueInstance()方法从而触发 SingletonHolder.INSTANCE 时 SingletonHolder 才会被加载,此时初始化 INSTANCE 实例,并且JVM 能确保 INSTANCE 只被实例化一次。
private Singleton (){
}
public static final Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
}
2. 工厂方法模式:
-
定义
定义一个用于创建对象的接口,让子类决定实例化哪一个类。
-
优点:
- 有用户只需要知道具体工厂的名称就可得到所要的产品,无须知道产品的具体创建过程
- 在系统增加新的产品时只需要添加具体产品类和对应的具体工厂类,无须对原工厂进行任何修改,满足开闭原则
-
缺点:每增加一个产品就要增加一个具体产品类和一个对应的具体工厂类,这增加了系统的复杂度。
-
主要角色:
-
抽象工厂( Abstract Factory):提供了创建产品的接口,调用者通过它访问具体工厂的工厂方法 new Product()来创建产品。
-
具体工厂( ConcreteFactory):主要是实现抽象工厂中的抽象方法,完成具体产品的创建。
-
抽象产品( Product):定乂了产品的规范,描述了产品的主要特性和功能
-
具体产品( Concrete Product):实现了抽象产品角色所定义的接口,由具体工厂来创建,它同具体工厂之间一一对应。
-
-
举例
工厂方法模式的典型例子,自行车分为山地自行车和公路自行车等,当需要买自行车时,我们直接去自行车厂里告诉厂长我们需要的自行车即可。
1、定义一个接口自行车,Bike。
public interface Bike{
void ride();
}
2、定义实现类山地自行车,MBike
public class MBike implements Bike{
@Override
public void draw() {
System.out.println("MBike Rides... ");
}
}
3、定义实现类公路自行车,RBike
public class RBike implements Bike{
@Override
public void draw() {
System.out.println("RBike Rides... ");
}
}
4、工厂类,负责创建对象
public class BikeFactory {
//使用 getShape 方法获取形状类型的对象
public Bike getBike(String bikeType){
if(bikeType == null){
return null;
}
if(bikeType.equalsIgnoreCase("MBike")){
return new MBike();
}else if(shapeType.equalsIgnoreCase("RBike")){
return new RBike();
}
return null;
}
}
5、子类决定实例化哪一个类
public class FactoryPatternDemo {
public static void main(String[] args) {
BikeFactory bikeFactory = new BikeFactory();
Bike bike1 = bikeFactory.getBike("MBike");
bike1.ride();
Bike bike2= bikeFactory.getBike("RBike");
bike2.ride();
}
}
3. 抽象工厂模式:
- 定义
提供一个创建一系列相关或相互依赖对象的接口,而无需指定它们具体的类。
-
简介
- 抽象工厂模式主要解决接口选择的问题,典型例子是手机和电脑问题,假设抽象产品1是手机,具体产品是Android手机和iOS手机,抽象产品2是电脑,具体产品是Windows电脑和Mac电脑,抽象工厂是同时生产手机和电脑的公司,具体工厂是联想公司和苹果公司。
- 抽象工厂模式将考虑多等级产品的生产,将同一个具体工厂所生产的位于不同等级的一组产品称为一个产品族
- 抽象工厂模式是工厂方法模式的升级版本,工厂方法模式只⽣生产一个等级的产品,而抽象工厂模式可生产多个等级的产品。
- 抽象工厂的优点是当一个产品族中的多个对象被设计成一起工作时,它能保证客户端始终只使用同一个产品族中的对象。缺点是产品族扩展非常困难,要增加一个系列的某一产品,既要在抽象工厂里加代码,又要在具体工厂里加代码.
-
主要角色:
- 抽象工厂(Abstract Factory):提供了了创建产品的接口,它包含多个创建产品的方法newProduct() ,可以创建多个不同等级的产品。
- 具体工厂(Concrete Factory):主要是实现抽象工厂中的多个抽象方法,完成具体产品的创建。
- 抽象产品(Product):定义了产品的规范,描述了了产品的主要特性和功能,抽象工厂模式有多个抽象品。
- 具体产品(ConcreteProduct):实现了抽象产品角色所定义的接口,由具体工厂来创建,它 同具体工厂之间是多对一的关系。
4. 建造者模式:
- 定义
将一个复杂的对象分解为多个简单的对象,然后一步一步构建而成。将一个复杂的构建与其表示相分离,使得同样的构建过程可以创建不同的表示。
-
简介
- 建造者模式主要解决在软件系统中一个复杂对象的创建工作。通常一个复杂对象是由各个部分的子对象用一定的算法构成,由于需求的变化,这个复杂对象的各个部分经常面临着剧烈的变化,但是将它们组合在一起的算法却相对稳定。
- 建造者模式的一个典型例子是Android中的AlertDialog的构建过程。还有个例子是计算机的组装,计算机是个复杂的对象,它是有很多零件组装而成,显示器、操作系统,鼠标等,通过创建Builder接口来控制零件的组装过程,这样当组件发生变化时,虽然经过同样的构建过程,但是最后得到的结果不同。
- 建造者( Builder)模式和工厂模式的关注点不同:建造者模式注重零部件的组装过程,而工厂方法模式更注重零部件的创建过程,但两者可以结合使用。
-
主要角色:
- 产品角色( Product):它是包含多个组成部件的复杂对象,由具体建造者来创建其各个滅部件。
- 抽象建造者( Builder):它是一个包含创建产品各个子部件的抽象方法的接口,通常还包含一个返回复杂产品的方法 getResult()
- 具体建造者( Concrete builder):实现 Builder接口,完成复杂产品的各个部件的具体创建方法
- 指挥者( Director):它调用建造者对象中的部件构造与装配方法完成复杂对象的创建,在指挥者中不涉及具体产品的信息。
5. 原型模式:
- 定义
用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。
- 简介
原型模式不难理解,它主要是用在实例创建的时候,因为有的时候我们通过new创建一个对象时可能成本过高,这时我们可以考虑直接通过直接克隆实例快速创建对象。克隆后的实例与原实例内部属性一致。原型模式需要注意一个深拷贝和浅拷贝的问题。
11.2 结构型模式(7种)
结构型模式描述如何将类或对象按某种布局组成更更大的结构。它分为类结构型模式和对象结构型模式,前者采⽤用继承机制来组织接口和类,后者釆用组合或聚合来组合对象。由于组合关系或聚合关系比继承关系耦合度低,满足“合成复用原则”,所以对象结构型模式比类结构型模式具有更大的灵活性。
1. 适配器模式:
- 定义
将一个类的接口转换成另外一个客户希望的接口。适配器模式使得原本由于接口不兼容而不能一起工作的那些类可以一起工作。
-
简介
- 适配器模式包括两种类型,类适配器模式和对象适配器模式。
- 这里主要介绍对象适配器模式,因为它更灵活。适配器模式中主要有三个部分,Target、Adapter和Adaptee,其中Target是目标角色,Adaptee是需要被转换的对象,Adapter就是适配器。例如,现在有个手机充电电压是5v,而插座电压是220v,这时我们可以把充电器看成是Adapter,它将电压进行转化最后得到结果。
-
主要角色:
- 目标( Target)接口:当前系统业务所期待的接口,它可以是抽象类或接口
- 适配者( Adaptee)类:它是被访问和适配的现存组件库中的组件接口。
- 适配器( Adapter)类:它是一个转换器,通过继承或引用适配者的对象,把适配者接口转换成目标接口,让客户按目标接口的格式访问适配者。
// Target 目标接口
public interface Volt5 {
public int getVolt5();
}
// Adaptee 需要被转换的对象
public class Volt220 {
public int getVolt220() {
return 220;
}
}
//Adapter 适配器
public class VoltAdapter implements Volt5 {
Volt220 mVolt220;
public VoltAdapter(Volt220 adaptee) {
mVolt220 = adaptee;
}
public int getVolt220() {
return 220;
}
@Override
public int getVolt5() {
return 5;
}
}
2. 桥接模式:
- 定义
将抽象部分与实现部分分离,使它们都可以独立的变化。用组合关系代替继承关系来实现,从而降低了抽象和实现这两个可变维度的耦合度。
-
简介
- 在软件系统中,某些类型由于自身的逻辑,它具有两个或多个维度的变化,那么如何应对这种“多维度的变化”?这就要使用桥接模式。桥接模式需要重点理解的抽象部分,实现部分,脱耦。
- 一个典型的例子是咖啡加糖问题,抽象部分有Coffee,其下有LargeCoffee,SmallCoffee,实现部分是CoffeeAdd,其下有Sugar,Normal,抽象类Coffee中引用CoffeeAdd,这样CoffeeAdd其实就是一个桥接。
-
主要角色:
-
抽象化( Abstraction)角色:定义抽象类,并包含一个对实现化对象的引用。
-
扩展抽象化( Refined abstraction)角色:是抽象化角色的子类,实现父类中的业务方法,并通过组合关系调用实现化角色中的业务方法
-
实现化( Implementor)角色:定义实现化角色的接口,供扩展抽象化角色调用。
-
具体实现化( Concrete Implementor)角色:给岀实现化角色接口的具体实现
-
3. 装饰模式:
- 定义
动态地给一个对象添加一些额外的职责。就增加功能来说,Decorator模式相比生成子类更为灵活。
-
简介
- 装饰模式是作为现有的类的一个包装,它允许向一个现有的对象添加新的功能,同时又不改变其结构。
- 这种模式创建了一个装饰类,用来包装原有的类,并在保持类方法签名完整性的前提下,提供了额外的功能。
- 装饰模式主要是装饰器类Decorator的设计,它对指定对象进行装饰。
-
主要优点:
- 有采用装饰模式扩展对象的功能比采用继承方式更加灵活。
- 可以设计出多个不同的具体装饰类,创造出多个不同行为的组合。
-
主要缺点是:装饰模式增加了许多子类,如果过度使用会使程序变得很复杂
-
主要角色:
- 抽象构件( Component)角色:定义一个抽象接口以规范准备接收附加责任的对象
- 具体构件( Concrete Component)角色:实现抽象构件,通过裝饰角色为其添加些职责
- 抽象装饰( Decorator)角色:继承抽象构件,并包含具体构件的实例,可以通过其子类扩展具体构件的功能
- 具体装饰( ConcreteDecorator)角色:实现抽象装饰的相关方法,并给具体构件对象添加附加的责任
4. 组合模式:
- 定义
将对象组合成树形结构以表示“部分-整体”的层次结构,使得用户对单个对象和组合对象的使用具有一致性。
- 简介
组合模式理解起来相对简单,典型的例子就是假设公司A,里面有不同的部门,不同的部分下有不同的员工,这样一个部门下的所有员工组合成了一个部门,所有部门组合成了整个公司。
5. 外观模式:
- 定义
为子系统中的一组接口提供一个一致的界面,外观模式定义了一个高层接口,这个接口使得这一子系统更加容易使用。
- 简介
外观模式的一个典型例子是去医院看病,挂号、门诊、划价、取药,让患者或患者家属觉得很复杂,如果有提供接待人员,只让接待人员来处理,就很方便。
6. 享元模式:
- 定义
运用共享技术有效地支持大量细粒度的对象。
- 简介
在有大量对象时,有可能会造成内存溢出,我们把其中共同的部分抽象出来,如果有相同的业务请求,直接返回在内存中已有的对象,避免重新创建。
7. 代理模式:
-
定义
为其他对象提供一种代理以控制对这个对象的访问。
-
简介
- 代理模式主要解决在直接访问对象时带来的问题。
- 举个例子,猪八戒去找高翠兰结果是孙悟空变的,可以这样理解:把高翠兰的外貌抽象出来,高翠兰本人和孙悟空都实现了这个接口,猪八戒访问高翠兰的时候看不出来这个是孙悟空,所以说孙悟空是高翠兰代理类。
-
主要角色:
- 抽象主题( Subject)类:通过接口或抽象类声明真实主题和代理对象实现的业务方法
- 真实主题( Real Subject)类:实现了抽象主题中的具体业务,是代理对象所代表的真实对象,是最终要引用的对象。
- 代理( Proxy)类:提供了与真实主题相同的接口,其内部含有对真实主题的引用,它可以访问、控制或扩展真实主题的功能。
11.3 行为型模式(11种)
- 行为型模式用于描述程序在运行时复杂的流程控制,即描述多个类或对象之间怎样相互协作共同完成单个对象都无法单独完成的任务,它涉及算法与对象间职责的分配。
- 行为型模式分为类行为模式和对象行为模式,前者采用继承机制来在类间分派行为,后者采用组合或聚合在对象间分配行为。由于组合关系或聚合关系比继承关系耦合度低,满足“合成复用原则”,所以对象行为模式比类行为模式具有更大的灵活性。
1. 模板方法模式:
- 定义
一个操作中的算法的框架,而将一些步骤延迟到子类中,使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。
- 例子
- 模板方法模式一个典型例子就是Android中的异步任务类AsyncTask,它对异步任务的执行进行了流程封装,子类继承它时,只需在指定的流程中实现具体的操作即可。
2. 命令模式:
- 定义
将一个请求封装为一个对象,从而可用不同的请求对客户进行参数化;对请求排队或记录请求日志,以及支持可取消的操作
- 简介
命令模式主要是通过调用者调用接受者执行命令,这个模式中需要理解的是三个角色:(1) Receiver 真正的命令执行对象 (2) Command 持有一个对Receiver的引用,调用Receiver的相关方法。(3) Invoker 请求者,持有一个对Command的引用,调用Command的方法执行具体命令。
3. 迭代器模式:
- 定义
提供一种方法顺序访问一个聚合对象中各个元素, 而又不需暴露该对象的内部表示。
- 简介
在Java集合框架中我们知道对于一个指定的集合类,我们可以使用一个特定的Iterator迭代器来对集合中的所有元素进行遍历。这样结合来看,迭代器模式很好理解了。
4. 观察者模式:(发布-订阅模式、模型-视图模式)
-
定义
定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。
-
简介
- 观察者模式可以结合Android中的ListView来理解,ListView关联的适配器Adapter在数据发生变化时会通过notifyDataSetChanged()方法来通知界面刷新。
-
主要角色:
- 抽象主题( Subject)角色:也叫抽象目标类,它提供了一个用于保存观察者对象的聚集类和增加、删除观察者对象的方法,以及通知所有观察者的抽象方法
- 具体主题( Concrete Subject)角色:也叫具体目标类,它实现抽象目标中的通知方法,当具体主题的内部状态发生改变时,通知所有注册过的观察者对象。
- 抽象观察者( observer)角色:它是一个抽象类或接口,它包含了一个更新自己的抽象方法,当接到具体主题的更改通知时被调用。
- 具体观察者( Concrete observer)角色:实现抽象观察者中定乂的抽象方法,以便在得到目标的更改通知时更新自身的状态。
5. 中介者模式:
- 定义
用一个中介对象来封装一系列的对象交互。中介者使各对象不需要显式地相互引用,从而使其耦合松散,而且可以独立地改变它们之间的交互。
- 简介
中介者模式的典型例子就是未加入 WTO 之前各个国家相互贸易,结构复杂,大家都加入WTO后是各个国家通过 WTO 来互相贸易,变得规范。
6. 备忘录模式:
- 定义
用一个中介对象来封装一系列的对象交互。中介者使各对象不需要显式地相互引用,从而使其耦合松散,而且可以独立地改变它们之间的交互。
- 简介
中介者模式的典型例子就是未加入 WTO 之前各个国家相互贸易,结构复杂,大家都加入WTO后是各个国家通过 WTO 来互相贸易,变得规范。
7. 解释器模式:
- 定义
给定一个语言,定义它的文法的一种表示,并定义一个解释器,这个解释器使用该表示来解释语言中的句子。
- 简介
解释器的典型例子是在编译原理中的应用,如果一种特定类型的问题发生的频率足够高,那么可能就值得将该问题的各个实例表述为一个简单语言中的句子。这样就可以构建一个解释器,该解释器通过解释这些句子来解决该问题。
8. 状态模式:
- 定义
允许一个对象在其内部状态改变时改变它的行为。对象看起来似乎修改了它的类。
- 简介
状态模式主要解决对象的行为依赖于它的状态(属性),并且可以根据它的状态改变而改变它的相关行为。典型的例子是一个人在不同的状态下完成一件事的结果可能是不同的。
9. 策略模式:
- 定义
定义一系列的算法,把它们一个个封装起来, 并且使它们可相互替换。本模式使得算法可独立于使用它的客户而变化。
- 简介
从策略模式的定义可以看到它主要是将算法和客户独立开,一个典型的例子是排序算法,我们给定一个数组,输出排序后的结果,但是过程中我们可以采取不同的排序算法,这些算法其实就是策略。
10. 责任链模式:
- 定义
使多个对象都有机会处理请求,从而避免请求的发送者和接收者之间的耦合关系。将这些对象连成一条链,并沿着这条链传递该请求,直到有一个对象处理它为止。
- 简介
责任链模式,避免请求发送者与接收者耦合在一起,让多个对象都有可能接收请求,将这些对象连接成一条链,并且沿着这条链传递请求,直到有对象处理它为止。
11. 访问者模式:
- 定义
封装一些作用于某种数据结构中的各元素的操作。它使你可以在不改变各元素的类的前提下定义作用于这些元素的新操作。
- 简介
访问者模式是一种将数据操作和数据结构分离的设计模式,它通常使用在对象结构比较稳定,但是经常需要在此对象结构上定义新的操作,或者需要对一个对象结构中的对象进行很多不同的并且不相关的操作,而需要避免让这些操作"污染"这些对象的类,使用访问者模式将这些封装到类中。
11.4 设计模式汇总
1. 常见的设计模式你能说一下吗
11.5 Spring中使用到的设计模式
Design Patterns(设计模式) 表示面向对象软件开发中最好的计算机编程实践。 Spring 框架中广泛使用了不同类型的设计模式,下面我们来看看到底有哪些设计模式?
1. 控制反转(IoC
)和依赖注入(DI
)
-
IoC
(Inversion of Control,控制翻转) 是Spring 中一个非常非常重要的概念,它不是什么技术,而是一种解耦的设计思想。它的主要目的是借助于“第三方”(Spring 中的 IOC 容器) 实现具有依赖关系的对象之间的解耦(IOC容易管理对象,你只管使用即可),从而降低代码之间的耦合度。IoC 是一个原则,而不是一个模式,以下模式(但不限于)实现了IoC
原则。 -
Spring IOC 容器就像是一个工厂一样,当我们需要创建一个对象的时候,只需要配置好配置文件/注解即可,完全不用考虑对象是如何被创建出来的。 IOC 容器负责创建对象,将对象连接在一起,配置这些对象,并从创建中处理这些对象的整个生命周期,直到它们被完全销毁。
-
在实际项目中一个 Service 类如果有几百甚至上千个类作为它的底层,我们需要实例化这个 Service,你可能要每次都要搞清这个 Service 所有底层类的构造函数,这可能会把人逼疯。如果利用 IOC 的话,你只需要配置好,然后在需要的地方引用就行了,这大大增加了项目的可维护性且降低了开发难度。关于Spring IOC 的理解,推荐看这一下知乎的一个回答:https://www.zhihu.com/question/23277575/answer/169698662 ,非常不错。
-
控制翻转怎么理解呢? 举个例子:“对象a 依赖了对象 b,当对象 a 需要使用 对象 b的时候必须自己去创建。但是当系统引入了 IOC 容器后, 对象a 和对象 b 之前就失去了直接的联系。这个时候,当对象 a 需要使用 对象 b的时候, 我们可以指定 IOC 容器去创建一个对象b注入到对象 a 中”。 对象 a 获得依赖对象 b 的过程,由主动行为变为了被动行为,控制权翻转,这就是控制反转名字的由来。
-
DI(
Dependecy Inject
,依赖注入)是实现控制反转的一种设计模式,依赖注入就是将实例变量传入到一个对象中去。
2. 工厂设计模式
Spring使用工厂模式可以通过 \(BeanFactory\) 或 \(ApplicationContext\) 创建 bean 对象。
两者对比:
- \(BeanFactory\) :延迟注入(使用到某个 bean 的时候才会注入),相比于\(BeanFactory\) 来说会占用更少的内存,程序启动速度更快。
- \(ApplicationContext\) :容器启动的时候,不管你用没用到,一次性创建所有 bean 。\(BeanFactory\) 仅提供了最基本的依赖注入支持,\(ApplicationContext\) 扩展了 \(BeanFactory\) ,除了有\(BeanFactory\)的功能还有额外更多功能,所以一般开发人员使用\(ApplicationContext\)会更多。
\(ApplicationContext\) 的三个实现类:
- \(ClassPathXmlApplication\):把上下文文件当成类路径资源。
- \(FileSystemXmlApplication\):从文件系统中的 XML 文件载入上下文定义信息。
- \(XmlWebApplicationContext\):从Web系统中的XML文件载入上下文定义信息。
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.FileSystemXmlApplicationContext;
public class App {
public static void main(String[] args) {
ApplicationContext context = new FileSystemXmlApplicationContext(
"C:/work/IOC Containers/springframework.applicationcontext/src/main/resources/bean-factory-config.xml");
HelloApplicationContext obj = (HelloApplicationContext) context.getBean("helloApplicationContext");
obj.getMsg();
}
}
3. 单例设计模式
在我们的系统中,有一些对象其实我们只需要一个,比如说:线程池、缓存、对话框、注册表、日志对象、充当打印机、显卡等设备驱动程序的对象。事实上,这一类对象只能有一个实例,如果制造出多个实例就可能会导致一些问题的产生,比如:程序的行为异常、资源使用过量、或者不一致性的结果。
使用单例模式的好处:
- 对于频繁使用的对象,可以省略创建对象所花费的时间,这对于那些重量级对象而言,是非常可观的一笔系统开销;
- 由于 new 操作的次数减少,因而对系统内存的使用频率也会降低,这将减轻 GC 压力,缩短 GC 停顿时间。
Spring 中 bean 的默认作用域就是 singleton(单例)的。 除了 singleton 作用域,Spring 中 bean 还有下面几种作用域:
- prototype : 每次请求都会创建一个新的 bean 实例。
- request : 每一次HTTP请求都会产生一个新的bean,该bean仅在当前HTTP request内有效。
- session : 每一次HTTP请求都会产生一个新的 bean,该bean仅在当前 HTTP session 内有效。
- global-session: 全局session作用域,仅仅在基于portlet的web应用中才有意义,Spring5已经没有了。Portlet 是能够生成语义代码(例如:HTML)片段的小型Java Web插件。它们基于portlet容器,可以像servlet一样处理HTTP请求。但是,与 servlet 不同,每个 portlet 都有不同的会话
Spring 实现单例的方式:
-
xml : <bean id="userService" class="top.snailclimb.UserService" scope="singleton"/>
-
注解:@Scope(value = "singleton")
Spring 通过 \(ConcurrentHashMap\) 实现单例注册表的特殊方式实现单例模式。Spring 实现单例的核心代码如下
// 通过 ConcurrentHashMap(线程安全) 实现单例注册表
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<String, Object>(64);
public Object getSingleton(String beanName, ObjectFactory<?> singletonFactory) {
Assert.notNull(beanName, "'beanName' must not be null");
synchronized (this.singletonObjects) {
// 检查缓存中是否存在实例
Object singletonObject = this.singletonObjects.get(beanName);
if (singletonObject == null) {
//...省略了很多代码
try {
singletonObject = singletonFactory.getObject();
}
//...省略了很多代码
// 如果实例对象在不存在,我们注册到单例注册表中。
addSingleton(beanName, singletonObject);
}
return (singletonObject != NULL_OBJECT ? singletonObject : null);
}
}
//将对象添加到单例注册表
protected void addSingleton(String beanName, Object singletonObject) {
synchronized (this.singletonObjects) {
this.singletonObjects.put(beanName, (singletonObject != null ? singletonObject : NULL_OBJECT));
}
}
}
4. 代理设计模式
代理模式在 AOP 中的应用
-
AOP(Aspect-Oriented Programming:面向切面编程)能够将那些与业务无关,却为业务模块所共同调用的逻辑或责任(例如事务处理、日志管理、权限控制等)封装起来,便于减少系统的重复代码,降低模块间的耦合度,并有利于未来的可拓展性和可维护性。
-
Spring AOP 就是基于动态代理的,如果要代理的对象,实现了某个接口,那么Spring AOP会使用JDK Proxy,去创建代理对象,而对于没有实现接口的对象,就无法使用 JDK Proxy 去进行代理了,这时候Spring AOP会使用\(Cglib\) ,这时候Spring AOP会使用 \(Cglib\) 生成一个被代理对象的子类来作为代理,如下图所示:
- 使用 AOP 之后我们可以把一些通用功能抽象出来,在需要用到的地方直接使用即可,这样大大简化了代码量。我们需要增加新功能时也方便,这样也提高了系统扩展性。日志功能、事务管理等等场景都用到了 AOP 。
Spring AOP 和 AspectJ AOP 有什么区别?
-
Spring AOP 属于运行时增强,而 AspectJ 是编译时增强。 Spring AOP 基于代理(Proxying),而 AspectJ 基于字节码操作(Bytecode Manipulation)。
-
Spring AOP 已经集成了 AspectJ ,AspectJ 应该算的上是 Java 生态系统中最完整的 AOP 框架了。AspectJ 相比于 Spring AOP 功能更加强大,但是 Spring AOP 相对来说更简单,
-
如果我们的切面比较少,那么两者性能差异不大。但是,当切面太多的话,最好选择 AspectJ ,它比Spring AOP 快很多。
5. 模板方法(行为设计模式)
- 模板方法模式是一种行为设计模式,它定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。
- 模板方法使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤的实现方式。
public abstract class Template {
//这是我们的模板方法
public final void TemplateMethod(){
PrimitiveOperation1();
PrimitiveOperation2();
PrimitiveOperation3();
}
protected void PrimitiveOperation1(){
//当前类实现
}
//被子类实现的方法
protected abstract void PrimitiveOperation2();
protected abstract void PrimitiveOperation3();
}
public class TemplateImpl extends Template {
@Override
public void PrimitiveOperation2() {
//当前类实现
}
@Override
public void PrimitiveOperation3() {
//当前类实现
}
}
- Spring 中 \(jdbcTemplate\)、\(hibernateTemplate\) 等以 Template 结尾的对数据库操作的类,它们就使用到了模板模式。一般情况下,我们都是使用继承的方式来实现模板模式,但是 Spring 并没有使用这种方式,而是使用Callback 模式与模板方法模式配合,既达到了代码复用的效果,同时增加了灵活性。
6. 观察者模式(对象行为型模式)
- 观察者模式是一种对象行为型模式。它表示的是一种对象与对象之间具有依赖关系,当一个对象发生改变的时候,这个对象所依赖的对象也会做出反应。Spring 事件驱动模型就是观察者模式很经典的一个应用。Spring 事件驱动模型非常有用,在很多场景都可以解耦我们的代码。比如我们每次添加商品的时候都需要重新更新商品索引,这个时候就可以利用观察者模式来解决这个问题。
Spring 事件驱动模型中的三种角色
-
事件角色
\(ApplicationEvent\) (\(org.springframework.context\)包下)充当事件的角色,这是一个抽象类,它继承了\(java.util.EventObject\)并实现了 \(java.io.Serializable\)接口。 -
Spring 中默认存在以下事件,他们都是对 \(ApplicationContextEvent\) 的实现(继承自
ApplicationContextEvent
):- \(ContextStartedEvent:ApplicationContext\) 启动后触发的事件;
- \(ContextStoppedEvent:ApplicationContext\) 停止后触发的事件;
- \(ContextRefreshedEvent:ApplicationContext\) 初始化或刷新完成后触发的事件;
- \(ContextClosedEvent:ApplicationContext\) 关闭后触发的事件。
-
事件监听者角色
ApplicationListener
充当了事件监听者角色,它是一个接口,里面只定义了一个onApplicationEvent()
方法来处理ApplicationEvent。ApplicationListener
接口类源码如下,可以看出接口定义看出接口中的事件只要实现了ApplicationEvent
就可以了。所以,在 Spring中我们只要实现ApplicationListener
接口实现onApplicationEvent
() 方法即可完成监听事件 -
事件发布者角色
ApplicationEventPublisher
充当了事件的发布者,它也是一个接口。
ApplicationEventPublisher
接口的publishEvent
()这个方法在AbstractApplicationContext
类中被实现,阅读这个方法的实现,你会发现实际上事件真正是通过ApplicationEventMulticaster
来广播出去的。
Spring 的事件流程总结
- 定义一个事件: 实现一个继承自
ApplicationEvent
,并且写相应的构造函数; - 定义一个事件监听者:实现
ApplicationListener
接口,重写onApplicationEvent
() 方法; - 使用事件发布者发布消息: 可以通过
ApplicationEventPublisher
的publishEvent
() 方法发布消息。
Example:
// 定义一个事件,继承自ApplicationEvent并且写相应的构造函数
public class DemoEvent extends ApplicationEvent{
private static final long serialVersionUID = 1L;
private String message;
public DemoEvent(Object source,String message){
super(source);
this.message = message;
}
public String getMessage() {
return message;
}
// 定义一个事件监听者,实现ApplicationListener接口,重写 onApplicationEvent() 方法;
@Component
public class DemoListener implements ApplicationListener<DemoEvent>{
//使用onApplicationEvent接收消息
@Override
public void onApplicationEvent(DemoEvent event) {
String msg = event.getMessage();
System.out.println("接收到的信息是:"+msg);
}
}
// 发布事件,可以通过ApplicationEventPublisher 的 publishEvent() 方法发布消息。
@Component
public class DemoPublisher {
@Autowired
ApplicationContext applicationContext;
public void publish(String message){
//发布事件
applicationContext.publishEvent(new DemoEvent(this, message));
}
}
当调用 DemoPublisher
的 publish
() 方法的时候,比如 demoPublisher.publish("你好")
,控制台就会打印出:接收到的信息是:你好 。
7. 适配器模式(Wrapper)
适配器模式(Adapter Pattern) 将一个接口转换成客户希望的另一个接口,适配器模式使接口不兼容的那些类可以一起工作,其别名为包装器(Wrapper)。
spring AOP中的适配器模式
-
我们知道 Spring AOP 的实现是基于代理模式,但是 Spring AOP 的增强或通知(Advice)使用到了适配器模式,与之相关的接口是
AdvisorAdapter
。 -
Advice 常用的类型有:
BeforeAdvice
(目标方法调用前,前置通知)、AfterAdvice
(目标方法调用后,后置通知)、AfterReturningAdvice
(目标方法执行结束后,return之前)等等。 -
每个类型Advice(通知)都有对应的拦截器:
MethodBeforeAdviceInterceptor
、AfterReturningAdviceAdapter
、AfterReturningAdviceInterceptor
。Spring预定义的通知要通过对应的适配器,适配成MethodInterceptor
接口(方法拦截器)类型的对象(如:MethodBeforeAdviceInterceptor
负责适配MethodBeforeAdvice
)。
spring MVC中的适配器模式
-
在Spring MVC中,
DispatcherServlet
根据请求信息调用HandlerMapping
,解析请求对应的 Handler。解析到对应的 Handler(也就是我们平常说的 Controller 控制器)后,开始由HandlerAdapter
适配器处理。HandlerAdapter
作为期望接口,具体的适配器实现类用于对目标类进行适配,Controller 作为需要适配的类。 -
为什么要在 Spring MVC 中使用适配器模式? Spring MVC 中的 Controller 种类众多,不同类型的 Controller 通过不同的方法来对请求进行处理。如果不利用适配器模式的话,
DispatcherServlet
直接获取对应类型的 Controller,需要的自行来判断,像下面这段代码一样:
if(mappedHandler.getHandler() instanceof MultiActionController){
((MultiActionController)mappedHandler.getHandler()).xxx
}else if(mappedHandler.getHandler() instanceof XXX){
...
}else if(...){
...
}
- 假如我们再增加一个 Controller类型就要在上面代码中再加入一行 判断语句,这种形式就使得程序难以维护,也违反了设计模式中的开闭原则 – 对扩展开放,对修改关闭。
8. 装饰者模式
-
装饰者模式可以动态地给对象添加一些额外的属性或行为。相比于使用继承,装饰者模式更加灵活。简单点儿说就是当我们需要修改原有的功能,但我们又不愿直接去修改原有的代码时,设计一个Decorator套在原有代码外面。
-
其实在 JDK 中就有很多地方用到了装饰者模式,比如
InputStream
家族,InputStream
类下有FileInputStream
(读取文件)、BufferedInputStream
(增加缓存,使读取文件速度大大提升)等子类都在不修改InputStream
代码的情况下扩展了它的功能。 -
Spring 中配置
DataSource
的时候,DataSource
可能是不同的数据库和数据源。我们能否根据客户的需求在少修改原有类的代码下动态切换不同的数据源?这个时候就要用到装饰者模式(这一点我自己还没太理解具体原理)。Spring 中用到的包装器模式在类名上含有 Wrapper或者 Decorator。这些类基本上都是动态地给一个对象添加一些额外的职责。
总结
Spring 框架中用到了哪些设计模式?
- 工厂设计模式 : Spring使用工厂模式通过
BeanFactory
、ApplicationContext
创建 bean 对象。 - 代理设计模式 : Spring AOP 功能的实现。(依赖注入)
- 单例设计模式 : Spring 中的 Bean 默认都是单例的。
- 模板方法模式 : Spring 中
jdbcTemplate
、hibernateTemplate
等以 Template 结尾的对数据库操作的类,它们就使用到了模板模式。 - 包装器设计模式 : 我们的项目需要连接多个数据库,而且不同的客户在每次访问中根据需要会去访问不同的数据库。这种模式让我们可以根据客户的需求能够动态切换不同的数据源。
- 观察者模式: Spring 事件驱动模型就是观察者模式很经典的一个应用。
- 适配器模式 :Spring AOP 的增强或通知(Advice)使用到了适配器模式、spring MVC 中也是用到了适配器模式适配Controller。
Spring中的设计模式
控制反转(IoC)和依赖注入(DI)
IoC(Inversion of Control,控制翻转) 是Spring 中一个非常非常重要的概念,它不是什么技术,而是一种解耦的设计思想。它的主要目的是借助于“第三方”(Spring 中的 IOC 容器) 实现具有依赖关系的对象之间的解耦(IOC容易管理对象,你只管使用即可),从而降低代码之间的耦合度。IOC 是一个原则,而不是一个模式,以下模式(但不限于)实现了IoC原则。
Spring IOC 容器就像是一个工厂一样,当我们需要创建一个对象的时候,只需要配置好配置文件/注解即可,完全不用考虑对象是如何被创建出来的。 IOC 容器负责创建对象,将对象连接在一起,配置这些对象,并从创建中处理这些对象的整个生命周期,直到它们被完全销毁。
在实际项目中一个 Service 类如果有几百甚至上千个类作为它的底层,我们需要实例化这个 Service,你可能要每次都要搞清这个 Service 所有底层类的构造函数,这可能会把人逼疯。如果利用 IOC 的话,你只需要配置好,然后在需要的地方引用就行了,这大大增加了项目的可维护性且降低了开发难度。关于Spring IOC 的理解,推荐看这一下知乎的一个回答:https://www.zhihu.com/question/23277575/answer/169698662 ,非常不错。
控制翻转怎么理解呢? 举个例子:"对象a 依赖了对象 b,当对象 a 需要使用 对象 b的时候必须自己去创建。但是当系统引入了 IOC 容器后, 对象a 和对象 b 之前就失去了直接的联系。这个时候,当对象 a 需要使用 对象 b的时候, 我们可以指定 IOC 容器去创建一个对象b注入到对象 a 中"。 对象 a 获得依赖对象 b 的过程,由主动行为变为了被动行为,控制权翻转,这就是控制反转名字的由来。
DI(Dependecy Inject,依赖注入)是实现控制反转的一种设计模式,依赖注入就是将实例变量传入到一个对象中去。
工厂设计模式
Spring使用工厂模式可以通过 BeanFactory
或 ApplicationContext
创建 bean 对象。
两者对比:
BeanFactory
:延迟注入(使用到某个 bean 的时候才会注入),相比于ApplicationContext
来说会占用更少的内存,程序启动速度更快。ApplicationContext
:容器启动的时候,不管你用没用到,一次性创建所有 bean 。BeanFactory
仅提供了最基本的依赖注入支持,ApplicationContext
扩展了BeanFactory
,除了有BeanFactory
的功能还有额外更多功能,所以一般开发人员使用ApplicationContext
会更多。
ApplicationContext的三个实现类:
ClassPathXmlApplication
:把上下文文件当成类路径资源。FileSystemXmlApplication
:从文件系统中的 XML 文件载入上下文定义信息。XmlWebApplicationContext
:从Web系统中的XML文件载入上下文定义信息。
Example:
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.FileSystemXmlApplicationContext;
public class App {
public static void main(String[] args) {
ApplicationContext context = new FileSystemXmlApplicationContext(
"C:/work/IOC Containers/springframework.applicationcontext/src/main/resources/bean-factory-config.xml");
HelloApplicationContext obj = (HelloApplicationContext) context.getBean("helloApplicationContext");
obj.getMsg();
}
}
单例设计模式
在我们的系统中,有一些对象其实我们只需要一个,比如说:线程池、缓存、对话框、注册表、日志对象、充当打印机、显卡等设备驱动程序的对象。事实上,这一类对象只能有一个实例,如果制造出多个实例就可能会导致一些问题的产生,比如:程序的行为异常、资源使用过量、或者不一致性的结果。
使用单例模式的好处:
- 对于频繁使用的对象,可以省略创建对象所花费的时间,这对于那些重量级对象而言,是非常可观的一笔系统开销;
- 由于 new 操作的次数减少,因而对系统内存的使用频率也会降低,这将减轻 GC 压力,缩短 GC 停顿时间。
Spring 中 bean 的默认作用域就是 singleton(单例)的。 除了 singleton 作用域,Spring 中 bean 还有下面几种作用域:
- prototype : 每次请求都会创建一个新的 bean 实例。
- request : 每一次HTTP请求都会产生一个新的bean,该bean仅在当前HTTP request内有效。
- session : 每一次HTTP请求都会产生一个新的 bean,该bean仅在当前 HTTP session 内有效。
- global-session: 全局session作用域,仅仅在基于portlet的web应用中才有意义,Spring5已经没有了。Portlet是能够生成语义代码(例如:HTML)片段的小型Java Web插件。它们基于portlet容器,可以像servlet一样处理HTTP请求。但是,与 servlet 不同,每个 portlet 都有不同的会话
Spring 实现单例的方式:
- xml :
<bean id="userService" class="top.snailclimb.UserService" scope="singleton"/>
- 注解:
@Scope(value = "singleton")
Spring 通过 ConcurrentHashMap
实现单例注册表的特殊方式实现单例模式。Spring 实现单例的核心代码如下
// 通过 ConcurrentHashMap(线程安全) 实现单例注册表
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<String, Object>(64);
public Object getSingleton(String beanName, ObjectFactory<?> singletonFactory) {
Assert.notNull(beanName, "'beanName' must not be null");
synchronized (this.singletonObjects) {
// 检查缓存中是否存在实例
Object singletonObject = this.singletonObjects.get(beanName);
if (singletonObject == null) {
//...省略了很多代码
try {
singletonObject = singletonFactory.getObject();
}
//...省略了很多代码
// 如果实例对象在不存在,我们注册到单例注册表中。
addSingleton(beanName, singletonObject);
}
return (singletonObject != NULL_OBJECT ? singletonObject : null);
}
}
//将对象添加到单例注册表
protected void addSingleton(String beanName, Object singletonObject) {
synchronized (this.singletonObjects) {
this.singletonObjects.put(beanName, (singletonObject != null ? singletonObject : NULL_OBJECT));
}
}
}
代理设计模式
代理模式在 AOP 中的应用
AOP(Aspect-Oriented Programming:面向切面编程)能够将那些与业务无关,却为业务模块所共同调用的逻辑或责任(例如事务处理、日志管理、权限控制等)封装起来,便于减少系统的重复代码,降低模块间的耦合度,并有利于未来的可拓展性和可维护性。
Spring AOP 就是基于动态代理的,如果要代理的对象,实现了某个接口,那么Spring AOP会使用JDK Proxy,去创建代理对象,而对于没有实现接口的对象,就无法使用 JDK Proxy 去进行代理了,这时候Spring AOP会使用Cglib ,这时候Spring AOP会使用 Cglib 生成一个被代理对象的子类来作为代理,如下图所示:
当然你也可以使用 AspectJ ,Spring AOP 已经集成了AspectJ ,AspectJ 应该算的上是 Java 生态系统中最完整的 AOP 框架了。
使用 AOP 之后我们可以把一些通用功能抽象出来,在需要用到的地方直接使用即可,这样大大简化了代码量。我们需要增加新功能时也方便,这样也提高了系统扩展性。日志功能、事务管理等等场景都用到了 AOP 。
Spring AOP 和 AspectJ AOP 有什么区别?
Spring AOP 属于运行时增强,而 AspectJ 是编译时增强。 Spring AOP 基于代理(Proxying),而 AspectJ 基于字节码操作(Bytecode Manipulation)。
Spring AOP 已经集成了 AspectJ ,AspectJ 应该算的上是 Java 生态系统中最完整的 AOP 框架了。AspectJ 相比于 Spring AOP 功能更加强大,但是 Spring AOP 相对来说更简单,
如果我们的切面比较少,那么两者性能差异不大。但是,当切面太多的话,最好选择 AspectJ ,它比Spring AOP 快很多。
模板方法
模板方法模式是一种行为设计模式,它定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。 模板方法使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤的实现方式。
public abstract class Template {
//这是我们的模板方法
public final void TemplateMethod(){
PrimitiveOperation1();
PrimitiveOperation2();
PrimitiveOperation3();
}
protected void PrimitiveOperation1(){
//当前类实现
}
//被子类实现的方法
protected abstract void PrimitiveOperation2();
protected abstract void PrimitiveOperation3();
}
public class TemplateImpl extends Template {
@Override
public void PrimitiveOperation2() {
//当前类实现
}
@Override
public void PrimitiveOperation3() {
//当前类实现
}
}
Spring 中 jdbcTemplate
、hibernateTemplate
等以 Template 结尾的对数据库操作的类,它们就使用到了模板模式。一般情况下,我们都是使用继承的方式来实现模板模式,但是 Spring 并没有使用这种方式,而是使用Callback 模式与模板方法模式配合,既达到了代码复用的效果,同时增加了灵活性。
观察者模式
观察者模式是一种对象行为型模式。它表示的是一种对象与对象之间具有依赖关系,当一个对象发生改变的时候,这个对象所依赖的对象也会做出反应。Spring 事件驱动模型就是观察者模式很经典的一个应用。Spring 事件驱动模型非常有用,在很多场景都可以解耦我们的代码。比如我们每次添加商品的时候都需要重新更新商品索引,这个时候就可以利用观察者模式来解决这个问题。
Spring 事件驱动模型中的三种角色
事件角色
ApplicationEvent
(org.springframework.context
包下)充当事件的角色,这是一个抽象类,它继承了java.util.EventObject
并实现了 java.io.Serializable
接口。
Spring 中默认存在以下事件,他们都是对 ApplicationContextEvent
的实现(继承自ApplicationContextEvent
):
ContextStartedEvent
:ApplicationContext
启动后触发的事件;ContextStoppedEvent
:ApplicationContext
停止后触发的事件;ContextRefreshedEvent
:ApplicationContext
初始化或刷新完成后触发的事件;ContextClosedEvent
:ApplicationContext
关闭后触发的事件。
事件监听者角色
ApplicationListener
充当了事件监听者角色,它是一个接口,里面只定义了一个 onApplicationEvent()
方法来处理ApplicationEvent
。ApplicationListener
接口类源码如下,可以看出接口定义看出接口中的事件只要实现了 ApplicationEvent
就可以了。所以,在 Spring中我们只要实现 ApplicationListener
接口实现 onApplicationEvent()
方法即可完成监听事件
package org.springframework.context;
import java.util.EventListener;
@FunctionalInterface
public interface ApplicationListener<E extends ApplicationEvent> extends EventListener {
void onApplicationEvent(E var1);
}
事件发布者角色
ApplicationEventPublisher
充当了事件的发布者,它也是一个接口。
@FunctionalInterface
public interface ApplicationEventPublisher {
default void publishEvent(ApplicationEvent event) {
this.publishEvent((Object)event);
}
void publishEvent(Object var1);
}
ApplicationEventPublisher
接口的publishEvent()
这个方法在AbstractApplicationContext
类中被实现,阅读这个方法的实现,你会发现实际上事件真正是通过ApplicationEventMulticaster
来广播出去的。具体内容过多,就不在这里分析了,后面可能会单独写一篇文章提到。
Spring 的事件流程总结
- 定义一个事件: 实现一个继承自
ApplicationEvent
,并且写相应的构造函数; - 定义一个事件监听者:实现
ApplicationListener
接口,重写onApplicationEvent()
方法; - 使用事件发布者发布消息: 可以通过
ApplicationEventPublisher
的publishEvent()
方法发布消息。
Example:
// 定义一个事件,继承自ApplicationEvent并且写相应的构造函数
public class DemoEvent extends ApplicationEvent{
private static final long serialVersionUID = 1L;
private String message;
public DemoEvent(Object source,String message){
super(source);
this.message = message;
}
public String getMessage() {
return message;
}
// 定义一个事件监听者,实现ApplicationListener接口,重写 onApplicationEvent() 方法;
@Component
public class DemoListener implements ApplicationListener<DemoEvent>{
//使用onApplicationEvent接收消息
@Override
public void onApplicationEvent(DemoEvent event) {
String msg = event.getMessage();
System.out.println("接收到的信息是:"+msg);
}
}
// 发布事件,可以通过ApplicationEventPublisher 的 publishEvent() 方法发布消息。
@Component
public class DemoPublisher {
@Autowired
ApplicationContext applicationContext;
public void publish(String message){
//发布事件
applicationContext.publishEvent(new DemoEvent(this, message));
}
}
当调用 DemoPublisher
的 publish()
方法的时候,比如 demoPublisher.publish("你好")
,控制台就会打印出:接收到的信息是:你好
。
适配器模式
适配器模式(Adapter Pattern) 将一个接口转换成客户希望的另一个接口,适配器模式使接口不兼容的那些类可以一起工作,其别名为包装器(Wrapper)。
spring AOP中的适配器模式
我们知道 Spring AOP 的实现是基于代理模式,但是 Spring AOP 的增强或通知(Advice)使用到了适配器模式,与之相关的接口是AdvisorAdapter
。Advice 常用的类型有:BeforeAdvice
(目标方法调用前,前置通知)、AfterAdvice
(目标方法调用后,后置通知)、AfterReturningAdvice
(目标方法执行结束后,return之前)等等。每个类型Advice(通知)都有对应的拦截器:MethodBeforeAdviceInterceptor
、AfterReturningAdviceAdapter
、AfterReturningAdviceInterceptor
。Spring预定义的通知要通过对应的适配器,适配成 MethodInterceptor
接口(方法拦截器)类型的对象(如:MethodBeforeAdviceInterceptor
负责适配 MethodBeforeAdvice
)。
spring MVC中的适配器模式
在Spring MVC中,DispatcherServlet
根据请求信息调用 HandlerMapping
,解析请求对应的 Handler
。解析到对应的 Handler
(也就是我们平常说的 Controller
控制器)后,开始由HandlerAdapter
适配器处理。HandlerAdapter
作为期望接口,具体的适配器实现类用于对目标类进行适配,Controller
作为需要适配的类。
为什么要在 Spring MVC 中使用适配器模式? Spring MVC 中的 Controller
种类众多,不同类型的 Controller
通过不同的方法来对请求进行处理。如果不利用适配器模式的话,DispatcherServlet
直接获取对应类型的 Controller
,需要的自行来判断,像下面这段代码一样:
if(mappedHandler.getHandler() instanceof MultiActionController){
((MultiActionController)mappedHandler.getHandler()).xxx
}else if(mappedHandler.getHandler() instanceof XXX){
...
}else if(...){
...
}
假如我们再增加一个 Controller
类型就要在上面代码中再加入一行 判断语句,这种形式就使得程序难以维护,也违反了设计模式中的开闭原则 – 对扩展开放,对修改关闭。
装饰者模式
装饰者模式可以动态地给对象添加一些额外的属性或行为。相比于使用继承,装饰者模式更加灵活。简单点儿说就是当我们需要修改原有的功能,但我们又不愿直接去修改原有的代码时,设计一个Decorator套在原有代码外面。其实在 JDK 中就有很多地方用到了装饰者模式,比如 InputStream
家族,InputStream
类下有 FileInputStream
(读取文件)、BufferedInputStream
(增加缓存,使读取文件速度大大提升)等子类都在不修改InputStream
代码的情况下扩展了它的功能。
Spring 中配置 DataSource 的时候,DataSource 可能是不同的数据库和数据源。我们能否根据客户的需求在少修改原有类的代码下动态切换不同的数据源?这个时候就要用到装饰者模式(这一点我自己还没太理解具体原理)。Spring 中用到的包装器模式在类名上含有 Wrapper
或者 Decorator
。这些类基本上都是动态地给一个对象添加一些额外的职责
总结
Spring 框架中用到了哪些设计模式?
- 工厂设计模式 : Spring使用工厂模式通过
BeanFactory
、ApplicationContext
创建 bean 对象。 - 代理设计模式 : Spring AOP 功能的实现。
- 单例设计模式 : Spring 中的 Bean 默认都是单例的。
- 模板方法模式 : Spring 中
jdbcTemplate
、hibernateTemplate
等以 Template 结尾的对数据库操作的类,它们就使用到了模板模式。 - 包装器设计模式 : 我们的项目需要连接多个数据库,而且不同的客户在每次访问中根据需要会去访问不同的数据库。这种模式让我们可以根据客户的需求能够动态切换不同的数据源。
- 观察者模式: Spring 事件驱动模型就是观察者模式很经典的一个应用。
- 适配器模式 :Spring AOP 的增强或通知(Advice)使用到了适配器模式、spring MVC 中也是用到了适配器模式适配
Controller
。
代理模式详解
1. 代理模式
代理模式是一种比较好的理解的设计模式。简单来说就是 我们使用代理对象来代替对真实对象(real object)的访问,这样就可以在不修改原目标对象的前提下,提供额外的功能操作,扩展目标对象的功能。
代理模式的主要作用是扩展目标对象的功能,比如说在目标对象的某个方法执行前后你可以增加一些自定义的操作。
举个例子:你的找了一小红来帮你问话,小红就看作是代理我的代理对象,代理的行为(方法)是问话。
https://medium.com/@mithunsasidharan/understanding-the-proxy-design-pattern-5e63fe38052a
代理模式有静态代理和动态代理两种实现方式,我们 先来看一下静态代理模式的实现。
2. 静态代理
静态代理中,我们对目标对象的每个方法的增强都是手动完成的(后面会具体演示代码),非常不灵活(比如接口一旦新增加方法,目标对象和代理对象都要进行修改)且麻烦(需要对每个目标类都单独写一个代理类)。 实际应用场景非常非常少,日常开发几乎看不到使用静态代理的场景。
上面我们是从实现和应用角度来说的静态代理,从 JVM 层面来说, 静态代理在编译时就将接口、实现类、代理类这些都变成了一个个实际的 class 文件。
静态代理实现步骤:
- 定义一个接口及其实现类;
- 创建一个代理类同样实现这个接口
- 将目标对象注入进代理类,然后在代理类的对应方法调用目标类中的对应方法。这样的话,我们就可以通过代理类屏蔽对目标对象的访问,并且可以在目标方法执行前后做一些自己想做的事情。
下面通过代码展示!
1.定义发送短信的接口
public interface SmsService {
String send(String message);
}
2.实现发送短信的接口
public class SmsServiceImpl implements SmsService {
public String send(String message) {
System.out.println("send message:" + message);
return message;
}
}
3.创建代理类并同样实现发送短信的接口
public class SmsProxy implements SmsService {
private final SmsService smsService;
public SmsProxy(SmsService smsService) {
this.smsService = smsService;
}
@Override
public String send(String message) {
//调用方法之前,我们可以添加自己的操作
System.out.println("before method send()");
smsService.send(message);
//调用方法之后,我们同样可以添加自己的操作
System.out.println("after method send()");
return null;
}
}
4.实际使用
public class Main {
public static void main(String[] args) {
SmsService smsService = new SmsServiceImpl();
SmsProxy smsProxy = new SmsProxy(smsService);
smsProxy.send("java");
}
}
运行上述代码之后,控制台打印出:
before method send()
send message:java
after method send()
可以输出结果看出,我们已经增加了 SmsServiceImpl
的send()
方法。
3. 动态代理
相比于静态代理来说,动态代理更加灵活。我们不需要针对每个目标类都单独创建一个代理类,并且也不需要我们必须实现接口,我们可以直接代理实现类( CGLIB 动态代理机制)。
从 JVM 角度来说,动态代理是在运行时动态生成类字节码,并加载到 JVM 中的。
说到动态代理,Spring AOP、RPC 框架应该是两个不得不的提的,它们的实现都依赖了动态代理。
动态代理在我们日常开发中使用的相对较小,但是在框架中的几乎是必用的一门技术。学会了动态代理之后,对于我们理解和学习各种框架的原理也非常有帮助。
就 Java 来说,动态代理的实现方式有很多种,比如 JDK 动态代理、CGLIB 动态代理等等。
guide-rpc-framework 使用的是 JDK 动态代理,我们先来看看 JDK 动态代理的使用。
另外,虽然 guide-rpc-framework 没有用到 CGLIB 动态代理 ,我们这里还是简单介绍一下其使用以及和JDK 动态代理的对比。
3.1. JDK 动态代理机制
3.1.1. 介绍
在 Java 动态代理机制中 InvocationHandler
接口和 Proxy
类是核心。
Proxy
类中使用频率最高的方法是:newProxyInstance()
,这个方法主要用来生成一个代理对象。
public static Object newProxyInstance(ClassLoader loader,
Class<?>[] interfaces,
InvocationHandler h)
throws IllegalArgumentException
{
......
}
这个方法一共有 3 个参数:
- loader :类加载器,用于加载代理对象。
- interfaces : 被代理类实现的一些接口;
- h : 实现了
InvocationHandler
接口的对象;
要实现动态代理的话,还必须需要实现InvocationHandler
来自定义处理逻辑。 当我们的动态代理对象调用一个方法时候,这个方法的调用就会被转发到实现InvocationHandler
接口类的 invoke
方法来调用。
public interface InvocationHandler {
/**
* 当你使用代理对象调用方法的时候实际会调用到这个方法
*/
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable;
}
invoke()
方法有下面三个参数:
- proxy :动态生成的代理类
- method : 与代理类对象调用的方法相对应
- args : 当前 method 方法的参数
也就是说:你通过Proxy
类的 newProxyInstance()
创建的代理对象在调用方法的时候,实际会调用到实现InvocationHandler
接口的类的 invoke()
方法。 你可以在 invoke()
方法中自定义处理逻辑,比如在方法执行前后做什么事情。
3.1.2. JDK 动态代理类使用步骤
- 定义一个接口及其实现类;
- 自定义
InvocationHandler
并重写invoke
方法,在invoke
方法中我们会调用原生方法(被代理类的方法)并自定义一些处理逻辑; - 通过
Proxy.newProxyInstance(ClassLoader loader,Class[] interfaces,InvocationHandler h)
方法创建代理对象;
3.1.3. 代码示例
这样说可能会有点空洞和难以理解,我上个例子,大家感受一下吧!
1.定义发送短信的接口
public interface SmsService {
String send(String message);
}
2.实现发送短信的接口
public class SmsServiceImpl implements SmsService {
public String send(String message) {
System.out.println("send message:" + message);
return message;
}
}
3.定义一个 JDK 动态代理类
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
/**
* @author shuang.kou
* @createTime 2020年05月11日 11:23:00
*/
public class DebugInvocationHandler implements InvocationHandler {
/**
* 代理类中的真实对象
*/
private final Object target;
public DebugInvocationHandler(Object target) {
this.target = target;
}
public Object invoke(Object proxy, Method method, Object[] args) throws InvocationTargetException, IllegalAccessException {
//调用方法之前,我们可以添加自己的操作
System.out.println("before method " + method.getName());
Object result = method.invoke(target, args);
//调用方法之后,我们同样可以添加自己的操作
System.out.println("after method " + method.getName());
return result;
}
}
invoke()
方法: 当我们的动态代理对象调用原生方法的时候,最终实际上调用到的是 invoke()
方法,然后 invoke()
方法代替我们去调用了被代理对象的原生方法。
4.获取代理对象的工厂类
public class JdkProxyFactory {
public static Object getProxy(Object target) {
return Proxy.newProxyInstance(
target.getClass().getClassLoader(), // 目标类的类加载
target.getClass().getInterfaces(), // 代理需要实现的接口,可指定多个
new DebugInvocationHandler(target) // 代理对象对应的自定义 InvocationHandler
);
}
}
getProxy()
:主要通过Proxy.newProxyInstance()
方法获取某个类的代理对象
5.实际使用
DebugProxy debugProxy = new DebugProxy(new SmsServiceImpl());
SmsService smsService = debugProxy.getProxy(SmsService.class);
smsService.send("java");
运行上述代码之后,控制台打印出:
before method send
send message:java
after method send
3.2. CGLIB 动态代理机制
3.2.1. 介绍
JDK 动态代理有一个最致命的问题是其只能代理实现了接口的类。
为了解决这个问题,我们可以用 CGLIB 动态代理机制来避免。
CGLIB(Code Generation Library)是一个基于ASM的字节码生成库,它允许我们在运行时对字节码进行修改和动态生成。CGLIB 通过继承方式实现代理。很多知名的开源框架都使用到了CGLIB, 例如 Spring 中的 AOP 模块中:如果目标对象实现了接口,则默认采用 JDK 动态代理,否则采用 CGLIB 动态代理。
在 CGLIB 动态代理机制中 MethodInterceptor
接口和 Enhancer
类是核心。
你需要自定义 MethodInterceptor
并重写 intercept
方法,intercept
用于拦截增强被代理类的方法。
public interface MethodInterceptor
extends Callback{
// 拦截被代理类中的方法
public Object intercept(Object obj, java.lang.reflect.Method method, Object[] args,
MethodProxy proxy) throws Throwable;
}
- obj :被代理的对象(需要增强的对象)
- method :被拦截的方法(需要增强的方法)
- args :方法入参
- methodProxy :用于调用原始方法
你可以通过 Enhancer
类来动态获取被代理类,当代理类调用方法的时候,实际调用的是 MethodInterceptor
中的 intercept
方法。
3.2.2. CGLIB 动态代理类使用步骤
- 定义一个类;
- 自定义
MethodInterceptor
并重写intercept
方法,intercept
用于拦截增强被代理类的方法,和 JDK 动态代理中的invoke
方法类似; - 通过
Enhancer
类的create()
创建代理类;
3.2.3. 代码示例
不同于 JDK 动态代理不需要额外的依赖。CGLIB(Code Generation Library) 实际是属于一个开源项目,如果你要使用它的话,需要手动添加相关依赖。
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>3.3.0</version>
</dependency>
1.实现一个使用阿里云发送短信的类
package github.javaguide.dynamicProxy.cglibDynamicProxy;
public class AliSmsService {
public String send(String message) {
System.out.println("send message:" + message);
return message;
}
}
2.自定义 MethodInterceptor
(方法拦截器)
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
import java.lang.reflect.Method;
/**
* 自定义MethodInterceptor
*/
public class DebugMethodInterceptor implements MethodInterceptor {
/**
* @param o 被代理的对象(需要增强的对象)
* @param method 被拦截的方法(需要增强的方法)
* @param args 方法入参
* @param methodProxy 用于调用原始方法
*/
@Override
public Object intercept(Object o, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
//调用方法之前,我们可以添加自己的操作
System.out.println("before method " + method.getName());
Object object = methodProxy.invokeSuper(o, args);
//调用方法之后,我们同样可以添加自己的操作
System.out.println("after method " + method.getName());
return object;
}
}
3.获取代理类
import net.sf.cglib.proxy.Enhancer;
public class CglibProxyFactory {
public static Object getProxy(Class<?> clazz) {
// 创建动态代理增强类
Enhancer enhancer = new Enhancer();
// 设置类加载器
enhancer.setClassLoader(clazz.getClassLoader());
// 设置被代理类
enhancer.setSuperclass(clazz);
// 设置方法拦截器
enhancer.setCallback(new DebugMethodInterceptor());
// 创建代理类
return enhancer.create();
}
}
4.实际使用
AliSmsService aliSmsService = (AliSmsService) CglibProxyFactory.getProxy(AliSmsService.class);
aliSmsService.send("java");
运行上述代码之后,控制台打印出:
before method send
send message:java
after method send
3.3. JDK 动态代理和 CGLIB 动态代理对比
- JDK 动态代理只能只能代理实现了接口的类,而 CGLIB 可以代理未实现任何接口的类。 另外, CGLIB 动态代理是通过生成一个被代理类的子类来拦截被代理类的方法调用,因此不能代理声明为 final 类型的类和方法。
- 就二者的效率来说,大部分情况都是 JDK 动态代理更优秀,随着 JDK 版本的升级,这个优势更加明显。
4. 静态代理和动态代理的对比
- 灵活性 :动态代理更加灵活,不需要必须实现接口,可以直接代理实现类,并且可以不需要针对每个目标类都创建一个代理类。另外,静态代理中,接口一旦新增加方法,目标对象和代理对象都要进行修改,这是非常麻烦的!
- JVM 层面 :静态代理在编译时就将接口、实现类、代理类这些都变成了一个个实际的 class 文件。而动态代理是在运行时动态生成类字节码,并加载到 JVM 中的。
单例模式详解
一 单例模式简介
1.1 定义
保证一个类仅有一个实例,并提供一个访问它的全局访问点。
1.2 为什么要用单例模式呢?
在我们的系统中,有一些对象其实我们只需要一个,比如说:线程池、缓存、对话框、注册表、日志对象、充当打印机、显卡等设备驱动程序的对象。事实上,这一类对象只能有一个实例,如果制造出多个实例就可能会导致一些问题的产生,比如:程序的行为异常、资源使用过量、或者不一致性的结果。
简单来说使用单例模式可以带来下面几个好处:
- 对于频繁使用的对象,可以省略创建对象所花费的时间,这对于那些重量级对象而言,是非常可观的一笔系统开销;
- 由于 new 操作的次数减少,因而对系统内存的使用频率也会降低,这将减轻 GC 压力,缩短 GC 停顿时间。
1.3 为什么不使用全局变量确保一个类只有一个实例呢?
我们知道全局变量分为静态变量和实例变量,静态变量也可以保证该类的实例只存在一个。
只要程序加载了类的字节码,不用创建任何实例对象,静态变量就会被分配空间,静态变量就可以被使用了。
但是,如果说这个对象非常消耗资源,而且程序某次的执行中一直没用,这样就造成了资源的浪费。利用单例模式的话,我们就可以实现在需要使用时才创建对象,这样就避免了不必要的资源浪费。 不仅仅是因为这个原因,在程序中我们要尽量避免全局变量的使用,大量使用全局变量给程序的调试、维护等带来困难。
二 单例的模式的实现
通常单例模式在Java语言中,有两种构建方式:
- 饿汉方式。指全局的单例实例在类装载时构建
- 懒汉方式。指全局的单例实例在第一次被使用时构建。
不管是那种创建方式,它们通常都存在下面几点相似处:
- 单例类必须要有一个 private 访问级别的构造函数,只有这样,才能确保单例不会在系统中的其他代码内被实例化;
- instance 成员变量和 uniqueInstance 方法必须是 static 的。
2.1 饿汉方式(线程安全)
public class Singleton {
//在静态初始化器中创建单例实例,这段代码保证了线程安全
private static Singleton uniqueInstance = new Singleton();
//Singleton类只有一个构造方法并且是被private修饰的,所以用户无法通过new方法创建该对象实例
private Singleton(){}
public static Singleton getInstance(){
return uniqueInstance;
}
}
所谓 “饿汉方式” 就是说JVM在加载这个类时就马上创建此唯一的单例实例,不管你用不用,先创建了再说,如果一直没有被使用,便浪费了空间,典型的空间换时间,每次调用的时候,就不需要再判断,节省了运行时间。
2.2 懒汉式(非线程安全和synchronized关键字线程安全版本 )
public class Singleton {
private static Singleton uniqueInstance;
private Singleton (){
}
//没有加入synchronized关键字的版本是线程不安全的
public static Singleton getInstance() {
//判断当前单例是否已经存在,若存在则返回,不存在则再建立单例
if (uniqueInstance == null) {
uniqueInstance = new Singleton();
}
return uniqueInstance;
}
}
所谓 “ 懒汉式” 就是说单例实例在第一次被使用时构建,而不是在JVM在加载这个类时就马上创建此唯一的单例实例。
但是上面这种方式很明显是线程不安全的,如果多个线程同时访问getInstance()方法时就会出现问题。如果想要保证线程安全,一种比较常见的方式就是在getInstance() 方法前加上synchronized关键字,如下:
public static synchronized Singleton getInstance() {
if (instance == null) {
uniqueInstance = new Singleton();
}
return uniqueInstance;
}
我们知道synchronized关键字偏重量级锁。虽然在JavaSE1.6之后synchronized关键字进行了主要包括:为了减少获得锁和释放锁带来的性能消耗而引入的偏向锁和轻量级锁以及其它各种优化之后执行效率有了显著提升。
但是在程序中每次使用getInstance() 都要经过synchronized加锁这一层,这难免会增加getInstance()的方法的时间消费,而且还可能会发生阻塞。我们下面介绍到的 双重检查加锁版本 就是为了解决这个问题而存在的。
2.3 懒汉式(双重检查加锁版本)
利用双重检查加锁(double-checked locking),首先检查是否实例已经创建,如果尚未创建,“才”进行同步。这样以来,只有一次同步,这正是我们想要的效果。
public class Singleton {
//volatile保证,当uniqueInstance变量被初始化成Singleton实例时,多个线程可以正确处理uniqueInstance变量
private volatile static Singleton uniqueInstance;//使用 volatile 可以禁止 JVM 的指令重排,保证在多线程环境下也能正常运行。
private Singleton() {
}
public static Singleton getInstance() {
//检查实例,如果不存在,就进入同步代码块
if (uniqueInstance == null) {
//只有第一次才彻底执行这里的代码
synchronized(Singleton.class) {
//进入同步代码块后,再检查一次,如果仍是null,才创建实例
if (uniqueInstance == null) {
uniqueInstance = new Singleton();
}
}
}
return uniqueInstance;
}
}
很明显,这种方式相比于使用synchronized关键字的方法,可以大大减少getInstance() 的时间消费。
我们上面使用到了volatile关键字来保证数据的可见性,关于volatile关键字的内容可以看我的这篇文章:
《Java多线程学习(三)volatile关键字》: https://blog.csdn.net/qq_34337272/article/details/79680771
注意: 双重检查加锁版本不适用于1.4及更早版本的Java。
1.4及更早版本的Java中,许多JVM对于volatile关键字的实现会导致双重检查加锁的失效。
2.4 懒汉式(登记式/静态内部类方式)
静态内部实现的单例是懒加载的且线程安全。
只有通过显式调用 getInstance 方法时,才会显式装载 SingletonHolder 类,从而实例化 instance(只有第一次使用这个单例的实例的时候才加载,同时不会有线程安全问题)。
public class Singleton {
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
private Singleton (){}
public static final Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
2.5 饿汉式(枚举方式)
这种实现方式还没有被广泛采用,但这是实现单例模式的最佳方法。 它更简洁,自动支持序列化机制,绝对防止多次实例化 (如果单例类实现了Serializable接口,默认情况下每次反序列化总会创建一个新的实例对象,关于单例与序列化的问题可以查看这一篇文章《单例与序列化的那些事儿》),同时这种方式也是《Effective Java 》以及《Java与模式》的作者推荐的方式。
public enum Singleton {
//定义一个枚举的元素,它就是 Singleton 的一个实例
INSTANCE;
public void doSomeThing() {
System.out.println("枚举方法实现单例");
}
}
使用方法:
public class ESTest {
public static void main(String[] args) {
Singleton singleton = Singleton.INSTANCE;
singleton.doSomeThing();//output:枚举方法实现单例
}
}
工厂模式详解
一 工厂模式介绍
1.1 工厂模式的定义
先来看一下GOF为工厂模式的定义:
“Define an interface for creating an object, but let subclasses decide which class to instantiate. Factory Method lets a class defer instantiation to subclasses.”(在基类中定义创建对象的一个接口,让子类决定实例化哪个类。工厂方法让一个类的实例化延迟到子类中进行。)
1.2 工厂模式的分类:
(1)简单工厂(Simple Factory)模式,又称静态工厂方法模式(Static Factory Method Pattern)。
(2)工厂方法(Factory Method)模式,又称多态性工厂(Polymorphic Factory)模式或虚拟构造子(Virtual Constructor)模式;
(3)抽象工厂(Abstract Factory)模式,又称工具箱(Kit 或Toolkit)模式。
1.3 在开源框架中的使用
举两个比较常见的例子(我暂时可以准确想到的,当然还有很多很多):
(1)Spring中通过getBean(“xxx”)获取Bean;
(2) Java消息服务JMS中(下面以消息队列ActiveMQ为例子)
关于消息队列ActiveMQ的使用可以查看:消息队列ActiveMQ的使用详解
// 1、创建一个连接工厂对象,需要指定服务的ip及端口。
ConnectionFactory connectionFactory = new ActiveMQConnectionFactory("tcp://192.168.25.155:61616");
// 2、使用工厂对象创建一个Connection对象。
1.4 为什么要用工厂模式
(1) 解耦 :把对象的创建和使用的过程分开
(2)降低代码重复: 如果创建某个对象的过程都很复杂,需要一定的代码量,而且很多地方都要用到,那么就会有很多的重复代码。
(3) 降低维护成本 :由于创建过程都由工厂统一管理,所以发生业务逻辑变化,不需要找到所有需要创建对象B的地方去逐个修正,只需要在工厂里修改即可,降低维护成本。
关于工厂模式的作用,Mark一篇文章:https://blog.csdn.net/lovelion/article/details/7523392
二 简单工厂模式
2.1 介绍
严格的说,简单工厂模式并不是23种常用的设计模式之一,它只算工厂模式的一个特殊实现。简单工厂模式在实际中的应用相对于其他2个工厂模式用的还是相对少得多,因为它只适应很多简单的情况。
最重要的是它违背了我们在概述中说的 开放-封闭原则 (虽然可以通过反射的机制来避免,后面我们会介绍到) 。因为每次你要新添加一个功能,都需要在生switch-case 语句(或者if-else 语句)中去修改代码,添加分支条件。
2.2 适用场景
(1)需要创建的对象较少。
(2)客户端不关心对象的创建过程。
2.3 简单工厂模式角色分配:
- 工厂(Factory)角色 :简单工厂模式的核心,它负责实现创建所有实例的内部逻辑。工厂类可以被外界直接调用,创建所需的产品对象。
- 抽象产品(Product)角色 :简单工厂模式所创建的所有对象的父类,它负责描述所有实例所共有的公共接口。
- 具体产品(Concrete Product)角色:简单工厂模式的创建目标,所有创建的对象都是充当这个角色的某个具体类的实例。
2.4 简单工厂实例
创建一个可以绘制不同形状的绘图工具,可以绘制圆形,正方形,三角形,每个图形都会有一个draw()方法用于绘图.
(1)创建Shape接口
public interface Shape {
void draw();
}
(2)创建实现该接口的具体图形类
圆形
public class Circle implements Shape {
public Circle() {
System.out.println("Circle");
}
@Override
public void draw() {
System.out.println("Draw Circle");
}
}
长方形
public class Rectangle implements Shape {
public Rectangle() {
System.out.println("Rectangle");
}
@Override
public void draw() {
System.out.println("Draw Rectangle");
}
}
正方形
public class Square implements Shape {
public Square() {
System.out.println("Square");
}
@Override
public void draw() {
System.out.println("Draw Square");
}
}
(3)创建工厂类:
public class ShapeFactory {
// 使用 getShape 方法获取形状类型的对象
public static Shape getShape(String shapeType) {
if (shapeType == null) {
return null;
}
if (shapeType.equalsIgnoreCase("CIRCLE")) {
return new Circle();
} else if (shapeType.equalsIgnoreCase("RECTANGLE")) {
return new Rectangle();
} else if (shapeType.equalsIgnoreCase("SQUARE")) {
return new Square();
}
return null;
}
}
(4)测试方法:
public class Test {
public static void main(String[] args) {
// 获取 Circle 的对象,并调用它的 draw 方法
Shape circle = ShapeFactory.getShape("CIRCLE");
circle.draw();
// 获取 Rectangle 的对象,并调用它的 draw 方法
Shape rectangle = ShapeFactory.getShape("RECTANGLE");
rectangle.draw();
// 获取 Square 的对象,并调用它的 draw 方法
Shape square = ShapeFactory.getShape("SQUARE");
square.draw();
}
}
输出结果:
Circle
Draw Circle
Rectangle
Draw Rectangle
Square
Draw Square
这样的实现有个问题,如果我们新增产品类的话,就需要修改工厂类中的getShape()方法,这很明显不符合 开放-封闭原则 。
2.5 使用反射机制改善简单工厂
将工厂类改为下面的形式:
package factory_pattern;
/**
* 利用反射解决简单工厂每次增加新了产品类都要修改产品工厂的弊端
*
* @author Administrator
*
*/
public class ShapeFactory2 {
public static Object getClass(Class<? extends Shape> clazz) {
Object obj = null;
try {
obj = Class.forName(clazz.getName()).newInstance();
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
return obj;
}
}
测试方法:
package factory_pattern;
public class Test2 {
public static void main(String[] args) {
Circle circle = (Circle) ShapeFactory2.getClass(factory_pattern.Circle.class);
circle.draw();
Rectangle rectangle = (Rectangle) ShapeFactory2.getClass(factory_pattern.Rectangle.class);
rectangle.draw();
Square square = (Square) ShapeFactory2.getClass(factory_pattern.Square.class);
square.draw();
}
}
这种方式的虽然符合了 开放-关闭原则 ,但是每一次传入的都是产品类的全部路径,这样比较麻烦。如果需要改善的话可以通过 反射+配置文件 的形式来改善,这种方式使用的也是比较多的。
3 工厂方法模式(每个对象都有与之对应的工厂)
3.1 介绍
工厂方法模式应该是在工厂模式家族中是用的最多模式,一般项目中存在最多的就是这个模式。
工厂方法模式是简单工厂的仅一步深化, 在工厂方法模式中,我们不再提供一个统一的工厂类来创建所有的对象,而是针对不同的对象提供不同的工厂。也就是说 每个对象都有一个与之对应的工厂 。
3.2 适用场景
- 一个类不知道它所需要的对象的类:在工厂方法模式中,客户端不需要知道具体产品类的类名,只需要知道所对应的工厂即可,具体的产品对象由具体工厂类创建;客户端需要知道创建具体产品的工厂类。
- 一个类通过其子类来指定创建哪个对象:在工厂方法模式中,对于抽象工厂类只需要提供一个创建产品的接口,而由其子类来确定具体要创建的对象,利用面向对象的多态性和里氏
- 将创建对象的任务委托给多个工厂子类中的某一个,客户端在使用时可以无需关心是哪一个工厂子类创建产品子类,需要时再动态指定,可将具体工厂类的类名存储在配置文件或数据库中。
3.3 工厂方法模式角色分配:
- 抽象工厂(Abstract Factory)角色:是工厂方法模式的核心,与应用程序无关。任何在模式中创建的对象的工厂类必须实现这个接口。
- 具体工厂(Concrete Factory)角色 :这是实现抽象工厂接口的具体工厂类,包含与应用程序密切相关的逻辑,并且受到应用程序调用以创建某一种产品对象。
- 抽象产品(AbstractProduct)角色 :工厂方法模式所创建的对象的超类型,也就是产品对象的共同父类或共同拥有的接口。
- 具体产品(Concrete Product)角色 :这个角色实现了抽象产品角色所定义的接口。某具体产品有专门的具体工厂创建,它们之间往往一一对应
3.4 工厂方法模式实例
上面简单工厂例子中的图形接口以及相关图像实现类不变。我们只需要增加一个工厂接口以及实现这个接口的工厂类即可。
(1)增加一个工厂接口:
public interface Factory {
public Shape getShape();
}
(2)增加相关工厂类:
圆形工厂类
public class CircleFactory implements Factory {
@Override
public Shape getShape() {
// TODO Auto-generated method stub
return new Circle();
}
}
长方形工厂类
public class RectangleFactory implements Factory{
@Override
public Shape getShape() {
// TODO Auto-generated method stub
return new Rectangle();
}
}
圆形工厂类
public class SquareFactory implements Factory{
@Override
public Shape getShape() {
// TODO Auto-generated method stub
return new Square();
}
}
(3)测试:
public class Test {
public static void main(String[] args) {
Factory circlefactory = new CircleFactory();
Shape circle = circlefactory.getShape();
circle.draw();
}
}
输出结果:
Circle
Draw Circle
4 抽象工厂模式
4.1 介绍
在工厂方法模式中,其实我们有一个潜在意识的意识。那就是我们生产的都是同一类产品。抽象工厂模式是工厂方法的进一步深化,在这个模式中的工厂类不单单可以创建一种产品,而是可以创建一组产品。
抽象工厂应该是比较最难理解的一个工厂模式了。
4.2 适用场景
- 和工厂方法一样客户端不需要知道它所创建的对象的类。
- 需要一组对象共同完成某种功能时,并且可能存在多组对象完成不同功能的情况。(同属于同一个产品族的产品)
- 系统结构稳定,不会频繁的增加对象。(因为一旦增加就需要修改原有代码,不符合开闭原则)
4.3 抽象工厂方法模式角色分配:
- 抽象工厂(AbstractFactory)角色 :是工厂方法模式的核心,与应用程序无关。任何在模式中创建的对象的工厂类必须实现这个接口。
- 具体工厂类(ConreteFactory)角色 :这是实现抽象工厂接口的具体工厂类,包含与应用程序密切相关的逻辑,并且受到应用程序调用以创建某一种产品对象。
- 抽象产品(Abstract Product)角色 :工厂方法模式所创建的对象的超类型,也就是产品对象的共同父类或共同拥有的接口。
- 具体产品(Concrete Product)角色 :抽象工厂模式所创建的任何产品对象都是某一个具体产品类的实例。在抽象工厂中创建的产品属于同一产品族,这不同于工厂模式中的工厂只创建单一产品,我后面也会详解介绍到。
4.4 抽象工厂的工厂和工厂方法中的工厂有什么区别呢?
抽象工厂是生产一整套有产品的(至少要生产两个产品),这些产品必须相互是有关系或有依赖的,而工厂方法中的工厂是生产单一产品的工厂。
下面就是抽象工厂图示:
4.5 抽象工厂模式实例
不知道大家玩过穿越火线或者吃鸡这类游戏了吗,游戏中存在各种枪。我们假设现在存在AK、M4A1两类枪,每一种枪对应一种子弹。我们现在这样考虑生产AK的工厂可以顺便生产AK使用的子弹,生产M4A1的工厂可以顺便生产M4A1使用的子弹。(AK工厂生产AK系列产品包括子弹啊,AK枪的类型啊这些,M4A1工厂同理)
(1)创建相关接口:
枪
public interface Gun {
public void shooting();
}
子弹
public interface Bullet {
public void load();
}
(2)创建接口对应实现类:
AK类
public class AK implements Gun{
@Override
public void shooting() {
System.out.println("shooting with AK");
}
}
M4A1类
public class M4A1 implements Gun {
@Override
public void shooting() {
System.out.println("shooting with M4A1");
}
}
AK子弹类
public class AK_Bullet implements Bullet {
@Override
public void load() {
System.out.println("Load bullets with AK");
}
}
M4A1子弹类
public class M4A1
_Bullet implements Bullet {
@Override
public void load() {
System.out.println("Load bullets with M4A1");
}
}
(3)创建工厂接口
public interface Factory {
public Gun produceGun();
public Bullet produceBullet();
}
(4)创建具体工厂
生产AK和AK子弹的工厂
public class AK_Factory implements Factory{
@Override
public Gun produceGun() {
return new AK();
}
@Override
public Bullet produceBullet() {
return new AK_Bullet();
}
}
生产M4A1和M4A1子弹的工厂
public class M4A1_Factory implements Factory{
@Override
public Gun produceGun() {
return new M4A1();
}
@Override
public Bullet produceBullet() {
return new M4A1_Bullet();
}
}
(5)测试
public class Test {
public static void main(String[] args) {
Factory factory;
Gun gun;
Bullet bullet;
factory =new AK_Factory();
bullet=factory.produceBullet();
bullet.load();
gun=factory.produceGun();
gun.shooting();
}
}
输出结果:
Load bullets with AK
shooting with AK
适配器模式详解
适配器模式(Adapter Pattern)
属于结构型模式
的一种,把一个类的接口变成客户端所期待的另一种接口,从而使原本接口不匹配而无法一起工作的两个类能够在一起工作...
概述
当你想使用一个已经存在的类,而它的接口不符合你的需求,或者你想创建一个可重用的类(与不兼容接口无关的类),这时候可以考虑使用适配器模式
。同时它也是一种包装模式,它与装饰模式
同样具有包装的功能。
案例
笔者算是小米的忠实用户了,从大学期间起至今都是购买的小米,期间发现小米5
在推出的时候,它会送一个type-c
的转接口给我们,那会type-c
数据线应该还不算普及,这种做法还是蛮好的,在使用转接口后Micro USB
得以重复利用,这样一来即使原装的米5数据线丢了也没关系,只要有type-c
转接口,一样可以用Micro USB
充电/连接电脑
类适配器
1.首先定义M4DataLine
代表是Micro USB
,我们目的就是通过适配器能够用米4数据线连接米5手机
class M4DataLine {
public void connection() {
System.out.println("使用小米4数据线连接...");
}
}
2.定义客户端使用的接口,与业务相关
interface Target {
void connection();
}
class M5DataLine implements Target {
@Override
public void connection() {
System.out.println("使用小米5数据线连接...");
}
}
3.创建适配器类,继承了被适配类,同时实现标准接口
class M5DataLineAdapter extends M4DataLine implements Target {
@Override
public void connection() {
System.out.println("插入 type-c 转接头");
super.connection();
}
}
4.客户端代码,测试
public class AdapterMain {
public static void main(String[] args) {
Target target = new M5DataLine();
target.connection();
Target adapter = new M5DataLineAdapter();
adapter.connection();
}
}
5.结果
使用小米5数据线连接...
插入 type-c 转接头
使用小米4数据线连接...
对象适配器
创建适配器类,实现标准接口,将这个调用委托给实现新接口的对象来处理
class M5DataLineAdapter implements Target {
private Target target;
public M5DataLineAdapter(Target target) {
this.target = target;
}
@Override
public void connection() {
System.out.println("插入 type-c 转接头");
target.connection();
}
}
public class AdapterMain {
public static void main(String[] args) {
// 使用特殊功能类,即适配类
Target adapter = new M5DataLineAdapter(new M5DataLine());
adapter.connection();
}
}
区别
类适配器:对象继承的方式,静态的定义。
对象适配器:依赖于对象的组合,都是采用对象组合的方式,也就是对象适配器实现的方式。
JDK 中的适配器使用
使用适配器模式的类
java.util.Arrays#asList()
java.io.InputStreamReader(InputStream)
java.io.OutputStreamWriter(OutputStream)
Java I/O 库大量使用了适配器模式,如 ByteArrayInputStream
是一个适配器类,它继承了 InputStream
的接口,并且封装了一个 byte 数组。换言之,它将一个 byte 数组的接口适配成 InputStream 流处理器的接口。
在 OutputStream
类型中,所有的原始流处理器都是适配器类。ByteArrayOutputStream
继承了 OutputStream
类型,同时持有一个对 byte 数组的引用。它一个 byte 数组的接口适配成 OutputString 类型的接口,因此也是一个对象形式的适配器模式的应用。
FileOutputStream
继承了 OutputStream
类型,同时持有一个对 FileDiscriptor
对象的引用。这是一个将 FileDiscriptor
接口适配成 OutputStream
接口形式的对象型适配器模式。
Reader
类型的原始流处理器都是适配器模式的应用。StringReader
是一个适配器类,StringReader
类继承了 Reader
类型,持有一个对 String 对象的引用。它将 String 的接口适配成 Reader
类型的接口。
Spring 中使用适配器模式的典型应用
在 Spring 的 AOP 里通过使用的 Advice(通知)来增强被代理类的功能。Spring 实现这一 AOP 功能的原理就使用代理模式(1、JDK 动态代理。2、CGLib 字节码生成技术代理。)对类进行方法级别的切面增强,即,生成被代理类的代理类,并在代理类的方法前,设置拦截器,通过执行拦截器中的内容增强了代理方法的功能,实现的面向切面编程。
Advice(通知)的类型有:BeforeAdvice、AfterReturningAdvice、ThrowSadvice 等。每个类型 Advice(通知)都有对应的拦截器,MethodBeforeAdviceInterceptor、AfterReturningAdviceInterceptor、ThrowsAdviceInterceptor。Spring 需要将每个 Advice(通知)都封装成对应的拦截器类型,返回给容器,所以需要使用适配器模式对 Advice 进行转换。
public interface MethodBeforeAdvice extends BeforeAdvice {
void before(Method method, Object[] args, Object target) throws Throwable;
}
public interface AdvisorAdapter {
boolean supportsAdvice(Advice advice);
MethodInterceptor getInterceptor(Advisor advisor);
}
class MethodBeforeAdviceAdapter implements AdvisorAdapter, Serializable {
@Override
public boolean supportsAdvice(Advice advice) {
return (advice instanceof MethodBeforeAdvice);
}
@Override
public MethodInterceptor getInterceptor(Advisor advisor) {
MethodBeforeAdvice advice = (MethodBeforeAdvice) advisor.getAdvice();
return new MethodBeforeAdviceInterceptor(advice);
}
}
默认的适配器注册表
public class DefaultAdvisorAdapterRegistry implements AdvisorAdapterRegistry, Serializable {
private final List<AdvisorAdapter> adapters = new ArrayList<AdvisorAdapter>(3);
public DefaultAdvisorAdapterRegistry() {
// 注册适配器
registerAdvisorAdapter(new MethodBeforeAdviceAdapter());
registerAdvisorAdapter(new AfterReturningAdviceAdapter());
registerAdvisorAdapter(new ThrowsAdviceAdapter());
}
@Override
public Advisor wrap(Object adviceObject) throws UnknownAdviceTypeException {
if (adviceObject instanceof Advisor) {
return (Advisor) adviceObject;
}
if (!(adviceObject instanceof Advice)) {
throw new UnknownAdviceTypeException(adviceObject);
}
Advice advice = (Advice) adviceObject;
if (advice instanceof MethodInterceptor) {
// So well-known it doesn't even need an adapter.
return new DefaultPointcutAdvisor(advice);
}
for (AdvisorAdapter adapter : this.adapters) {
// 检查是否支持,这里调用了适配器的方法
if (adapter.supportsAdvice(advice)) {
return new DefaultPointcutAdvisor(advice);
}
}
throw new UnknownAdviceTypeException(advice);
}
@Override
public MethodInterceptor[] getInterceptors(Advisor advisor) throws UnknownAdviceTypeException {
List<MethodInterceptor> interceptors = new ArrayList<MethodInterceptor>(3);
Advice advice = advisor.getAdvice();
if (advice instanceof MethodInterceptor) {
interceptors.add((MethodInterceptor) advice);
}
for (AdvisorAdapter adapter : this.adapters) {
// 检查是否支持,这里调用了适配器的方法
if (adapter.supportsAdvice(advice)) {
interceptors.add(adapter.getInterceptor(advisor));
}
}
if (interceptors.isEmpty()) {
throw new UnknownAdviceTypeException(advisor.getAdvice());
}
return interceptors.toArray(new MethodInterceptor[interceptors.size()]);
}
@Override
public void registerAdvisorAdapter(AdvisorAdapter adapter) {
this.adapters.add(adapter);
}
}
总结
优点
- 可以让任何两个没有关联的类一起运行
- 提高了类的复用,想使用现有的类,而此类的接口标准又不符合现有系统的需要。通过适配器模式就可以让这些功能得到更好的复用。
- 增加了类的透明度,客户端只关注结果
- 使用适配器的时候,可以调用自己开发的功能,从而自然地扩展系统的功能。
缺点
- 过多使用会导致系统凌乱,追溯困难(内部转发导致,调用A适配成B)
适用场景
- 系统需要使用一些现有的类,而这些类的接口(如方法名)不符合系统的需要,甚至没有这些类的源代码。
- 想创建一个可以重复使用的类,用于与一些彼此之间没有太大关联的一些类,包括一些可能在将来引进的类一起工作。
小故事
魏文王问名医扁鹊说:“你们家兄弟三人,都精于医术,到底哪一位最好呢?”
扁鹊答:“大哥最好,二哥次之,我最差。”
文王再问:“那么为什么你最出名呢?”
扁鹊答说:“我大哥治病,是治病于病情发作之前。由于一般人不知道他率先能铲除病因,所以他的名气无法传出去,只有我们家的人才知道。我二哥治病,是治病于病情初起之时。一般人以为他只能治轻微的小病,所以他的名气只及于本乡里。而我扁鹊治病,是治病于病情严重之时。一般人都看到我在经脉上穿针管来放血、在皮肤上敷药等大手术,所以以为我的医术高明,名气因此响遍全国。”
比较起来,能防范于未然是最高明的,但往往因防范在前,不会出现恶果,使事物保持了原态,没有“明显”的功绩而被忽略。正如不见防火英雄,只有救火英雄一样。高明者不见得一定名声显赫。
建议尽量使用对象的适配器模式,少用继承。适配器模式也是一种包装模式,它与装饰模式同样具有包装的功能,此外,对象适配器模式
还具有委托的意思。总的来说,适配器模式属于补偿模式,专门用来在系统后期扩展、修改时使用,但要注意不要过度使用适配器模式。