常用设计模式之白话精简理解及应用-上

前置条件

基础知识

学习设计模式之前我们需要具备一些基础知识,首先需要熟悉面向对象软件开发经历的三个阶段,即OOA(面向对象分析)、OOD(面向对象设计)、OOP(面向对象编程)。

  • OOP中有两个最基础概念是类和对象,还有封装、继承、多态、抽象四大面向对象编程的特性。
  • OOAD里需要会掌握使用UML(统一建模语言,简单、统一、图形化表达软件设计中动态和静态信息)常用的图如用例图、类图、状态图、活动图、时序图、构建图、部署图,UML是我们用来描述面向对象或设计模式的设计思路。

UML2.0

UML2.0包括以下14类图,本篇只是提出但不详细学习这些图,需要各自掌握类、接口、类图,并能运用自如画图,后续我们有时间再专门针对UML详细展开。

image-20220102173543834

类图表示方法

  • 类使用包含类名、属性和方法且带有分割线的矩阵来表示,如下图Employee类,它包含name,age和adress这三个属性,以及work()方法

image-20220108132443249

  • 属性/方法名称前加的加号和减号表示了这个属性/方法的可见性,UML类图中表示可见符号有三种:
    • +:表示public
    • -:表示private
    • #:表示protected
  • 属性的完整表示方式是:
    • 可见性 名称 : 类型 [ = 缺省值 ]
  • 方法的完整表示方式是:
    • 可见性 名称(参数列表) [ : 返回类型],注意:
    • 中括号中的内容表示可选
    • 也有将类型放在变量名后面,返回类型放在方法名前面

类与类之间关系

在软件设计中类与类之间关系包括泛化、 实现、组合 、 聚合 、 关联 、 依赖 。

  • 泛化、实现依赖关系强度相同,这两种关系也是依赖最强的系;泛化体现的是类与类的纵向关系,是父类与子类的继承关系,是is-a关系,采用实线+空心三角箭头;实现体现的是类与接口的纵向关系,实现采用虚线+空心三角箭头

  • 其他四种关系体现的是类和类、或者类与接口的横向关系,较难区分,强弱程度依次为:组合>聚合>关联>依赖,依赖的关系程度最强。

    • 依赖关系是一种使用的关系,也是一种临时性关系,通常在代码层面体现为某个类通过局部变量、方法的参数、静态方法来调用访问另一个类(被依赖的类)中某些方法;采用带箭头虚线,箭头指向被依赖的类
    • 关联关系是对象之间的一种引用关系,是类与类之间最常用的一种关系;分为一般关联、组合、聚合关系;在代码中通常将一个类的对象作为另外一个类的成员变量(全局变量)来实现。
      • 一般关联:也即是通常说的关联关系,可以单向的,也可以双向的;双向关联采用两个箭头或者没有箭头的实线,单向关联采用带一个箭头的实线,箭头从使用类指向被关联的类。
      • 聚合:关联关系中的一种,整体和部分关系,也即是has-a关系,部分可以脱离整体单独存在。采用带空心菱形的实线,菱形指向整体
      • 组合:也是关联关系中的一种,整体和部分关系,也即是contains-a关系是一种更强烈的聚合关系,部分不能脱离整体单独存在。采用带实心菱形的实线,菱形指向整体

    下面通过一张图涵盖上面6种类之间的关系的运用示例并加深理解。

image-20220102235219949

七大设计原则

总体原则

软件设计有七大设计原则,分别是开闭原则、单一职责原则、接口隔离原则、里氏替换原则、依赖倒置原则、合成复用原则、迪米特法则,设计原则作为设计模式的指导思想,而设计模式则是基于设计原则实践发展沉淀的经验,目的是为了提高程序可靠性、可维护性、可复用性、可扩展性,实现面向对象编程的解耦,类与类做到低耦合高内聚;软件编程过程需综合考虑并尽量遵守这些设计原则。引用一句口诀:访问加限制、函数要节俭、依赖不允许、动态加接口、父类要抽象、扩展不更改。

开闭原则

定义

开闭原则(Open-Closed Principle,OCP)是规范我们程序员编程最基本、最重要的原则,强调对扩展开放、对修改关闭,即软件实体应尽量在不修改原有代码的情况下进行扩展。开闭原则是其他设计原则的总纲,是面向对象的可复用设计的首要基石,其他设计原则本质是围绕开闭原则在不同角度去展开。

例子

下面举一个违反开闭原则的例子:支持显示各种类型的图表,如饼状图和柱状图等,类图设计如下:

image-20220103201305985

在ChartDisplay类的display()方法中有如下if else if的处理逻辑代码

if (type.equals("pie")) {
PieChart chart = new PieChart();
chart.display();
}
else if (type.equals("bar")) {
BarChart chart = new BarChart();
chart.display();

在上面ChartDisplay类代码中如果需要增加一个新的图表类如折线图LineChart,则需要修改ChartDisplay类的display()方法的源代码,增加新的判断逻辑,这样就违反了开闭原则;比如可采用策略模式的设计模式来解决上面这种违反开闭原则会变化if else的问题。

单一职责原则

概述

  • 单一职责(Single Responsibility Principle,SRP),一个类只有一个引起它变化的原因。
  • 主要是控制类的粒度大小实现高内聚和低耦合,它是最简单但又是最难运用的原则,单一职责是指如何定义一个模块、类、方法和实现其封装,可以说是比较依赖经验。
  • 我们要根据需求和实际情况来灵活运用单一职责原则,需要设计人员有较强的面向对象业务分析能力和丰富实践经验进行职责分离,将不同变化原因的职责封装到不同的类中。
  • 一个类不要太累,如果类承担职责过多则复用的可能性就越小,且多职责耦合一起当其中一个或几个职责发生变化可能影响其它职责的运行。
  • 在实际过程中如果生搬硬套会引起类的增多,添加额外的维护成本,其实是很难全面做到类级别单一职责原则,如果代码的逻辑足够简单时,我们可以在代码级别违反单一职责原则,当类中的方法数量少,并且业务逻辑不是特别复杂时,可以降级到方法级别单一原则。

反例

吃类Eat.java,如果后续增加吃的方式那么就需要在Eat类修改,这样就有可能影响了原有功能,违反开闭原则。

package cn.itxs.principle.srp;

public class Eat {
    public void doEat(String animal){
        if ("狮子".equals(animal)){
            System.out.println(animal+"在大口吃肉!");
        }else if ("黄牛".equals(animal)) {
            System.out.println(animal+"在细嚼慢咽吃草!");
        }
    }
}

测试类SrpMain.java

package cn.itxs.principle.srp;

public class SrpMain {

    public static void main(String[] args) {
        Eat animal = new Eat();
        animal.doEat("狮子");
        animal.doEat("黄牛");
    }
}

正例

  • 类级别

吃肉类EatMeet.java

package cn.itxs.principle.srp;

public class EatMeet {
    public void doEat(String animal){
        System.out.println(animal+"在大口吃肉!");
    }
}

吃草类EatGrass.java

package cn.itxs.principle.srp;

public class EatGrass {
    public void doEat(String animal){
        System.out.println(animal+"在细嚼慢咽吃草!");
    }
}

测试类SrpMain.java

package cn.itxs.principle.srp;

public class SrpMain {

    public static void main(String[] args) {
        EatMeet animalMeet = new EatMeet();
        animalMeet.doEat("狮子");

        EatGrass animalGrass = new EatGrass();
        animalGrass.doEat("黄牛");
    }
}
  • 方法级别

吃类Eat.java

package cn.itxs.principle.srp;

public class Eat {
    public void doEatMeet(String animal){
        System.out.println(animal+"在大口吃肉!");
    }

    public void doEatGrass(String animal){
        System.out.println(animal+"在细嚼慢咽吃草!");
    }
}

测试类SrpMain.java

package cn.itxs.principle.srp;

public class SrpMain {
    public static void main(String[] args) {
        Eat animal = new Eat();
        animal.doEatMeet("狮子");
        animal.doEatGrass("黄牛");        
    }
}

接口隔离原则

概述

  • 上面单一职责原则强调的是职责,站在业务逻辑的角度;接口隔离原则(Interface Segregation Principle,ISP)则是强调接口的方法尽量少。
  • 设计接口精简单一接口,可理解为接口的单一职责;使用多个专门的接口比使用单一的总接口要好,也即是接口粒度最小化,不要实现与之无关的接口包括空实现。
  • 接口是用来扩展类的功能,凸显的是功能方法层面,其中抽象化也是开闭原则的关键

反例

Behavior行为接口

package cn.itxs.principle.isp;

public interface Behavior {
    void work();
    void eat();
}

Man类实现行为接口

package cn.itxs.principle.isp;

public class Man implements Behavior{
    @Override
    public void work() {
        System.out.println("man do work!");
    }

    @Override
    public void eat() {
        System.out.println("man eat something!");
    }
}

Robot类实现行为接口

package cn.itxs.principle.isp;

public class Robot implements Behavior{
    @Override
    public void work() {
        System.out.println("robot do work!");
    }
	//机器人不需要吃饭,空实现
    @Override
    public void eat() {}
}

正例

WorkBehavior接口

package cn.itxs.principle.isp;

public interface WorkBehavior {
    void work();
}

EatBehavior接口

package cn.itxs.principle.isp;

public interface EatBehavior {
    void eat();
}

Man类实现Work和Eat行为接口

package cn.itxs.principle.isp;

public class Man implements WorkBehavior,EatBehavior{
    @Override
    public void work() {
        System.out.println("man do work!");
    }

    @Override
    public void eat() {
        System.out.println("man eat something!");
    }
}

Robot类只实现WorkBehavior接口

package cn.itxs.principle.isp;

public class Robot implements WorkBehavior{
    @Override
    public void work() {
        System.out.println("robot do work!");
    }
}

里氏替换原则

概述

  • 里氏替换原则(Liskov Substitution principle,LSP)强调的是设计与实现要依赖于抽象而非具体;子类只能去扩展基类,而不是隐藏或者覆盖基类。
  • 里氏替换原则是实现开闭原则的一个方式,是描述类之类关系原则,强调不要破坏继承体系,不要轻易去修改父类的方法,表现的结果是所有使用父类的地方用子类替换后程序依然能够正常的运行;
  • 里氏替换原则可以用来规范我们对于继承的使用,保证了父类的复用性,同时也能够降低系统出错误的故障,防止误操作,最终目的是为了增强程序健壮性。

反例

计算器类Caculator.java

package cn.itxs.principle.lsp;

public class Caculator {
    public int add(int a,int b){
        return a+b;
    }
}

特殊计算器类SpecialCaculator.java

package cn.itxs.principle.lsp;

public class SpecialCaculator extends Caculator{
    @Override
    public int add(int a, int b) {
        return a-b;
    }
}

测试类ISPMain.java

package cn.itxs.principle.lsp;

public class ISPMain {
    public static void main(String[] args) {
        Caculator caculator = new SpecialCaculator();
        System.out.println("100+20="+caculator.add(100, 20));
    }
}

输出结果为100+20=80,我们发现原来运行正常的加法功能发生了错误,原因子类SpecialCaculator可能无意重写Caculator的add方法,造成原本运行加法功能的代码调用了类SpecialCaculator的重写后的方法而导致出现了错误。

正例

特殊计算器类SpecialCaculator.java

package cn.itxs.principle.lsp;

public class SpecialCaculator extends Caculator{
    public int sub(int a, int b) {
        return a-b;
    }
}

测试类ISPMain.java

package cn.itxs.principle.lsp;

public class ISPMain {
    public static void main(String[] args) {
        SpecialCaculator specialCaculator = new SpecialCaculator();
        System.out.println("100+20="+specialCaculator.add(100, 20));
        System.out.println("80-30="+specialCaculator.sub(80, 30));
    }
}

依赖倒置原则

概述

  • 依赖倒置原则(Dependence Inversion Principle,DIP)指的是面向接口编程,或者说是抽象编程和思维。抽象不应该依赖细节、细节依赖抽象。
  • 依赖倒置原则是是开闭原则的基础,主要体现在成员变量、方法的参数、方法的返回值、局部变量这些方面进行依赖倒置,只要是声明的类型都基本建议使用接口或者抽象类型来声明,而不要使用实现类型声明;依赖倒置原则实现的3种方式如下:
    • 通过构造方法声明依赖对象。
    • 设值注入(setter注入),在类中通过Setter方法声明依赖关系。
    • 接口注入,在接口方法中声明依赖对象。

反例

Doc文档读取类ReadDoc.java

package cn.itxs.principle.dip;

public class ReadDoc {
    public void read(){
        System.out.println("read doc!");
    }
}

Excel文件读取类ReadExcel.java

package cn.itxs.principle.dip;

public class ReadExcel {
    public void read(){
        System.out.println("read excel!");
    }
}

ReadXml文档读取类ReadXml.java

package cn.itxs.principle.dip;

public class ReadXml {
    public void read(){
        System.out.println("read xml!");
    }
}

读取工具类ReadTool.java

package cn.itxs.principle.dip;

public class ReadTool {
    public void read(ReadXml r){
        r.read();
    }

    public void read(ReadDoc r){
        r.read();
    }

    public void read(ReadExcel r){
        r.read();
    }
}

上面的设计如果需要扩展读取其他文件内容,需要在ReadTool类中增加相应类型的read方法,这就违反了开闭原则。

正例

先设计一个读接口

package cn.itxs.principle.dip;

public interface Readable {
    void read();
}

然后Excel、Doc、Xml实现类都实现Readable读接口

public class ReadExcel implements Readable{
    @Override
    public void read(){
        System.out.println("read excel!");
    }
}
package cn.itxs.principle.dip;

public class ReadXml implements Readable{
    @Override
    public void read(){
        System.out.println("read xml!");
    }
}
package cn.itxs.principle.dip;

public class ReadDoc implements Readable{
    @Override
    public void read(){
        System.out.println("read doc!");
    }
}

下面我们选择set方法设值注入的方式来演示

package cn.itxs.principle.dip;

public class ReadTool {

    private Readable readable;

    public void setreadable(Readable readable) {
        this.readable = readable;
    }

    public void read(){
        readable.read();
    }
}

读取工具类ReadTool.java

package cn.itxs.principle.dip;

public class DIPMain {
    public static void main(String[] args) {
        ReadTool readTool = new ReadTool();
        readTool.setreadable(new ReadXml());
        readTool.read();
        readTool.setreadable(new ReadExcel());
        readTool.read();
        readTool.setreadable(new ReadDoc());
        readTool.read();
    }
}

这样我们依赖的是抽象,增加读取类型的时候只需要增加实现类即可,ReadTool就无需再进行修改了。

合成复用原则

概述

  • 合成复用原则(Composite Reuse Principle,CRP)也是描述类之类关系原则,尽量使用对象组合而不是使用继承来达到复用的目的。
  • 合成复用原则从执行层面也即是说要优先考虑使用组合或聚合关系的复用,次用或少用继承关系复用。而如果只能使用继承关系那就要遵循里氏替换原则。
  • 继承作为面向对象三大特性之一,在给程序设计带来巨大便利的同时,也带来了弊端。比如使用继承会给程序带来侵入性,程序的可移植性降低,增加了对象间的耦合性,如果一个类被其他的类所继承,则当这个类需要修改时,必须考虑到所有的子类,并且父类修改后,所有涉及到子类的功能都有可能会产生故障。
  • 继承是静态复用,而通过聚合或组合的复用是动态复用。所谓的静态复用是在编码阶段已经明确了类之间的关系;动态复用则是在程序运行阶段,根据实际要求注入相应的对象完成复用的,动态复用比静态复用更具有灵活性。

反例

汉堡抽象类Hamburger.java

package cn.itxs.principle.crp;

public abstract class Hamburger {
    abstract void meat();
}

鸡肉汉堡类ChickenHamburger.java

package cn.itxs.principle.crp;

public class ChickenHamburger extends Hamburger{
    @Override
    void meat() {
        System.out.println("鸡肉汉堡");
    }
}

牛肉汉堡类BeefHamburger.java

package cn.itxs.principle.crp;

public class BeefHamburger extends Hamburger{
    @Override
    void meat() {
        System.out.println("牛肉汉堡");
    }
}

芝士鸡肉汉堡类CheeseChickenHamburger.java

package cn.itxs.principle.crp;

public class CheeseChickenHamburger extends ChickenHamburger{
    public void burden(){
        System.out.println("芝士");
        super.meat();
    }
}

黄油鸡肉汉堡类ButterChickenHamburger.java

package cn.itxs.principle.crp;

public class ButterChickenHamburger extends ChickenHamburger{
    public void burden(){
        System.out.println("黄油");
        super.meat();
    }
}

芝士牛肉汉堡类CheeseBeefHamburger.java

package cn.itxs.principle.crp;

public class CheeseBeefHamburger extends BeefHamburger{
    public void burden(){
        System.out.println("芝士");
        super.meat();
    }
}

黄油牛肉汉堡类ButterBeefHamburger.java

package cn.itxs.principle.crp;

public class ButterBeefHamburger extends BeefHamburger{
    public void burden(){
        System.out.println("黄油");
        super.meat();
    }
}

测试类,输出芝士牛肉汉堡

package cn.itxs.principle.crp;

public class CRPMain {
    public static void main(String[] args) {
        CheeseBeefHamburger hamburger = new CheeseBeefHamburger();
        hamburger.burden();
    }
}

上图可以看出继承关系实现产生了大量的子类,而且增加了新的肉类或是颜色都要修改源码,这违背了开闭原则,显然不可取,但如果改用组合关系实现就很好的解决了以上问题,图例

正例

采用组合或聚合复用方式,第一步先将将配料Burden抽象为接口,并实现黄油,芝士两个配料类,第二步将Burden对象组合在汉堡类Hamburger中,最终我们用更少的类就可以实现上面的功能(ButterChickenHamburger、ButterBeefHamburger、CheeseChickenHamburger、CheeseBeefHamburger);而且以后当增加猪肉、虾肉汉堡或者新增加奶酪配料都不需要修改原来的代码,只要增加对应的实现类即可,符合开闭原则。

Burden接口Burden.java

package cn.itxs.principle.crp;

public interface Burden {
    void burdenKind();
}

芝士类Cheese.java

package cn.itxs.principle.crp;

public class Cheese implements Burden{
    @Override
    public void burdenKind() {
        System.out.println("芝士");
    }
}

黄油类Cheese.java

package cn.itxs.principle.crp;

public class Butter implements Burden{
    @Override
    public void burdenKind() {
        System.out.println("黄油");
    }
}

汉堡抽象类Hamburger.java

package cn.itxs.principle.crp;

public abstract class Hamburger {
    abstract void meat();
    private Burden burden;

    public Burden getBurden() {
        return burden;
    }

    public void setBurden(Burden burden) {
        this.burden = burden;
    }
}

测试类同样输出芝士牛肉汉堡

package cn.itxs.principle.crp;

public class CRPMain {
    public static void main(String[] args) {
        BeefHamburger beefHamburger = new BeefHamburger();
        Burden cheese = new Cheese();
        beefHamburger.setBurden(cheese);
        beefHamburger.getBurden().burdenKind();
        beefHamburger.meat();
    }
}

迪米特法则

概述

  • 迪米特法则(Law of Demeter,LoD)核心是强调要降低类与类之间耦合度,尽可能减少与其他实体相互作用,提高模块相对独立性。
  • 迪米特法则也叫作最少知识原则(The Least Knowledge Principle),一个类对于其他类知道的越少越好,就是说一个对象应当对其他对象有尽可能少的了解,只和直接的朋友通信,不和陌生人说话。在类中朋友一般包含如下:
    • 当前对象本身this。
    • 以参数形式传入当前对象方法中的对象。
    • 当前对象的成员对象。
    • 如果当前对象的成员是一个集合,那么集合的元素也是朋友。
    • 当前对象所创建的对象。
  • 从依赖者角度来说只依赖应该依赖的对象;从被依赖角度来说只暴露应该暴露的方法。

反例

短视频类Video.java

package cn.itxs.principle.lod;

public class Video {
    private String title;

    public Video(String title) {
        this.title = title;
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }
}

抖音app类App.java

package cn.itxs.principle.lod;

public class App {
    public void play(Video video){
        System.out.println("播放短视频,标题为"+video.getTitle());
    }
}

手机类Phone.java

package cn.itxs.principle.lod;

public class Phone {
    private App app = new App();
    private Video video = new Video("大话设计模式");

    public void playVideo(){
        app.play(video);
    }
}

测试类,输出“播放短视频,标题为大话设计模式”

package cn.itxs.principle.lod;

public class LODMain {
    public static void main(String[] args) {
        Phone phone = new Phone();
        phone.playVideo();
    }
}

短视频和抖音app对象都在手机上,手机和短视频是没有太大联系,短视频对象不应该在手机类里面。应该修改为手机里面有抖音APP,抖音APP里面有短视频,这才符合迪米特法则;手机和抖音APP是朋友,抖音APP和短视频是朋友,在软件设计里朋友的朋友不是朋友,也就是手机和短视频不是朋友,所以它们不应该有直接交集。

正例

其他不变,只修改抖音App和Phone类,抖音App类App.java

package cn.itxs.principle.lod;

public class App {
    private Video video = new Video("大话设计模式");
    public void play(){
        System.out.println("播放短视频,标题为"+video.getTitle());
    }
}

手机类Phone.java

package cn.itxs.principle.lod;

public class Phone {
    private App app = new App();
    public void playVideo(){
        app.play();
    }
}

常见设计模式

分类

  • 根据目的来分

    • 创建型模式:作用于对象的创建,将对象的创建与使用分离。其中囊括了单例、原型、工厂方法、抽象工厂、建造者5 种创建型模式。
    • 结构型模式:将类或对象按某种布局组成更大的结构,其中以代理、适配器、桥接、装饰、外观、享元、组合 7 种结构型模式为主。
    • 行为型模式:作用于类或对象之间相互协作共同完成单个对象无法单独完成的任务,以及怎样分配职责。主要包含了模板方法、策略、命令、职责链、状态、观察者、中介者、迭代器、访问者、备忘录、解释器等 11 种行为型模式。
  • 作用范围来分

    • 类模式:用于处理类与子类之间的关系,这些关系通过继承来建立,在编译时刻便确定下来了。工厂方法、(类)适配器、模板方法、解释器均属于该模式。
    • 对象模式:用于处理对象之间的关系,这些关系可以通过组合或聚合来实现,在运行时刻是可以变化的,更具动态性。除了以上 4 种,其他的都是对象模式。
  • 具体功能分类表

    image-20220108180510545

设计模式是经过大量实践和归纳总结的范式,是可以直接运用于软件开发中,设计模式是一种风格,从某种意义上不止上面23种,比如像MVC也可算是一个设计模式。

单例模式

概述

  • 单例模式确保某个类只有一个实例,而且自行实例化并向整个系统提供这个实例。
    • 单例类只能有一个实例。
    • 单例类必须自己创建自己的唯一实例。
    • 单例类必须给所有其他对象提供这一实例。
  • 单例模式保证了全局对象的唯一性,比如系统启动读取配置文件就需要单例保证配置的一致性;其他的如线程池、缓存、日志对象、对话框、打印机、显卡的驱动程序对象也常被设计成单例。
  • 单例模式写法只要有两大类饿汉式和懒汉式。饿汉式由JVM加载类后初始化不存在线程安全问题,但会浪费内存一般也不推荐使用。
  • 常见使用场景
    • 需要频繁的进行创建和销毁的对象。
    • 创建对象时耗时过多或耗费资源过多,但又经常用到的对象。
    • 工具类对象。
    • 频繁访问数据库或文件的对象。

饿汉式

package cn.itxs.pattern.singleton;

public class HungerSingleton {
    private static final HungerSingleton instance = new HungerSingleton();
    private HungerSingleton(){

    }
    public HungerSingleton getInstance(){
        return instance;
    }
}

双层检测锁

package cn.itxs.pattern.singleton;

public class DoubleCheckSingleton {
    //volatile有两个作用,其一保证instance的可见性,其二禁止jvm指令重排序优化
    private static volatile DoubleCheckSingleton instance = null;
    private DoubleCheckSingleton(){

    }
    public static DoubleCheckSingleton getInstance(){
        //第一个if主要用于非第一次创建单例后可以直接返回的性能优化
        if (instance == null){
            //采用同步代码块保证线程安全
            synchronized (DoubleCheckSingleton.class){
                if (instance == null){
                    //这一行jvm内部执行多步,1先申请堆内存,2对象初始化,3对象指向内存地址;2和3由于jvm有指令重排序优化所以存在3先执行可能会导致instance还没有初始化完成,其他线程就得到了这个instance不完整单例对象的引用值而报错。
                    instance = new DoubleCheckSingleton();
                }
            }
        }
        return instance;
    }
}

静态内部类

静态内部类的方式满足懒加载,没有线程安全问题,同时也十分高效,代码也容易让人理解

package cn.itxs.pattern.singleton;

public class InnerSingleton {
    private InnerSingleton(){

    }
    private static class SingletonHolder{
        private static final InnerSingleton instance = new InnerSingleton();
    }
    public static InnerSingleton getInstance(){
        return SingletonHolder.instance;
    }
}

反射攻击

不管是上面的饿汉式、懒汉式的双层检测锁、静态内部类也不能阻止反射攻击导致单例被破坏,虽然我们一开始都对构造器进行了私有化处理,但Java本身的反射机制却还是可以将private访问权限改为可访问,依旧可以创建出新的实例对象而破坏单例。测试类如下

package cn.itxs.pattern.singleton;

import java.lang.reflect.Constructor;

public class SingletonMain {
    public static void main(String[] args) {
        //反射攻击
        reflectAttack();
    }

    public static void reflectAttack() {
        System.out.println("正常单例获取对象");
        InnerSingleton instanceA = InnerSingleton.getInstance();
        System.out.println(instanceA);
        InnerSingleton instanceB = InnerSingleton.getInstance();
        System.out.println(instanceB);
        Constructor<InnerSingleton> constructor = null;
        try {
            constructor = InnerSingleton.class.getDeclaredConstructor(null);
            constructor.setAccessible(true);
            InnerSingleton instanceC = constructor.newInstance();
            System.out.println("反射攻击后单例获取对象");
            System.out.println(instanceC);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

image-20220108193051159

序列化和反序列化破坏

饿汉式、懒汉式的双层检测锁、静态内部类如果都实现序列化接口,也会被序列化和反序列化破坏单例。

在DoubleCheckSingleton.java中增加实现Serializable接口

image-20220108193944748

测试类代码如下:

package cn.itxs.pattern.singleton;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;

public class SingletonMain {
    public static void main(String[] args) {
        //序列化和反序列化破坏
        serializeDestroy();
    }

    public static void serializeDestroy() {
        System.out.println("正常单例获取对象");
        DoubleCheckSingleton instanceA = DoubleCheckSingleton.getInstance();
        System.out.println(instanceA);
        DoubleCheckSingleton instanceB = DoubleCheckSingleton.getInstance();
        System.out.println(instanceB);
        try {
            ByteArrayOutputStream bos=new ByteArrayOutputStream();
            ObjectOutputStream oos=new ObjectOutputStream(bos);
            oos.writeObject(instanceB);

            ByteArrayInputStream bis=new ByteArrayInputStream(bos.toByteArray());
            ObjectInputStream ois=new ObjectInputStream(bis);
            DoubleCheckSingleton instanceC = (DoubleCheckSingleton) ois.readObject();
            System.out.println("序列化破坏后单例获取对象");
            System.out.println(instanceC);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

image-20220108193647759

这里如何应对序列化和反序列化破坏,只需要在DoubleCheckSingleton.java进行小小的修改 ,添加一个方法readResolve()

image-20220108194343499

再次运行测试类输出后三个单例获取对象都是相同了

image-20220108194229277

枚举

枚举是最简洁、线程安全、不会被反射创建实例的单例实现,《Effective Java》中也表明了这种写法是最佳的单例实现模式。从Constructor构造器中反射实例化对象方法newInstance的源码可知:反射禁止了枚举对象的实例化,也就防止了反射攻击,不用自己在构造器实现复杂的重复实例化逻辑了。

image-20220109005325937

EnumSingleton.java

package cn.itxs.pattern.singleton;

public enum EnumSingleton {
    INSTANCE;
}

测试类

package cn.itxs.pattern.singleton;

import java.lang.reflect.Constructor;

public class SingletonMain {
    public static void main(String[] args) {
        //反射攻击
        reflectAttack();
    }

    public static void reflectAttack() {
        System.out.println("正常单例获取对象");
        EnumSingleton instanceA = EnumSingleton.INSTANCE;
        System.out.println(instanceA);
        EnumSingleton instanceB = EnumSingleton.INSTANCE;
        System.out.println(instanceB);
        Constructor<EnumSingleton> constructor = null;
        try {
            constructor = EnumSingleton.class.getDeclaredConstructor(null);
            constructor.setAccessible(true);
            EnumSingleton instanceC = constructor.newInstance();
            System.out.println("反射攻击后单例获取对象");
            System.out.println(instanceC);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

image-20220109010524358

模板方法

概述

  • 模板方法模式在一个方法中定义一个算法骨架,并将某些步骤推迟到子类中实现。模板方法模式可以让子类在不改变算法整体结构的情况下,重新定义算法中的某些步骤。
    • 算法骨架就是“模板”
    • 包含算法骨架的方法就是模板方法
  • 一个抽象类公开定义了执行它的方法的方式/模板。它的子类可以按需要重写方法实现,但调用将以抽象类中定义的方式进行。
  • 常见使用场景
    • 多个子类有公有的方法,并且逻辑基本相同时。
    • 重要、复杂的算法,可将不变的核心算法设计为模板方法,周边的相关细节功能、可变的行为由各个子类实现。
    • 重构时,模板方法模式是一个经常使用的模式,把相同代码抽取到父类,然后通过钩子函数约束其行为。

代码示例

游戏抽象类Game.java

package cn.itxs.pattern.template;

public abstract class Game {
    public final void play(){
        init();
        startPlay();
        endPlay();
    }

    //初始化游戏
    abstract void init();
    //开始游戏
    abstract void startPlay();
    //结束游戏
    abstract void endPlay();
}

英雄联盟游戏类LoL.java

package cn.itxs.pattern.template;

public class LoL extends Game{
    @Override
    void init() {
        System.out.println("英雄联盟手游初始化");
    }

    @Override
    void startPlay() {
        System.out.println("英雄联盟手游激烈进行中");
    }

    @Override
    void endPlay() {
        System.out.println("英雄联盟手游游戏结束,恭喜三连胜");
    }
}

和平精英游戏类Game.java

package cn.itxs.pattern.template;

public class Peace extends Game{
    @Override
    void init() {
        System.out.println("和平精英手游初始化");
    }

    @Override
    void startPlay() {
        System.out.println("和平精英手游激烈进行中");
    }

    @Override
    void endPlay() {
        System.out.println("和平精英手游游戏结束,大吉大利今晚吃鸡");
    }
}

测试类

package cn.itxs.pattern.template;

public class TemplateMain {
    public static void main(String[] args) {
        //玩英雄联盟游戏
        Game lol = new LoL();
        lol.play();
        //玩和平精英游戏
        Game peace = new Peace();
        peace.play();
    }
}

image-20220109020556722

**本人博客网站 **IT小神 www.itxiaoshen.com