0%

设计模式学习

设计模式

设计模式概述

  1. 所谓设计模式就是面向对象编程中遇到的各种现成的套路,使用合适的设计模式可以提高程序的可读性可重用性可扩展性

  2. 设计模式一书中提到有23种设计模式,分为以下三类

    image-20210516204549657
    • 所有的23种设计模式都要满足以下七大设计原则(实际上就是面向对象设计的五大原则+迪米特原则+合成复用原则)

      • 单一职责

        1. 一个类,最好只做一件事,只有一个引起它的变化功能方法,降低功能依赖
      • 开闭原则

        1. 类是可扩展的,但是不可修改的
          1. 对扩展开放,意味着有新的需求或变化时,可以对现有代码进行扩展,以适应新的情况。
          2. 对修改封闭,意味着类一旦设计完成,就可以独立完成其工作,而不要对其进行任何尝试的修改
        2. 实现开开放封闭原则的核心思想就是对抽象编程,而不对具体编程,因为抽象相对稳定。 让类依赖于固定的抽象,所以修改就是封闭的; 而通过面向对象的继承和多态机制,又可以实现对抽象类的继承,通过覆写其方法来改变固有行为,实现新的拓展方法,所以就是开放的
      • 里氏替换原则

        1. 子类必须能够替换其基类,同时,这一约束反过来则是不成立的,子类可以替换基类,但是基类不一定能替换子类
        2. 里氏替换原则描述的实际上就是面向对象编程过程中的继承这一特性的使用原则
      • 依赖倒置原则

        1. 依赖于抽象,面向接口编程
        2. 具体而言就是高层模块不依赖于底层模块(具体实现模块之间不相互依赖,而是统一依赖于抽象),二者都同依赖于抽象;抽象不依赖于具体,具体依赖于抽象
        3. 抽象是整个程序的骨架,必须稳定,而实现就是骨架上的血肉
      • 接口隔离原则

        1. 使用多个小的专门的接口,而不要使用一个大的总接口。强迫实现类去使用胖接口中根本用不到的方法与属性会带来污染以及隐患,如果不得以修改胖接口,那么会导致一系列的修改
        2. 分离的手段主要有以下两种:
          1. 委托分离,通过增加一个新的类型来委托客户的请求,隔离客户和接口的直接依赖,但是会增加系统的开销
          2. 多重继承分离,通过接口多继承来实现客户的需求,这种方式是较好的
      • 迪米特原则(模块之间降低依赖)

        • 每个模块对其他模块都要尽可能少地了解和依赖,降低代码耦合度
      • 合成复用原则

        • 尽量使用组合(has-a)/聚合(contains-a)而不是继承(is-a)达到软件复用的目的(类之间的关系大概有五种:继承、依赖、关联、聚合、组合
        • 在确定使用向上转型时再使用继承,否则使用组合或者聚合,组合中组合类与被包装类之间是松耦合的,而继承关系中的父类与子类是紧耦合的;继承关系是编译时期就确定的,而组合关系可以在运行期被确定,继承关系破坏了封装
        • 组合、聚合关系相当于继承关系的最明显的缺点是创建整体类的对象是需要同步创建被包装类的对象,而继承中,子类创建对象不要求创建父类对象

        类之间的五大关系

        1. 继承 子类继承与实现,继承是一种is-a关系
        2. 依赖 依赖关系在Java中体现为局域变量、方法的形参,或者对静态方法的调用
        3. 关联 关联关系一般使用成员变量来实现
        4. 聚合 聚合是关联关系的一种特例,他体现的是整体与部分、拥有的关系,即has-a的关系,整体与部分之间是可分离的,他们可以具有各自的生命周期
          1. 聚合是关联的特例,代码层面上一致,只能从语义上区分
        5. 组合 组合也是关联关系的一种特例,他体现的是一种contains-a的关系,这种关系比聚合更强,也称为强聚合;组合同样体现整体与部分间的关系,但此时整体与部分是不可分的,整体的生命周期结束也就意味着部分的生命周期结束
          1. 组合是关联的特例,代码层面上一致,只能从语义上区分
        6. 参考文章

创造型模式

  • 创造型模式就是用来生产对象的模式

单例模式

  • 解决一个类在内存中仅维持一个对象的要求,比如spring中的默认的bean,springMVC中的controller,或者是dvclab项目中的KeycloakAdapterContainerService,大概是与外部组件的交互服务要维护成单例的,以避免重复初始化浪费资源

单例模式的实现方式

  1. 最简单粗暴的饿汉式

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    public class Singleton{
    // 首先,将 new Singleton() 堵死
    private Singleton() {}
    // 创建私有静态实例,意味着这个类第一次使用(加载)的时候就会进行创建
    // 此处由JVM保证其线程安全性
    // 此显式赋值实际上是在类加载过程中的clinit方法中完成的,可以保证线程安全,并且可以保证只初始化了一次,也就是保证单例
    private static Singleton instance = new Singleton();

    public static Singleton getInstance() {
    return instance;
    }
    // 瞎写一个静态方法。这里想说的是,如果我们只是要调用 Singleton.getDate(...),
    // 本来是不想要生成 Singleton 实例的,不过没办法,已经生成了
    public static Date getDate(String mode) {return new Date();}
    }
    • 饿汉式的缺点,在getDate方法的注释中已经说明了
    • 由JVM保证线程安全与单例(只创建一次实例)
  2. 使用静态内部类(懒汉式

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    public class Singleton {

    //声明为 private 避免调用默认构造方法创建对象
    private Singleton() {
    }

    // 声明为 private 表明静态内部该类只能在该 Singleton 类中被访问
    // 本质上也是懒汉式也就是延迟加载,因为这个静态内部类并不会随着外部类的加载而加载,因为其是静态的
    private static class SingletonHolder {
    // 此处由JVM保证其线程安全性
    // 此显式赋值实际上是在类加载过程中的clinit方法中完成的,可以保证线程安全,并且可以保证只初始化了一次,也就是保证单例,并且又final修饰保证其不被修改
    private static final Singleton INSTANCE = new Singleton();
    }

    public static Singleton getUniqueInstance() {
    return SingletonHolder.INSTANCE;
    }
    }
    • 因为静态内部类的静态修饰符,导致其并没有指向外部类的引用,所以二者关系不太大,外部类除了getUniqueInstance方法也不会包含内部类的引用,因此不会一同被加载,只有调用了getUniqueInstance方法之后,才会加载内部类,并获得初始化后的静态内部类中的静态属性,也就是唯一的实例
    • 同样由JVM保证线程安全与单例(只创建一次实例)
  3. 使用锁的懒汉式

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    public class Singleton {  
    private static Singleton instance;
    private Singleton (){}
    public static synchronized Singleton getInstance() {
    if (instance == null) {
    instance = new Singleton();
    }
    return instance;
    }
    }
    • 使用锁保证了线程安全,但是效率很低,因为大多数情况下都不需要加锁,毕竟只需要创建一次实例后续就再不用创建了
  4. 使用双重验证机制+锁(volatile关键字使用的典型案例)(懒汉式)(实际上是前边的加锁方式的改进,提升加锁的效率,换句话说就是减低锁的粒度,从方法移动到代码块,当然也引来了其他的问题,最大的功臣是volatile的使用,解决了可能出现的未初始化的对象被使用的情况(对象的安全发布问题

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    public class Singleton {

    // 使用volatile修饰是关键的
    private static volatile Singleton instance;
    //声明为 private 避免调用默认构造方法创建对象
    private Singleton() {}
    // 双重锁检验
    public static Singleton getInstance() {
    if (instance == null) { // 第7行
    synchronized (Singleton.class) {
    if (instance == null) {
    instance = new Singleton(); // 第10行
    }
    }
    }
    return instance;
    }
    }
    • volatile发挥的作用(可见性,有序性)

      • 可见性,当if判断时,如果有其他线程成功创建了实例,可以对其他线程可见,进而跳过if-check

      • 虽然第十行处的代码已经是线程安全的,但是但是还是有重排序带来的隐患,分析如下:

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        instance = new Singleton(); // 第10行

        // 可以分解为以下三个步骤
        memory=allocate();// 分配内存 相当于c的malloc
        ctorInstanc(memory) //初始化对象
        s=memory //设置s指向刚分配的地址

        // 上述三个步骤可能会被重排序为 1-3-2,也就是:
        memory=allocate();// 分配内存 相当于c的malloc
        s=memory //设置s指向刚分配的地址
        ctorInstanc(memory) //初始化对象
        • 比如线程A在第10行执行了步骤1和步骤3,但是步骤2还没有执行完。这个时候另一个线程B执行到了第7行,它会判定instance不为空,然后直接返回了一个未初始化完成的instance!
        • s=memory这条指令是volatile变量的写,由于volatile禁止重排序所以不会在前边两条指令之前执行
        • 重排序不是指的Java语句的重排,而是具体执行指令的重排,当一个语句对应多条指令的时候,其内部的指令也有重排序的可能
    • 获得锁之后为什么还要进行if-check,因为等待锁的过程中,前边的线程可能已经获得锁并完成了唯一实例的初始化

    • 这里可能会有以下的误解:在自己的印象中,sync是比volatile作用范围更大的加锁,而new的语句在sync中,难道sync不能保证有序吗?

      • sync可以保证有序,但是不能防止出现指令重排,换句话说,sync与volatile在实现有序性的方法上是不同的,volatile通过内存屏障直接防止指令重排以实现有序性,而sync是通过互斥锁来实现有序性,也就是在单线程内可以执行任意的指令重排,但是其结果对于其他线程来说是一致的,在单例模式的案例中,意思就是,在单个线程执行完sync中的代码块后,其余的线程总能拿到一个实例,但是在这个单个线程执行的瞬间,内部的new关键字允许进行重排序,那么问题就出在第一个if判断处
      • 第一个if判断处针对instance变量进行了判断,并且尤其重要的是该if判断不在sync代码块内,因此指令重排序会影响到其他线程执行此if判断,就会发生上述的描述的情景了
    • final也是可以实现安全发布的,但是因为这里的是懒初始化,以及final的编译原理,所以并不能使用final(编译不通过)

  5. 使用枚举类

    • 和使用静态内部类来实现单例模式一样,都是借用了jvm本身的一些机制,来为单例模式做保证,静态内部类借用jvm来保证线程安全的特性,使用enum实现单例模式凭借的就是enum在jvm中具有唯一实例的特性
    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
    public enum Singleton {
    INSTANCE;
    // 属性举例
    private String name;

    public String getName () {
    return name;
    }

    public void setName (String name) {
    this.name = name;
    }
    }
    public class Tempclass {
    public static void main(String[] args) {
    for (int i =0; i < 100; i++) {
    new Thread(() -> {
    Singleton uniqueInstance = Singleton.INSTANCE;
    System.out.println(uniqueInstance.hashCode());
    }).start();
    }

    }

    }
  • 使用enum来构建单例模式的另外一个好处在于可以避免在反序列化时绕过普通类的private构造方法从而创建出多个实例

    • 当然在反序列化时也有其他的方法来避免破坏单例,比如readResolve方法的实现
  • 很多程序,尤其是Web程序,大部分服务类都应该被视作Singleton,如果全部按Singleton的写法写,会非常麻烦,所以,通常是通过约定让框架(例如Spring)来实例化这些类,保证只有一个实例,调用方自觉通过框架获取实例而不是new操作符,换句话说就是使用spring ioc的时候,就默认按照约定把所有的普通类看做单例,由框架去管理对象形成约束,而不是自己使用new创建对象

工厂模式

  • 所谓工厂模式实际上就是在要创建的类之外创建一个新的类–**工厂类**,由此类提供的静态或者实例方法来提供特定对象的创建

简单工厂模式(只需要一个工厂类负责创建单一的对象(同一继承树下的对象))

  • 简单工厂模式通常就是这样,一个工厂类 XxxFactory,里面有一个静态方法,根据我们不同的参数,返回不同的派生自同一个父类(或实现同一接口)的实例对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public classFoodFactory{

publicstatic Food makeFood(String name) {
if (name.equals("noodle")) {
Food noodle = new LanZhouNoodle();
noodle.addSpicy("more");
return noodle;
} else if (name.equals("chicken")) {
Food chicken = new HuangMenChicken();
chicken.addCondiment("potato");
return chicken;
} else {
return null;
}
}
}
  • Spring 中的 BeanFactory 使用简单工厂模式,产生 Bean 对象,除此之外还有Executor框架中的Executors工厂类创建线程池
  • 专门生产CPU的工厂

工厂模式(需要有多个工厂创建多类对象,在工厂上抽象出一个工厂类接口,由实现类决定创建什么类型的对象)

  • 相对于简单工厂模式,当需要有多个工厂时(需要做类的划分,比如将电脑元件划分为CPU主板),使用工厂模式,先选择工厂,再选择使用工厂创建什么对象
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
public interface FoodFactory{
Food makeFood(String name);
}
public class ChineseFoodFactoryimplementsFoodFactory{

@Override
public Food makeFood(String name) {
if (name.equals("A")) {
return new ChineseFoodA();
} else if (name.equals("B")) {
return new ChineseFoodB();
} else {
return null;
}
}
}
public class AmericanFoodFactoryimplementsFoodFactory{

@Override
public Food makeFood(String name) {
if (name.equals("A")) {
return new AmericanFoodA();
} else if (name.equals("B")) {
return new AmericanFoodB();
} else {
return null;
}
}
}

// ChineseFoodA、ChineseFoodB、AmericanFoodA、AmericanFoodB 都派生自 Food
public class APP{
public static void main(String[] args) {
// 先选择一个具体的工厂
FoodFactory factory = new ChineseFoodFactory();
// 由第一步的工厂产生具体的对象,不同的工厂造出不一样的对象
Food food = factory.makeFood("A");
}
}
  • Spring 的 FactoryBean 接口的 getObject 方法也是工厂方法
  • 生产电脑配件的工厂

抽象工厂模式

  • 当有了产品族的概念,或者说需要做进一步的类别划分时(将CPU划分为Intel CPUAMD CPU),此时为了保证对应性(兼容性)将原有的分类化归到更大范围的产品族这个概念中

    image-20210516222750544

  • 抽象工厂模式指提供一个创建一系列相关或相互依赖对象的接口,无需指定它们的具体类。例子:java.sql.Connection 接口,提供创建StatementPreparedStatement的方法

  • 生产特定品牌的电脑配件的工厂

建造者模式(构建模式)

  • 建造者模式的最大的亮点就是客户端使用的时候可以使用链式调用,这样当可设置的属性比较多时,使用链式调用的构建方式比直接使用构造函数传参更加清晰

    1
    2
    Food food = new FoodBuilder().a().b().c().build();
    Food food = Food.builder().a().b().c().build();
    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
    classUser{
    // 下面是“一堆”的属性
    private String name;
    private String password;
    private String nickName;
    private int age;

    // 构造方法私有化,不然客户端就会直接调用构造方法了
    private User(String name, String password, String nickName, int age) {
    this.name = name;
    this.password = password;
    this.nickName = nickName;
    this.age = age;
    }
    // 静态方法,用于生成一个 Builder,这个不一定要有,不过写这个方法是一个很好的习惯,
    // 有些代码要求别人写 new User.UserBuilder().a()...build() 看上去就没那么好
    publicstatic UserBuilder builder() {
    return new UserBuilder();
    }

    public static class UserBuilder{
    // 下面是和 User 一模一样的一堆属性
    private String name;
    private String password;
    private String nickName;
    private int age;

    privateUserBuilder() {
    }

    // 链式调用设置各个属性值,返回 this,即 UserBuilder
    public UserBuilder name(String name) {
    this.name = name;
    return this;
    }

    public UserBuilder password(String password) {
    this.password = password;
    return this;
    }

    public UserBuilder nickName(String nickName) {
    this.nickName = nickName;
    return this;
    }

    public UserBuilder age(int age) {
    this.age = age;
    return this;
    }

    // build() 方法负责将 UserBuilder 中设置好的属性“复制”到 User 中。
    // 当然,可以在 “复制” 之前做点检验
    public User build() {
    if (name == null || password == null) {
    throw new RuntimeException("用户名和密码必填");
    }
    if (age <= 0 || age >= 150) {
    throw new RuntimeException("年龄不合法");
    }
    // 还可以做赋予”默认值“的功能
    if (nickName == null) {
    nickName = name;
    }
    return new User(name, password, nickName, age);
    }
    }
    }
    • 是由构建者模式的核心就是多了一个Builder类,所有的属性都要先给这个Builder类,由他去执行验证(以及其他功能,比如强制提供某属性以及提供默认属性)和对象的创建
      • 其中链式调用的实现原理就是每一个链都返回this即可
    1
    2
    3
    4
    5
    6
    7
    8
    9
    public classAPP{
    publicstaticvoidmain(String[] args) {
    User d = User.builder()
    .name("foo")
    .password("pAss12345")
    .age(25)
    .build();
    }
    }
  • 构建者模式实际上还是比较繁琐的,使用lombok会比较简单

    1
    2
    3
    4
    5
    6
    7
    @Builder
    classUser{
    private String name;
    private String password;
    private String nickName;
    private int age;
    }

原型模式

  • 其实就是使用clone方法来根据一个已有的对象进行对象clone来生成新的对象,但是Object的clone方法默认只会实现浅拷贝,对于引用类型的属性只会拷贝引用,而不是clone出一个新的对象。需要我们重写clone方法,或者使用序列化再反序列化的方式实现深拷贝
  • 用的不多

结构型模式

  • 通过类和接口间的继承和引用实现创建复杂结构的对象

代理模式

  • 代理模式为其他对象提供一种代理以控制对这个对象的访问。优点是可以增强目标对象的功能,降低代码耦合度,扩展性好。缺点是在客户端和目标对象之间增加代理对象会导致请求处理速度变慢,增加系统复杂度
  • 代理的分类
    • 静态代理:在程序运行前就已经存在代理类的字节码文件,代理类和委托类的关系在运行前就确定了。
    • 动态代理:程序运行期间动态的生成,所以不存在代理类的字节码文件。代理类和委托类的关系是在程序运行时确定(基于反射)
      • Spring AOP与Mybatis等一系列框架都使用了动态代理的技术

适配器模式

  • 适配器模式将一个接口转换成客户希望的另一个接口,使接口不兼容的那些类可以一起工作
  • SpringMVC请求处理流程中的控制器适配器

行为型模式

  • 通过类之间不同通信方式(所谓不同的通信方式比如继承关系、通过中介类)实现不同行为

模板方法模式

  • 模板方法模式是一种行为设计模式,它定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。 模板方法使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤的实现方式
  • spring中的jdbcTemplatehibernateTemplate 等以 Template 结尾的对数据库操作的类,它们就使用到了模板模式;AQS类也使用了模板方法模式

装饰者模式

  • 装饰者模式可以动态地给对象添加一些额外的属性或行为,即需要修改原有的功能,但又不愿直接去修改原有的代码时,设计一个Decorator套在原有代码外面,符合开闭原则
  • 装饰者模式与代理模式从目的上来讲是很类似的,都会提供类似增强的功能,主要区别如下
    • 代理模式是为了实现对被代理对象的控制而实现的,被代理对象可能难以直接获得或者不想暴露给客户端(比如rpc框架中,实际上被代理的对象甚至不在一台服务器上),用户直接调用的实际上是代理对象的方法,再由代理对象去执行被代理对象的方法
    • 装饰者模式主要是为了解决模块功能的问题,避免出现为了增加新的功能而带来的子类爆炸的问题,并且可以实现模块功能之间的任意组合,最常见的就是Java IO的各种类,比如BufferedInputStreamGZIPInputStream等等
      • 装饰者模式的还有一个源码中的案例就是Executors线程池工厂类中提供的一系列包装类

观察者模式

  • 观察者模式是一种对象行为型模式。它表示的是一种对象与对象之间具有依赖关系,当一个对象发生改变的时候,这个对象所依赖的对象也会做出反应Spring 事件驱动模型就是观察者模式很经典的一个应用。Spring 事件驱动模型非常有用,在很多场景都可以解耦我们的代码。比如我们每次添加商品的时候都需要重新更新商品索引,这个时候就可以利用观察者模式来解决这个问题。