[TOC]
1. 设计模式概述
1. 1 软件设计模式的概念
软件设计模式(Software Design Pattern), 又称设计模式,是一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结。它描述了在软件设计过程中的,一些不断重复发生的问题,以及该问题的解决方案。也就是说,它是解决特定问题的一系列套路,是前辈们的代码设计总结的经验,具有一定的普遍性,可以反复使用。
1.2 学习设计模式的必要性
设计模式的本质是面向对象设计原则的实际运用,是对类的封装性、继承性和多态性以及类的关联关系和组合关系的充分理解。
正确使用设计模式具有以下优点:
- 可以提高程序员的思维能力、编程能力和设计能力
- 是程序设计更加标准化、代码编制更加工程化,使软件开发效率大大提高,从而缩短软件的开发周期
- 使设计的代码的可重用性高、可读性强、可靠性高、灵活性好、可维护性强
1.3 设计模式分类
创建型模式
用于描述“怎样创建对象”,它的主要特点是“将对象的创建和使用分离”。GOF(四人组)书中提供了单例、原型、工厂方法、抽象工厂、建造者等5种创建型模式。
结构型模式
用于描述如何将类或对象按某种布局组成更大的结构,GOF书中提供了代理、适配器、桥接、装饰、外观、享元、组合等7种结构型模式。
行为型模式
用于描述类或对象之间怎样相互协作共同完成单个对象无法单独完成的任务,以及怎样分配职责。GOF书中提供了模版方法、策略、命令、职责链、状态、观察者、中介者、迭代器、访问者、备忘录、解释器等11种行为型模式。
2. UML 类图
统一建模语言(Unified Modeling Language, UML)是用来设计软件的可视化建模语言。它的特点是简单、统一、图形化、能表达软件设计中的动态与静态信息。
UML从目标系统的不同角度出发,定义了用例图、类图、对象图、状态图、活动图、时序图、协作图、构件图、部署图等9种图。
2.1 类图概述
类图(Class diagram)是显示了模型的静态结构,特别是模型中存在的类、类的内部结构以及它们与其他类的关系等。类图不显示暂时性的信息。类图是面向对象建模的主要组成部分。
2.2 类图的作用
- 在软件工程中,类图是一种静态的结构图,描述了系统的类的集合,类的属性和类之间的关系,可以简化了人们对系统的误解;
- 类图是系统分析和设计阶段的重要产物,是系统编码和测试的重要模型。
2.3 类图的表示法
2.3.1 类的表示方式
在UML类图中,类使用包含类名、属性(field)和方法(method)且带有分割线的矩形来表示,比如下图表示一个Employee类,它包含name.age和address这三个属性,以及work()方法。
属性/方法名称前加的加号和减号表示了这个属性/方法的可见性,UML类图中表示可见性的符号有三种:
- +:表示public
- -:表示private
- #:表示protected
属性的完整表示方式是: 可见性 名称 : 类型 [ = 缺省值 ]
方法的完整表示方式是: 可见性 名称(参数列表) [ : 返回类型 ]
注意:
- 中括号中的内容表示是可选的
- 也有将类型放在变量名前面,返回值类型放在方法名前面
举个栗子:
上图Demo类中定义了三个方法:
- method() 方法:修饰符为public, 没有参数,没有返回值
- method1() 方法:修饰符为private, 没有参数,返回值为String
- method2()方法:修饰符为protected, 接收两个参数,第一个参数类型为int, 第二个参数类型为String,返回值类型是int
2.3.2 类与类之间关系的表示方式
2.3.2.1 关联关系
关联关系是对象之间的一种引用关系, 用于表示一类对象与另一类对象之间的联系,如老师和学生、师傅和徒弟、丈夫和妻子等。关联关系是类与类之间最常用的一种关系,分为一般关联关系、聚合关系和组合关系。我们先介绍一般关联。
关联又可以分为单向关联,双向关联,自关联。
1. 单向关联
在UML类图中单向关联用一个箭头的实现表示。上图表示每个顾客都有一个地址, 这通过让Customer类持有一个类型为Address的成员变量实现。
2. 双向关联
从上图中我们很容易看出,所谓的双向关联就是双方各自持有对方类型的成员变量。
在UML图中,双向关联用一个不带箭头的直线表示。上图中在Customer类中维护一个List< Product >,表示一个顾客可以购买多个商品;在Product类中维护一个Customer类型的成员变量表示这个产品被哪个顾客所购买。
3. 自关联
自关联在UML类图中用一个带有箭头且指向自身的线表示。上图的意思就是Node类包含类型为Node的成员变量,也就是“自己包含自己”。
2.3.2.2 聚合关系
聚合关系是关联关系的一种,是强关联关系,是整体和部分之间的关系。
聚合关系也是通过成员对象来实现的,其中成员对象是整体对象的一部分,但是成员对象可以脱离整体对象而独立存在。例如,学校与老师的关系,学校包含老师,但如果学校停办了,老师依然存在。
在UML类图中,聚合关系可以用带空心菱形的实线来表示,菱形指向整体。下图所示是大学和老师的关系图:
2.3.2.3 组合关系
组合表示类之间的整体与部分关系,但它是一种更强烈的聚合关系。
在组合关系中,整体对象可以控制部分对象的生命周期,一旦整体对象不存在,部分对象也将不存在,部分对象不能脱离整体对象而存在。例如,头和嘴的关系,没有了头,嘴也就不存在了。
在UML类图中,组合关系用带实心菱形的实线来表示,菱形指向实体。下图所示是头和嘴的关系图:
2.3.2.4 依赖关系
依赖关系是一种使用关系,它是对象之间耦合度最弱的一种关联方式,是临时性的关联。在代码中,某个类的方法通过局部变量、方法的参数或者对静态方法的调用来访问另一个类(被依赖类)中的某些方法来完成一些职责。
在UML类图中,依赖关系使用带箭头的虚线来表示,箭头从使用类指向被依赖类。下图所示是司机和汽车的关系图,司机驾驶汽车:
2.3.2.5 继承关系
继承关系是对象之间耦合度最大的一种关系,表示一般与特殊的关系,是父类与子类之间的关系,是一种继承关系。
在UML类图中,泛化关系用带空心三角箭头的实现来表示,箭头从子类指向父类。在代码实现时,使用面向对象的继承机制来实现泛化关系。例如,Student类和Teacher类都是Person的子类,其类图如下图所示:
2.3.2.6 实现关系
实现关系是接口与实现类之间的关系。在这种关系,类实现了接口,类中的操作实现了接口中所有声明的所有的抽象操作。
在UML类图中,实现关系使用带空心三角箭头的虚线来表示,箭头从实现类指向接口。例如,汽车和船实现了交通工具,其类图如下图所示:
3 软件设计原则
在软件开发中,为了提高软件系统的可维护性和可复用性,增加软件的可扩展性和灵活性,程序员要尽量根据6条原则来开发程序,从而提高软件开发效率、节约软件开发成本和维护成本。
3.1 开闭原则
对扩展开放,对修改关闭。在程序需要进行拓展的时候,不能去修改原有的代码,实现了一个热插拔的效果。简言之,是为了使程序的扩展性好,易于维护和升级。
想要达到这样的效果,我们需要使用接口和抽象类。
因为抽象灵活性好,适应性广,只要抽象的合理,可以基本保持软件架构的稳定。而软件中易变得细节可以从抽象派生来的实现类进行扩展,当软件需要发生变化时,只需要根据需求重新派生一个实现类来扩展就可以了。
下面以搜狗输入法
的皮肤为例介绍开闭原则的应用。
【例】搜狗输入法
的皮肤设计。
分析:搜狗输入法
的皮肤是输入法背景图片、窗口颜色和声音等元素的组合。用户可以根据自己的喜好更换皮肤,也可以从网上下载新的皮肤。这些皮肤都有共同的特点,可以为其定义一个抽象类(AbstractSkin), 而每个具体的皮肤(DefaultSpecificSkin和 HeimaSpecificSkin) 是其子类。用户窗体可以根据需要选择或者增加新的主题,而不需要修改原代码,所以它是满足开闭原则的。
以公司代码为例:同步基础数据时,保存更新基础数据时,定义了一个抽象类(AbstractService), 而每个具体的数据同步类(BaseDeptInfoService等)是其子类。
3.2 里氏替换
里氏替换原则是面向对象设计的基本原则之一。
里氏替换原则:任何基类可以出现的地方,子类一定可以出现。通俗理解:子类可以扩展父类的功能,但不能改变父类原有的功能。换句话说,子类继承父类时,除添加新的方法完成新增功能外,尽量不要重写父类的方法。
如果通过重写父类的方法来完成新的功能,这样写起来虽然简单,但是整个继承体系的可复用性会比较差,特别是运用多态比较频繁时,程序运行出错的概率会非常大。
下面看一个里氏替换原则中经典的一个例子
正方形不是长方形
在数学领域里,正方形毫无疑问是长方形,它是一个长宽相等的长方形。
代码如下:
长方形类(Rectangle)
1 | package cn.swifthealth.principles.replace; |
正方形(Square):
由于正方形的长宽高都相同,所以在方法setLength和setWidth中,对长度和宽度都需要赋相同的值。
1 | package cn.swifthealth.principles.replace; |
类RectanleDemo是我们的软件系统中的组件,它有一个resize方法依赖基类Rectangle, resize方法是RectangleDemo类中的一个方法,用来实现宽度主键增长的效果。
1 | package cn.swifthealth.principles.replace; |
我们运行一个这段代码就会发现,假如我们把一个普通长方形作为参数传入resize方法,就会看到长方形宽度主键增长的效果,当宽度大于长度,代码就会停止,这种行为结果符合我们的预期。假如我们再把一个正方形作为参数传入resize方法后,就会看到正方形的宽度和长度不会打印,一直在增长代码会一直运行下去,直至系统产生溢出错误。
所以,普通的长方形是适合这段代码的,正方形不适合。因此,Square和Rectangle类之间的继承关系违反了里氏替换原则。
如何改进呢?此时我们需要设计他们之间的关系。抽象出来一个四边形接口(Quadrilateral), 让Rectangle类和Square类实现Qudrilateral接口。
3.3 依赖倒置
高层模块不应该依赖底层模块,两者都应该依赖其抽象;抽象不应该依赖细节,细节应该依赖抽象。简单的说就是要求对抽象进行编程,不要对实现进行编程,这样就降低了客户与实现模块间的耦合。
下面来看一个例子来理解依赖倒置原则
【例】组装电脑
现在组装一台电脑,需要配件cpu,硬盘,内存条。只有这些配置都有了,计算机才能正常进行。选择cpu有很多选择,如Intel,AMD等,硬盘可以选择希捷,西数等,内存条可以选择金士顿、海盗船等。
类图如下:
代码如下:
希捷硬盘类(XiJieHardDisk):
希捷硬盘类(XiJieHardDisk):
1 | public class XiJieHardDisk implements HardDisk { |
Intel处理器(IntelCpu):
1 | public class IntelCpu implements Cpu { |
金士顿内存条(KingstonMemory):
1 | public class KingstonMemory implements Memory { |
电脑(Computer):
1 | public class Computer { |
测试类(TestComputer):
测试类用来组装电脑。
1 | public class TestComputer { |
上面的代码可以看到已经组装了一台电脑,但是似乎组装的电脑的CPU只能是Intel的,内存条只能是金士顿的,硬盘只能是希捷的。这对用户是非常不友好的,用户有了机箱以后,肯定是按照自己的喜好,选择自己喜欢的配件。
根据依赖倒置原则进行改进:代码我们只需要修改Computer类,让Computer类依赖抽象(各个配件的接口),而不是依赖于各个组件的具体实现类。
类图如下:
电脑(Computer):
1 |
|
面向对象的开发很好的解决了这个问题,一般情况下抽象的变化的概率很小,让用户程序依赖于抽象,实现的细节也依赖于抽象。即使实现细节不断变动,只要抽象不变,客户程序就不需要变化。这大大降低了客户程序与实现细节的耦合度。
3.4 接口隔离原则
客户端不应该被迫依赖于它不使用的方法;一个类对另一个类的依赖应该建立在最小的接口上。
下面看一个例子来理解接口隔离原则
【例】安全门案例
我们需要创建一个黑马品牌的安全门,该安全门具有防火、防水、防盗的功能。可以将防火、防水、防盗功能提取成一个接口
客户端不应该被迫依赖于它不使用的方法;一个类对另一个类的依赖应该建立在最小的接口上。
下面看一个例子来理解接口隔离原则
【例】安全门案例
我们需要创建一个黑马
品牌的安全门,该安全门具有防火、防水、防盗的功能。可以将防火,防水,防盗功能提取成一个接口,形成一套规范。类图如下:
上面的设计我们发现了它存在的问题,黑马品牌的安全门具有防盗,防水,防火的功能。现在如果我们还需要再创建一个传智品牌的安全门,而该安全门只具有防盗、防水功能呢?很显然如果实现SafetyDoor接口就违背了接口隔离原则,那么我们如何进行修改呢?看如下类图:
代码如下:
AntiTheft(接口):
1 | public interface AntiTheft { |
Fireproof(接口):
1 | public interface Fireproof { |
Waterproof(接口):
1 | public interface Waterproof { |
HeiMaSafetyDoor(类):
1 | public class HeiMaSafetyDoor implements AntiTheft,Fireproof,Waterproof { |
ItcastSafetyDoor(类):
1 | public class ItcastSafetyDoor implements AntiTheft,Fireproof { |
3.5 迪米特法则
迪米特法则又叫最少知识原则。
只和你的直接朋友交谈,不跟“陌生人”说话(Talk only to your immediate friends and not to strangers)。
其含义是:如果两个软件实体无须直接通信,那么就不应当发生直接的相互调用,可以通过第三方转发该调用。其目的是降低类之间的耦合度,提高模块的相对独立性。
迪米特法则中的“朋友”是指:当前对象本身、当前对象的成员对象、当前对象所创建的对象、当前对象的方法参数等,这些对象同当前对象存在关联、聚合或组合关系,可以直接访问这些对象的方法。
下面看一个例子来理解迪米特法则
【例】明星与经纪人的关系实例
明星由于全身心投入艺术,所以许多日常事务由经纪人负责处理,如和粉丝的见面会,和媒体公司的业务洽淡等。这里的经纪人是明星的朋友,而粉丝和媒体公司是陌生人,所以适合使用迪米特法则。
类图如下:
代码如下:
明星类(Star)
1 | public class Star { |
粉丝类(Fans)
1 | public class Fans { |
媒体公司类(Company)
1 | public class Company { |
经纪人类(Agent)
1 | public class Agent { |
3.6 合成复用原则
合成复用原则是指:尽量先使用组合或者聚合等关联关系来实现,其次才考虑使用继承关系来实现。
通常类的复用分为继承复用和合成复用两种。
继承复用虽然有简单和易实现的优点,但它也存在以下缺点:
- 继承复用破坏了类的封装性。因为继承会将父类的实现细节暴露给子类,父类对子类是透明的,所以这种复用又称为“白箱”复用。
- 子类与父类的耦合度高。父类的实现的任何改变都会导致子类的实现发生变化,这不利于类的扩展与维护。
- 它限制了复用的灵活性。从父类继承而来的实现是静态的,在编译时已经定义,所以在运行时不可能发生变化。
采用组合或聚合复用时,可以将已有对象纳入新对象中,使之成为新对象的一部分,新对象可以调用已有对象的功能,它有以下优点:
- 它维持了类的封装性。因为成分对象的内部细节是新对象看不见的,所以这种复用又称为“黑箱”复用。
- 对象间的耦合度低。可以在类的成员位置声明抽象。
- 复用的灵活性高。这种复用可以在运行时动态进行,新对象可以动态地引用与成分对象类型相同的对象。
下面看一个例子来理解合成复用原则
【例】汽车分类管理程序
汽车按“动力源”划分可分为汽油汽车、电动汽车等;按“颜色”划分可分为白色汽车、黑色汽车和红色汽车等。如果同时考虑这两种分类,其组合就很多。类图如下:
从上面类图我们可以看到使用继承复用产生了很多子类,如果现在又有新的动力源或者新的颜色的话,就需要再定义新的类。我们试着将继承复用改为聚合复用看一下。
4. 创建者模式
创建型模式的主要关注点是“怎样创建对象?”,它的主要特点是“将对象的创建与使用分离”。
这样可以降低系统的耦合度,使用者不需要关注对象的创建细节。
创建型模式分为:
- 单例模式
- 工厂方法模式
- 抽象工程模式
- 原型模式
- 建造者模式
4.1 单例模式🇨🇳
单例设计模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有一个单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。
4.1.1 单例模式的结构
单例模式的主要角色有以下两种:
- 单例类。只能创建一个实例的类。
- 访问类。使用单例类。
4.1.2 单例模式的实现
单例设计分为两种:
- 饿汉式:类加载就会导致该单实例对象被创建。
- 懒汉式:类加载不会导致该单实例对象被创建,而是首次使用该对象的时候才会被创建。
4.1.2.1 饿汉式-方式1 (静态变量方式)
1 | package cn.swifthealth.singleton.hungry; |
说明:
该方式在成员位置声明Singleton类型的静态变量,并创建Singleton类的对象instance。instance对象是随着类的加载而创建的。如果该对象足够大的话,而一直没有使用就会造成内存的浪费。
4.1.2.2 饿汉式-方式2(静态代码块方式)
1 | /** |
说明:
该方式在成员位置声明Singleton类型的静态变量,而对象的创建是在静态代码块中,也是对着类的加载而创建。所以和饿汉式的方式1基本上一样,当然该方式也存在内存浪费问题。
4.1.2.3 懒汉式-方式1(线程不安全)
1 | package cn.swifthealth.singleton.hungry.lazy; |
说明:
从上面的代码可以看出该方式在成员位置声明Singleton类型的静态变量,并没有进行对象的赋值操作,那么什么时候赋值的呢?当调用getInstance()方法获取Singleton类的对象的时候才创建singleton类的对象,这样就实现了懒加载的效果。但是,如果是多线程环境,会出现线程安全问题。
4.1.2.4 懒汉式-方式2 (线程安全)
1 | package cn.swifthealth.singleton.lazy; |
说明:
该方式也实现了懒加载效果,同时又解决了线程安全问题。但是在getInstance()方法上添加了synchronized关键字,导致该方法的执行效果特别低。从上面代码我们可以看出,其实就是在初始化instance的时候才会出现线程安全问题,一旦初始化完成就不存在了。
4.1.2.5 懒汉式-方式3 (双重检查锁)
再来讨论下懒汉模式中加锁的问题,对于getInstance()方法来说,绝大部分的操作都是读操作,读操作是线程安全的,所以我们没必要让每个线程必须持有锁才能调用这个方法,我们需要调整加锁时机。由此也产生了一种新的实现模式:双重检查锁模式。
说明:
1 | package cn.swifthealth.singleton.lazy; |
双重检查锁模式是一种非常好的单例实现模式,解决了单例、性能、线程安全问题。上面的双重检查锁模式看上去完美无缺,其实是存在问题,在多线程情况下,可能会出现空指针的问题。出现问题的原因是JVM在实例化对象的时候会进行优化和指令重排序操作。
要解决双重检查锁模式带来空指针异常的问题,只需要使用volatile
关键字,volatile
关键字可以保证可见性和有序性。
1 | package cn.swifthealth.singleton.lazy; |
说明:
添加volatile关键字之后的双重检查锁模式是一种比较好的单例模式,能够保证在多线程的情况下线程安全也不会有性能问题。
4.1.2.6 懒汉式-方式4 (静态内部类方式)
静态内部类单例模式中实例由内部类创建,由于JVM在加载外部类的过程中,是不会加载静态内部类的,只有内部类的属性/方法被调用时才会被加载,并初始化其静态属性。静态属性由于被static修饰,保证只被实例化一次,并且严格保证实例化顺序。
1 | package cn.swifthealth.singleton.lazy; |
说明:
第一次加载Singleton类时不会去初始化INSTANCE, 也只有第一次调用getInstance,虚拟机加载SingletonHolder并初始化INSTANCE,这样不仅能确保线程安全,也能保证Singleton类的唯一性。
小结:
静态内部类单例模式是一种优秀的单例模式,是开源项目中比较常用的一种单例模式。在没有加任何锁的情况下,保证了多线程下的安全,并且没有任何性能影响和空间的浪费。
4.1.2.7 枚举方式
枚举实现单例模式是极力推荐的单例实现模式,因为枚举类型是线程安全的,并且只会装载一次,设计者充分的利用了枚举的这个特性来实现单例模式,枚举的写法非常简单,而且枚举是所用单例实现中唯一一个不会被破坏的单例实现模式。
1 | /** |
说明:
枚举方式属于饿汉式方式。
4.1.3 存在问题
4.1.3.1 问题演示
破坏单例模式
使下面定义的单例类(Singleton)可以创建多个对象,枚举方式除外。有两种方式,分别是序列化和反射。
序列化和反序列化
Singleton类:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24package cn.swifthealth.serialzable;
import java.io.Serializable;
/**
* @Author: yangkai
* @Description: 破坏单例模式(序列化反序列化)
* @Date: Create in 下午4:25 2022/3/26
*/
public class Singleton implements Serializable {
// 私有构造方法
private Singleton() {};
private static class SingletonHandler {
private static final Singleton INSTANCE = new Singleton();
}
// 对外提供静态方法获取对象
public static Singleton getInstance() {
return SingletonHandler.INSTANCE;
}
}Test类:
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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49package cn.swifthealth.serialzable;
import java.io.*;
/**
* @Author: yangkai
* @Description:
* @Date: Create in 下午4:28 2022/3/26
*/
public class Test {
public static void main(String[] args) throws Exception {
// 往文件中写对象
wirteObject2File();
// 在文件中读取对象
Singleton singleton1 = readObjectFromFile();
Singleton singleton2 = readObjectFromFile();
System.out.println(singleton1 == singleton2);
}
/**
* 将文件中的类读取
* @return
* @throws Exception
*/
private static Singleton readObjectFromFile() throws Exception {
// 创建对象输入流对象
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("/home/yangkai/IdeaProjects/Design-pattern/src/main/java/cn/swifthealth/serialzable/1.txt"));
// 第一个读取Singleton对象
Singleton instance = (Singleton) ois.readObject();
return instance;
}
/**
* 将类写入到文件中
* @throws Exception
*/
private static void wirteObject2File() throws Exception {
// 获取Singleton类的对象
Singleton instance = Singleton.getInstance();
// 创建对象输出流
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("/home/yangkai/IdeaProjects/Design-pattern/src/main/java/cn/swifthealth/serialzable/1.txt"));
// 将instance对象写入到文件中
oos.writeObject(instance);
}
}上面代码运行结果是
false
,表明序列化和反序列化已经破坏了单例设计模式。反射方式
Singleton类:
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
26
27
28
29
30
31
32package cn.swifthealth.reflection;
/**
* @Author: yangkai
* @Description: 破坏单例模式(反射)
* @Date: Create in 下午5:05 2022/3/26
*/
public class Singleton {
//私有构造方法
private Singleton() {}
private static volatile Singleton instance;
//对外提供静态方法获取该对象
public static Singleton getInstance() {
if(instance != null) {
return instance;
}
synchronized (Singleton.class) {
if(instance != null) {
return instance;
}
instance = new Singleton();
return instance;
}
}
}Test类:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18public class Test {
public static void main(String[] args) throws Exception {
//获取Singleton类的字节码对象
Class clazz = Singleton.class;
//获取Singleton类的私有无参构造方法对象
Constructor constructor = clazz.getDeclaredConstructor();
//取消访问检查
constructor.setAccessible(true);
//创建Singleton类的对象s1
Singleton s1 = (Singleton) constructor.newInstance();
//创建Singleton类的对象s2
Singleton s2 = (Singleton) constructor.newInstance();
//判断通过反射创建的两个Singleton对象是否是同一个对象
System.out.println(s1 == s2);
}
}上面代码运行结果是
false
,表明序列化和反序列化已经破坏了单例设计模式注意:枚举方式不会出现这两个问题。
4.1.3.2 问题的解决
序列化、反序列化方式破坏单例模式的解决方法
在Singleton类中添加
readResolve()
方法,在反序列化时被反射调用,如果定义了这个方法,就返回这个方法的值,如果没有定义,则返回新new出来的对象。Singleton类:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21public class Singleton implements Serializable {
//私有构造方法
private Singleton() {}
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
//对外提供静态方法获取该对象
public static Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
/**
* 下面是为了解决序列化反序列化破解单例模式
*/
private Object readResolve() {
return SingletonHolder.INSTANCE;
}
}源码解析:
ObjectInputStream类:
readObject:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17public final Object readObject()
throws IOException, ClassNotFoundException
{
if (enableOverride) {
return readObjectOverride();
}
int outerHandle = passHandle;
try {
Object obj = readObject0(false); // 重点查看readObject0方法
handles.markDependency(outerHandle, passHandle);
ClassNotFoundException ex = handles.lookupException(passHandle);
if (ex != null) {
throw ex;
}
.....
}readObject0:
1
2
3
4
5
6
7
8
9
10
11
12
13
14private Object readObject0(boolean unshared) throws IOException {
...
try {
switch (tc) {
...
case TC_OBJECT:
return checkResolve(readOrdinaryObject(unshared));//重点查看readOrdinaryObject方法
...
}
} finally {
depth--;
bin.setBlockDataMode(oldMode);
}
}readOrdinaryObject:
1
2
3
4
5
6
7
8
9
10
11
12
13
14private Object readOrdinaryObject(boolean unshared) throws IOException {
...
//isInstantiable 返回true,执行 desc.newInstance(),通过反射创建新的单例类,
obj = desc.isInstantiable() ? desc.newInstance() : null;
...
// 在Singleton类中添加 readResolve 方法后 desc.hasReadResolveMethod() 方法执行结果为true
if (obj != null && handles.lookupException(passHandle) == null && desc.hasReadResolveMethod()) {
// 通过反射调用 Singleton 类中的 readResolve 方法,将返回值赋值给rep变量
// 这样多次调用ObjectInputStream类中的readObject方法,继而就会调用我们定义的readResolve方法,所以返回的是同一个对象。
Object rep = desc.invokeReadResolve(obj);
...
}
return obj;
}反射方式破解单例的解决方法
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
26
27
28
29
30public class Singleton {
//私有构造方法
private Singleton() {
/*
反射破解单例模式需要添加的代码
*/
if(instance != null) {
throw new RuntimeException();
}
}
private static volatile Singleton instance;
//对外提供静态方法获取该对象
public static Singleton getInstance() {
if(instance != null) {
return instance;
}
synchronized (Singleton.class) {
if(instance != null) {
return instance;
}
instance = new Singleton();
return instance;
}
}
}说明:
这种方式比较好理解。当通过反射方式调用构造方法进行创建创建时,直接抛异常。不运行此中操作。
4.1.4 举个栗子
Spring中Bean默认是单例,且是饿汉式,添加@Lazy注解后为懒汉式
Runtime类
1
2
3
4
5
6
7
8
9
10
11
12public class Runtime {
private static Runtime currentRuntime = new Runtime();
// 对外提供获取对象的方法
public static Runtime getRuntime() {
return currentRuntime;
}
// 私有构造方法
private Runtime() {}
...
}
4.2 工厂模式🇨🇳
4.2.1 概述
需求:设计一个咖啡店点餐系统
设计一个咖啡类(Coffee),并定义其两个子类(美式咖啡【AmericanCoffee】和拿铁咖啡【LatteCoffee】);再设计一个咖啡店类(CoffeeStore),咖啡店具有点咖啡的功能。原型模式
具体类的设计如下:
在java中,万物皆对象,这些对象都需要创建,如果创建的时候直接new该对象,就会对该对象耦合严重,假如我们要更换对象,所有new对象的地方都需要修改一遍,这显然违背了软件设计的开闭原则。如果我们使用工厂来生产对象,我们就只和工厂打交道就可以了,彻底和对象解耦,如果要更换对象,直接在工厂里更换该对象即可,达到了与对象解耦的目的;所以说,工厂模式最大的优点就是:解耦。
- 简单工厂模式(不属于GOF的23种经典设计模式)
- 工厂方法模式
- 抽象工厂模式
4.2.2 简单工厂模式
简单工厂模式不是一种设计模式,反而比较像一种编程习惯。
4.2.2.1 结构
简单工厂包含如下角色:
- 抽象产品:定义了产品的规范,描述了产品的主要特性和功能
- 具体产品:实现或继承抽象产品的子类
- 具体工厂:提供了创建产品的方法,调用者通过该方法来获取产品。
4.2.2.2 实现
现在使用简单工厂对上面的案例进行改进,类图如下:
工厂类代码如下:
1 | package cn.swifthealth.factory.simple; |
工厂(factory)处理创建对象的细节,一旦有了SimpleCoffeeFactory,CoffeeStore类中的orderCoffee()就变成此对象的客户,后期如果需要Coffee对象直接从工厂中获取即可。这样也就解除了和Coffee实现类的耦合,同时又产生了新的耦合,CoffeeStore对象和SimpleCoffeeFactory工厂对象的耦合,工厂对象和商品对象的耦合。
后期如果再加新品种的咖啡,我们势必要需求修改SimpleCoffeeFactory的代码,违反了开闭原则。工厂类的客户端可能有很多,比如创建美团外卖等,这样只需要修改工厂类的代码,省去其他的修改操作。
4.2.2.3 优缺点
优点:
封装了创建对象的过程,可以通过参数直接获取对象。把对象的创建和业务逻辑层分开,这样以后避免了修改客户端代码,如果要实现新产品直接修改工厂类,而不需要在原代码中修改,这样就降低了客户代码修改的可能性,更加容易扩展。
缺点:
增加新产品时还是需要修改工厂类的代码,违背了“开闭原则”。
4.2.2.3 扩展
在开发中也有一部分人将工厂类中的创建对象的功能定义为静态的,这个就是静态工厂模式,它也不是23种设计模式中的。代码如下:
1 | package cn.swifthealth.factory.simple; |
4.2.3 工厂方法模式
针对上例中的缺点,使用工厂方法模式就可以完美解决,完全遵循开闭原则。
4.2.3.1 概念
定义一个用于创建对象的接口,让子类决定实例化哪个产品类对象。工厂方法使一个产品类的实例化延迟到其工厂的子类。
4.2.3.2 结构
工厂方法模式的主要角色:
- 抽象工厂(Abstract Factory):提供了创建产品的接口,调用者通过它访问具体工厂的工厂方法来创建产品。
- 具体工厂(Concrete Factory): 主要是实现抽象工厂中的抽象方法,完成具体产品的创建
- 抽象产品(Product): 定义了产品的规范,描述了产品的主要特性和功能
- 具体产品(Concrete Product):实现 了抽象产品所定义的接口,由具体工厂来创建,它同具体工厂一一对应
4.2.3.3 实现
使用工厂方法模式对上例进行改进,类图如下:
代码如下:
抽象工厂:
1 | public interface CoffeeFactory { |
具体工厂:
1 | public class LatteCoffeeFactory implements CoffeeFactory { |
咖啡店类:
1 | public class CoffeeStore { |
从以上的编写的代码可以看到,要增加产品类时也要相应地增加工厂类,不需要修改工厂类的代码,这样就解决了简单工厂模式的缺点。
工厂方法模式是简单工厂模式的进一步抽象。由于使用了多态性,工厂方法模式保持了简单工厂模式的优点,而且克服了它的缺点。
4.2.3.4 优缺点
优点:
- 用户只需要知道具体工厂的名称就可以得到所要的产品,无需知道产品具体创建过程
- 在系统增加新产品时只需要增加具体产品类和对应的具体工厂类,无须对原工厂进行任何修改,满足开闭原则
缺点:
- 每增加一个产品就要增加一个具体产品类和一个对应的具体工厂类,这增加了系统的复杂度。
4.2.4 抽象工厂模式
4.2.4.1 概念
是一种为访问类提供一个创建一组相关或相互依赖对象的接口,且访问类无须指定所要产品的具体类就能得到同族的不同等级的产品的模式结构。
抽象工厂模式,是工厂方法模式的升级版本,工厂方法模式只生产一个等级的产品,而抽象工厂模式可生产多个等级的产品。
4.2.4.2 结构
抽象工厂的主要角色如下:
- 抽象工厂(Abstract Factory):提供了创建产品的接口,它包含多个创建产品的方法,可以创建多个不同等级的产品。
- 具体工厂(Concrete Factory): 主要是实现抽象工厂中的多个抽象方法,完成具体产品的创建
- 抽象产品(Product): 定义了产品的规范,描述了产品的主要特性和性能,抽象工厂模式有多个抽象产品
- 具体产品(Concrete Product): 实现了抽象产品角色所定义的接口,由具体工厂来创建,它痛具体工厂之间是多对一的关系
4.2.4.3 实现
现咖啡店业务发生改变,不仅要生产咖啡还要生产甜点,如提拉米苏、抹茶慕斯等,要是按照工厂方法模式,需要定义提拉米苏类、抹茶慕斯类、提拉米苏工厂、抹茶慕斯工厂、甜点工厂类,很容易发生类爆炸情况。其中拿铁咖啡、美式咖啡是一个产品等级,都是咖啡;提拉米苏、抹茶慕斯也是一个产品等级;拿铁咖啡和提拉米苏是同一产品族(也就是都属于意大利风味),美式咖啡和抹茶慕斯是同一产品族(也就是都属于美式风味)。所以这个案例可以使用抽象工厂模式实现。类图如下:
代码如下:
抽象工厂:
1 | public interface DessertFactory { |
具体工厂:
1 | //美式甜点工厂 |
如果要加同一个产品族的话,只需要再加一个对应的工厂类即可,不需要修改其他的类。
4.2.4.4 优缺点
优点:
当一个产品族中的多个对象被设计成一起工作时,它能保证客户端始终只使用同一个产品族中的对象。
缺点:
当产品族中需要增加一个新的产品时,所有的工厂类都需要进行修改。
4.2.4.5 使用场景
当需要创建的对象是一系列相互关联或相互依赖的产品族时,如电器工厂中的电视机、洗衣机、空调等。
系统中有多个产品族,但每次只使用其中的某一族产品。如有人只喜欢穿某一个品牌的衣服和鞋。
系统中提供了产品的类库,且所有产品的接口相同,客户端不依赖产品实例的创建细节和内部结构。
如:输入法换皮肤,一整套一起换。生成不同操作系统的程序。
4.2.5 模式扩展
简单工厂+配置文件解除耦合
可以通过工厂模式+配置文件的方式解除工厂对象和产品对象的耦合。在工厂类中加载配置文件中的全类名,并创建对象进行存储,客户端如果需要对象,直接进行获取即可。
第一步:定义配置文件
为了演示方便,我们使用properties文件作为配置文件,名称为bean.properties
1 | american=cn.swifthealth.factory.AmericanCoffee |
第二步:改进工厂类
1 | public class CoffeeFactory { |
静态成员变量用来存储创建的对象(键存储的是名称,值存储的是对应的对象),而读取配置文件以及创建对象写在静态代码块中,目的就是只需要执行一次。
4.2.6 再举个栗子
4.2.6.1 JDK源码解析-Collection.iterator方法
1 | public class Demo { |
对上面的代码大家应该很熟,使用迭代器遍历集合,获取集合中的元素。而单列集合获取迭代器的方法就使用到了工厂方法模式。我们看通过类图看看结构:
Collection接口是抽象工厂类,ArrayList是具体的工厂类;Iterator接口是抽象商品类,ArrayList类中的Iter内部类是具体的商品类。在具体的工厂类中iterator()方法创建具体的商品类的对象。
另:
1,DateForamt类中的getInstance()方法使用的是工厂模式;
2,Calendar类中的getInstance()方法使用的是工厂模式;
4.2.6.2 SyncDataHandlerFactory 数据处理类
4.3 原型模式
4.3.1 概述
用一个已经创建的实例作为原型,通过复制该原型对象来创建一个和原型对象相同的新对象。
4.3.2 结构
原型模式包含如下角色:
- 抽象原型类:规定了具体源性对象必须实现的clone()方法
- 具体原型类:实现抽象原型类的clone()方法,它是可被复制的对象
- 访问类:使用具体原型类中的clone()方法来复制新的对象
接口类图如下:
4.3.3 实现
原型模式的克隆分为浅克隆和深克隆。
浅克隆: 创建一个新的对象,新对象的属性和原来对象完全相同,对于非基本类型属性,仍指向原有属性所指向的对象的内存地址。
深克隆:创建一个新的对象,属性中引用的其他对象也会被克隆,不再指向原有对象地址。
Java中的Object类中提供了 clone()
方法来实现浅克隆。 Cloneable 接口是上面的类图中的抽象原型类,而实现了Cloneable接口的子实现类就是具体的原型类。代码如下:
Realizetype(具体的原型类):
1 | public class Realizetype implements Cloneable { |
PrototypeTest(测试访问类):
1 | public class PrototypeTest { |
4.3.4 案例
用原型模式生成”三号学生”奖状
同一学校的“三好学生”奖状,除了人名不同,其他都相同,可以用原型模式复制多个“三好学生”奖状出来。
类图如下:
代码如下:
1 | //奖状类 |
4.3.5 使用场景
- 对象的创建非常复杂,可以使用原型模式快捷的创建对象。
- 性能和安全要求比较高。
4.3.6 扩展(深克隆)
将上面的”三好学生”奖状的案例中Citation类的name属性修改为Student类型的属性。代码如下:
1 | //奖状类 |
运行结果:
说明:
stu对象和stu1对象是同一个对象,就会产生将stu1对象中name属性值改为“李四”,两个Citation(奖状)对象中显示的都是李四。这就是浅克隆的效果,对具体原型类(Citation)中的引用类型的属性进行引用的复制。这种情况需要使用深克隆,而进行深克隆需要使用对象流。代码如下:
1 | public class CitationTest1 { |
运行结果为:
注意: Citation类和Student类必须实现Serializable接口,否则会抛NotSerializableException异常。
4.3.7 举个栗子
- ArrayList中clone()方法的源码
1 | public Object clone() { |
Bean的作用域(Prototype)
每次注入或者通过Spring应用上下文获取的时候都会创建一个新的bean实例。
例如,如果你使用组件扫描来发现和声明bean,那么你可以在bean的类上使用@Scope注解,将其声明为原型bean:
这里,使用
ConfigurableBeanFactory
类的SCOPE_PROTOTYPE
常量设置了原型作用域。你当然也可以使用@Scope(“prototype”)
,但是使用SCOPE_PROTOTYPE常量更加安全并且不易出错。
4.4 建造者模式
4.4.1 概述
将一个复杂对象的构建与表示分离,使得构建过程和创建可以不同表示
- 分离了部件的构造(由Builder来负责)和装配(由Director负责)。从而可以构造出复杂的对象。这个模式适用于:某个对象的构建过程复杂的情况。
- 由于实现了构建和装配的解耦。不同的构建器,相同的装配,也可以做出不同的对象;相同的构建器,不同的装配顺序也可以做出不同的对象。也就是实现了构建算法,装配算法的解耦,实现了更好的复用
- 建造者模式可以将部件和其组装过程分开,一步一步创建一个复杂的对象。用户只需要指定复杂对象的类型就可以得到该对象,而无须知道其内部的具体构造细节。
4.4.2 结构
建造者(Builder)模式包含如下角色:
- 抽象建造者类(Builder):这个接口规定要实现复杂对象的那些部分的创建,并不涉及具体的部件对象的创建。
- 具体建造者类(ConcreteBuilder):实现Builder接口,完成复杂产品的各个部件的具体创建方法。在构造过程完成后,提供产品的实例。
- 产品类(Product): 要创建的复杂对象
- 指挥者类(Director): 调用具体建造者来创建复杂对象的各个部分,在指导者中不涉及具体产品的信息,只负责保证对象各个部分完整创建或按照某种顺序创建。
类图如下:
4.4.3 实现
创建共享单车
生产自行车是一个复杂的过程,它包含了车架,车座等组件的生产。而车架又有碳纤维,铝合金等材质的,车座有橡胶,真皮等材质。对于自行车的生产就可以使用建造者模式。
这里Bike是产品,包含车架,车座等组件;Builder是抽象建造者,MobikeBuilder和OfoBuilder是具体的建造者;Director是指挥者。类图如下:
具体代码如下:
1 | //自行车类 |
注意:
上面示例是 Builder模式的常规用法,指挥者类 Director 在建造者模式中具有很重要的作用,它用于指导具体构建者如何构建产品,控制调用先后次序,并向调用者返回完整的产品类,但是有些情况下需要简化系统结构,可以把指挥者类和抽象建造者进行结合
1 | // 抽象 builder 类 |
说明:
这样做确实简化了系统结构,但同时也加重了抽象建造者类的职责,也不是太符合单一职责原则,如果construct() 过于复杂,建议还是封装到 Director 中。
4.4.4 优缺点
优点:
- 建造者模式的封装性很好。使用建造者模式可以有效地封装变化,在使用建造者模式的场景中,一般产品类和建造者类是比较稳定的,因此,将主要的业务逻辑封装在指挥者类中对整体而言可以取得比较好的稳定性。
- 在建造者模式中,客户端不必知道产品内部组成的细节,将产品本身与产品的创建过程解耦,使得相同的创建过程可以创建不同的产品对象。
- 可以更加精细地控制产品的创建过程 。将复杂产品的创建步骤分解在不同的方法中,使得创建过程更加清晰,也更方便使用程序来控制创建过程。
- 建造者模式很容易进行扩展。如果有新的需求,通过实现一个新的建造者类就可以完成,基本上不用修改之前已经测试通过的代码,因此也就不会对原有功能引入风险。符合开闭原则。
缺点:
建造者模式所创建的产品一般具有较多的共同点,其组成部分相似,如果产品之间的差异性很大,则不适合使用建造者模式,因此其适用范围受到一定的限制。
4.4.5 使用场景
建造者(Builder)模式创建的是复杂对象,其产品的各个部分经常面临着剧烈的变化,但将它们组合在一起的算法却相对稳定,所以它通常在以下场合使用。
- 创建的对象较复杂,由多个部件构成,各部件面临着复杂的变化,但构件间的建造顺序是稳定的。
- 创建复杂对象的算法独立于该对象的组成部分以及它们的装配方式,即产品的构建过程和最终的表示是独立的。
4.4.6 模式扩展
建造者模式除了上面的用途外,在开发中还有一个常用的使用方式,就是当一个类构造器需要传入很多参数时,如果这个类的实例,代码可读性会非常差,而且很容易引入错误,此时就可以利用建造者模式进行重构。
重构前代码如下:
1 | public class Phone { |
上面在客户端代码中构建Phone对象,传递了四个参数,如果参数更多呢?代码的可读性及使用的成本就是比较高。
重构后代码:
1 | public class Phone { |
重构后的代码在使用起来更方便,某种程度上也可以提高开发效率。从软件设计上,对程序员的要求比较高。
4.5 创建者模式对比
4.6.1 工厂方法模式VS建造者模式
工厂方法模式注重的是整体对象的创建方式;而建造者模式注重的是部件构建的过程,意在通过一步一步地精确构造创建出一个复杂的对象。
我们举个简单例子来说明两者的差异,如要制造一个超人,如果使用工厂方法模式,直接产生出来的就是一个力大无穷、能够飞翔、内裤外穿的超人;而如果使用建造者模式,则需要组装手、头、脚、躯干等部分,然后再把内裤外穿,于是一个超人就诞生了。
4.6.2 抽象工厂模式VS建造者模式
抽象工厂模式实现对产品家族的创建,一个产品家族是这样的一系列产品:具有不同分类维度的产品组合,采用抽象工厂模式则是不需要关心构建过程,只关心什么产品由什么工厂生产即可。
建造者模式则是要求按照指定的蓝图建造产品,它的主要目的是通过组装零配件而产生一个新产品。
如果将抽象工厂模式看成汽车配件生产工厂,生产一个产品族的产品,那么建造者模式就是一个汽车组装工厂,通过对部件的组装可以返回一辆完整的汽车。
5. 结构型模式
结构型模式描述如何将类或者对象按某种布局组成更大的结构。它分为类结构型模式和对象结构型模式,前者采用继承机制来组织接口和类,后者采用组合或聚合来组合对象。
由于组合关系或聚合关系比继承关系耦合度低,满足“合成复用原则”,所以对象结构型模式比类结构型模式具有更大的灵活性。结构型模式分为以下 7 种:
- 代理模式
- 适配器模式
- 装饰者模式
- 桥接模式
- 外观模式
- 组合模式
- 享元模式
5.1 代理模式🇨🇳
5.1.1 概述
由于某些原因需要给某对象提供一个代理以控制对该对象的访问。这时,访问对象不适合或者不能直接引用目标对象,代理对象作为访问对象和目标对象之间的中介。
Java中的代理按照代理类生成时机不同又分为静态代理和动态代理。静态代理代理类在编译期就生成,而动态代理代理类则是在Java运行时动态生成。动态代理又有JDK代理和CGLib代理两种。
5.1.2 结构
代理(Proxy)模式分为三种角色:
- 抽象主题(Subject)类: 通过接口或抽象类声明真实主题和代理对象实现的业务方法。
- 真实主题(Real Subject)类: 实现了抽象主题中的具体业务,是代理对象所代表的真实对象,是最终要引用的对象。
- 代理(Proxy)类 : 提供了与真实主题相同的接口,其内部含有对真实主题的引用,它可以访问、控制或扩展真实主题的功能。
5.1.3 静态代理
我们通过案例来感受一下静态代理。
【例】火车站卖票
如果要买火车票的话,需要去火车站买票,坐车到火车站,排队等一系列的操作,显然比较麻烦。而火车站在多个地方都有代售点,我们去代售点买票就方便很多了。这个例子其实就是典型的代理模式,火车站是目标对象,代售点是代理对象。类图如下:
代码如下:
1 | //卖票接口 |
从上面代码中可以看出测试类直接访问的是ProxyPoint类对象,也就是说ProxyPoint作为访问对象和目标对象的中介。同时也对sell方法进行了增强(代理点收取一些服务费用)。
5.1.4 JDK动态代理
接下来我们使用动态代理实现上面案例,先说说JDK提供的动态代理。Java中提供了一个动态代理Proxy,Proxy并不是我们上述所说的代理对象的类,而是提供了一个创建代理对象的静态方法(newProxyInstance方法)来获取代理对象。
代码如下:
1 | //卖票接口 |
使用动态代理需要考虑的问题:
ProxyFactory是代理类吗?
ProxyFactory不是代理模式中所说的代理类,而代理类是程序在运行过程中动态的在内存中生成的类。通过阿里巴巴开源的 Java 诊断工具[Arthas阿尔萨斯](Tower 文档: Arthas基本功能分享 ( https://tower.im/teams/694585/repository_documents/101808 ))
查看代理类的结构:
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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83package com.sun.proxy;
import com.itheima.proxy.dynamic.jdk.SellTickets;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.lang.reflect.UndeclaredThrowableException;
public final class $Proxy0 extends Proxy implements SellTickets {
private static Method m1;
private static Method m2;
private static Method m3;
private static Method m0;
public $Proxy0(InvocationHandler invocationHandler) {
super(invocationHandler);
}
static {
try {
m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object"));
m2 = Class.forName("java.lang.Object").getMethod("toString", new Class[0]);
m3 = Class.forName("com.itheima.proxy.dynamic.jdk.SellTickets").getMethod("sell", new Class[0]);
m0 = Class.forName("java.lang.Object").getMethod("hashCode", new Class[0]);
return;
}
catch (NoSuchMethodException noSuchMethodException) {
throw new NoSuchMethodError(noSuchMethodException.getMessage());
}
catch (ClassNotFoundException classNotFoundException) {
throw new NoClassDefFoundError(classNotFoundException.getMessage());
}
}
public final boolean equals(Object object) {
try {
return (Boolean)this.h.invoke(this, m1, new Object[]{object});
}
catch (Error | RuntimeException throwable) {
throw throwable;
}
catch (Throwable throwable) {
throw new UndeclaredThrowableException(throwable);
}
}
public final String toString() {
try {
return (String)this.h.invoke(this, m2, null);
}
catch (Error | RuntimeException throwable) {
throw throwable;
}
catch (Throwable throwable) {
throw new UndeclaredThrowableException(throwable);
}
}
public final int hashCode() {
try {
return (Integer)this.h.invoke(this, m0, null);
}
catch (Error | RuntimeException throwable) {
throw throwable;
}
catch (Throwable throwable) {
throw new UndeclaredThrowableException(throwable);
}
}
public final void sell() {
try {
this.h.invoke(this, m3, null);
return;
}
catch (Error | RuntimeException throwable) {
throw throwable;
}
catch (Throwable throwable) {
throw new UndeclaredThrowableException(throwable);
}
}
}从上面的类中,我们可以看到以下几个信息:
- 代理类($Proxy0)实现了SellTickets。这也就印证了我们之前说的真实类和代理类实现同样的接口。
- 代理类($Proxy0)将我们提供了的匿名内部类对象传递给了父类。
动态代理的执行流程是什么样?
下面是摘取的重点代码:
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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57//程序运行过程中动态生成的代理类
public final class $Proxy0 extends Proxy implements SellTickets {
private static Method m3;
public $Proxy0(InvocationHandler invocationHandler) {
super(invocationHandler);
}
static {
m3 = Class.forName("com.itheima.proxy.dynamic.jdk.SellTickets").getMethod("sell", new Class[0]);
}
public final void sell() {
this.h.invoke(this, m3, null);
}
}
//Java提供的动态代理相关类
public class Proxy implements java.io.Serializable {
protected InvocationHandler h;
protected Proxy(InvocationHandler h) {
this.h = h;
}
}
//代理工厂类
public class ProxyFactory {
private TrainStation station = new TrainStation();
public SellTickets getProxyObject() {
SellTickets sellTickets = (SellTickets) Proxy.newProxyInstance(station.getClass().getClassLoader(),
station.getClass().getInterfaces(),
new InvocationHandler() {
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("代理点收取一些服务费用(JDK动态代理方式)");
Object result = method.invoke(station, args);
return result;
}
});
return sellTickets;
}
}
//测试访问类
public class Client {
public static void main(String[] args) {
//获取代理对象
ProxyFactory factory = new ProxyFactory();
SellTickets proxyObject = factory.getProxyObject();
proxyObject.sell();
}
}
执行流程如下:
1. 在测试类中通过代理对象调用sell()方法
2. 根据多态的特性,执行的是代理类($Proxy0)中的sell()方法
3. 代理类($Proxy0)中的sell()方法中又调用了InvocationHandler接口的子实现类对象的invoke方法
4. invoke方法通过反射执行了真实对象所属类(TrainStation)中的sell()方法
5.1.5 CGLIB动态代理
同样是上面的案例,我们再次使用CGLIB代理实现
同样是上面的案例,我们再次使用CGLIB代理实现。
如果没有定义SellTickets接口,只定义了TrainStation(火车站类)。很显然JDK代理是无法使用了,因为JDK动态代理要求必须定义接口,对接口进行代理。
CGLIB是一个功能强大,高性能的代码生成包。它为没有实现接口的类提供代理,为JDK的动态代理提供了很好的补充。
CGLIB是第三方提供的包,所以需要引入jar包的坐标:
1 | <dependency> |
代码如下:
1 | //火车站 |
5.1.6 三种代理的对比
jdk代理和CGLIB代理
使用CGLib实现动态代理,CGLib底层采用ASM字节码生成框架,使用字节码技术生成代理类,在JDK1.6之前比使用Java反射效率要高。唯一需要注意的是,CGLib不能对声明为final的类或者方法进行代理,因为CGLib原理是动态生成被代理类的子类。
在JDK1.6、JDK1.7、JDK1.8逐步对JDK动态代理优化之后,在调用次数较少的情况下,JDK代理效率高于CGLib代理效率,只有当进行大量调用的时候,JDK1.6和JDK1.7比CGLib代理效率低一点,但是到JDK1.8的时候,JDK代理效率高于CGLib代理。所以如果有接口使用JDK动态代理,如果没有接口使用CGLIB代理。
动态代理和静态代理
动态代理与静态代理相比较,最大的好处是接口中声明的所有方法都被转移到调用处理器一个集中的方法中处理(InvocationHandler.invoke)。这样,在接口方法数量比较多的时候,我们可以进行灵活处理,而不需要像静态代理那样每一个方法进行中转。
如果接口增加一个方法,静态代理模式除了所有实现类需要实现这个方法外,所有代理类也需要实现此方法。增加了代码维护的复杂度。而动态代理不会出现该问题
5.1.7 优缺点
优点:
- 代理模式在客户端与目标对象之间起到一个中介作用和保护目标对象的作用;
- 代理对象可以扩展目标对象的功能;
- 代理模式能将客户端与目标对象分离,在一定程度上降低了系统的耦合度;
缺点:
- 增加了系统的复杂度;
5.1.8 举个例子
- SpringIOC
5.2 适配器模式🇨🇳
5.2.1 概述
如果去欧洲国家去旅游的话,他们的插座如下图最左边,是欧洲标准。而我们使用的插头如下图最右边的。因此我们的笔记本电脑,手机在当地不能直接充电。所以就需要一个插座转换器,转换器第1面插入当地的插座,第2面供我们充电,这样使得我们的插头在当地能使用。生活中这样的例子很多,手机充电器(将220v转换为5v的电压),读卡器等,其实就是使用到了适配器模式。
定义:
将一个类的接口转换成客户希望的另外一个接口,使得原本由于接口不兼容而不能一起工作的那些类能一起工作。
适配器模式分为类适配器模式和对象适配器模式,前者类之间的耦合度比后者高,且要求程序员了解现有组件库中的相关组件的内部结构,所以应用相对较少些。
5.2.2 结构
适配器模式(Adapter)包含以下主要角色:
- 目标(Target)接口:当前系统业务所期待的接口,它可以是抽象类或接口。
- 适配者(Adaptee)类:它是被访问和适配的现存组件库中的组件接口。
- 适配器(Adapter)类:它是一个转换器,通过继承或引用适配者的对象,把适配者接口转换成目标接口,让客户按目标接口的格式访问适配者。
5.2.3 类适配器模式
实现方式:对象适配器模式可釆用将现有组件库中已经实现的组件引入适配器类中,该类同时实现当前系统的业务接口。
【例】读卡器
现有一台电脑只能读取SD卡,而要读取TF卡中的内容的话就需要使用到适配器模式。创建一个读卡器,将TF卡中的内容读取出来。
我们使用对象适配器模式将读卡器的案例进行改写。类图如下:
代码如下:
类适配器模式的代码,我们只需要修改适配器类(SDAdapterTF)和测试类。
1 | //SD卡的接口 |
注意:还有一个适配器模式是接口适配器模式。当不希望实现一个接口中所有的方法时,可以创建一个抽象类Adapter ,实现所有方法。而此时我们只需要继承该抽象类即可。
5.2.4 对象适配器模式
实现方式:对象适配器模式可釆用将现有组件库中已经实现的组件引入适配器类中,该类同时实现当前系统的业务接口。
【例】读卡器
我们使用对象适配器模式将读卡器的案例进行改写。类图如下:
代码如下:
类适配器模式的代码,我们只需要修改适配器类(SDAdapterTF)和测试类。
1 | //创建适配器对象(SD兼容TF) |
注意:还有一个适配器模式是接口适配器模式。当不希望实现一个接口中所有的方法时,可以创建一个抽象类Adapter ,实现所有方法。而此时我们只需要继承该抽象类即可。
5.2.5 应用场景
- 以前开发的系统存在满足新系统功能需求的类,但其接口同新系统的接口不一致。
- 使用第三方提供的组件,但组件接口定义和自己要求的接口定义不同。
5.2.6 JDK源码解析
Reader(字符流)、InputStream(字节流)的适配使用的是InputStreamReader。
InputStreamReader继承自java.io包中的Reader,对他中的抽象的未实现的方法给出实现。如:
1 | public int read() throws IOException { |
如上代码中的sd(StreamDecoder类对象),在Sun的JDK实现中,实际的方法实现是对sun.nio.cs.StreamDecoder类的同名方法的调用封装。类结构图如下:
从上图可以看出:
- InputStreamReader是对同样实现了Reader的StreamDecoder的封装。
- StreamDecoder不是Java SE API中的内容,是Sun JDK给出的自身实现。但我们知道他们对构造方法中的字节流类(InputStream)进行封装,并通过该类进行了字节流和字符流之间的解码转换。
结论:
从表层来看,InputStreamReader做了InputStream字节流类到Reader字符流之间的转换。而从如上Sun JDK中的实现类关系结构中可以看出,是StreamDecoder的设计实现在实际上采用了适配器模式。
桥接模式
装饰者模式
外观模式
享元模式
组合模式
行为型模式
模版方法模式
6.2 策略模式🇨🇳
6.2.1 概述
先看下面的图片,我们去旅游选择出行模式有很多种,可以骑自行车、可以坐汽车、可以坐火车、可以坐飞机。
作为一个程序猿,开发需要选择一款开发工具,当然可以进行代码开发的工具有很多,可以选择Idea进行开发,也可以使用eclipse进行开发,也可以使用其他的一些开发工具。
定义:
该模式定义了一系列算法,并将每个算法封装起来,使它们可以相互替换,且算法的变化不会影响使用算法的客户。策略模式属于对象行为模式,它通过对算法进行封装,把使用算法的责任和算法的实现分割开来,并委派给不同的对象对这些算法进行管理。
6.2.2 结构
策略模式的主要角色如下:
- 抽象策略(Strategy)类:这是一个抽象角色,通常由一个接口或者抽象类实现。此角色给出所有的具体策略类所需的接口。
- 具体策略(Concrete Strategy)类:实现了抽象策略定义的接口,提供具体的算法实现或行为。
- 环境(Context)类:持有一个策略类的引用,最终给客户端调用。
6.2.3 案例实现
【例】促销活动
一家百货公司在定年度的促销活动。针对不同的节日(春节、中秋节、圣诞节)推出不同的促销活动,有促销员将促销活动展示给客户。类图如下:
代码如下:
定义百货公司所有促销活动的共同接口:
1 | //为春节准备的促销活动A |
定义环境角色(Context):用于连接上下文,即把促销活动推销给客户,这里可以理解为销售员。
1 | public class SalesMan { |
6.2.4 优缺点
优点:
策略类之间可以自由转换
由于策略类都实现同一个接口,所以使他们之间可以自由切换
易于扩展
增加一个新的策略只需要添加一个具体的策略类即可,基本不需要改变原有的代码,符合“开闭原则”
避免使用多重条件选择语句(if else),充分体现了面向对象设计思想
缺点:
- 客户端必须知道所有的策略类,并自行决定使用哪一个策略类
- 策略模式将造成产生很多策略类,可以通过使用享元模式在一定程度上减少对象的数量。
6.2.5 使用场景
- 一个系统需要动态地在几种算法中选择一种时,可将每个算法封装到策略类中。
- 一个类定义了多种行为,并且这些行为在这个类的操作中以多个条件语句的形式出现,可将每个条件分支移入它们各自的策略类中以代替这些条件语句。
- 系统中各算法彼此完全独立,且要求对客户隐藏具体算法的实现细节时。
- 系统要求使用算法的客户不应该知道其操作的数据时,可使用策略模式来隐藏与算法相关的数据结构。
- 多个类只区别在表现行为不同,可以使用策略模式,在运行时动态选择具体要执行的行为。
6.2.6 再再再再举个栗子
6.2.6.1 如何使用策略模式解决大量使用if else 或 大量switch问题
现在大多数网站的登录方式不仅限于官网注册。所以许多平台都会集成微信、QQ、手机号登录。但是因为登录渠道的不同,所对应的处理逻辑也是不相同的,所以我们要根据登录方式的不同处理相应的逻辑。
1 | /** |
这样做的话,很显然代码显得不整洁。并且在后续需要增加其他登录方式时,处理方式只能是在后续增加else if ,所以我们这时候可以用策略模式来对不同的登录渠道进行不同的逻辑处理。
俺们移动到cn.swifthealth.strategy.demo中
看完demo,我们可以看一看监管平台代码。