详解23种设计模式——第一部分:概述+创建型模式

目录

[1. 概述](#1. 概述)

[2. 创建型模式](#2. 创建型模式)

[2.1 简单(静态)工厂模式](#2.1 简单(静态)工厂模式)

[2.1.1 介绍](#2.1.1 介绍)

[2.1.2 实现](#2.1.2 实现)

[2.2 工厂模式](#2.2 工厂模式)

[2.3 抽象工厂模式](#2.3 抽象工厂模式)

[2.4 单例模式](#2.4 单例模式)

[2.4.1 饿汉模式](#2.4.1 饿汉模式)

[2.4.2 懒汉模式](#2.4.2 懒汉模式)

[2.4.3 线程安全的懒汉式](#2.4.3 线程安全的懒汉式)

[2.4.4 DCL单例 - 高性能的懒汉式](#2.4.4 DCL单例 - 高性能的懒汉式)

[2.5 建造者模式](#2.5 建造者模式)

[2.6 原型模式](#2.6 原型模式)

[2.7 创建型模式总结](#2.7 创建型模式总结)


1. 概述

设计模式(Design Pattern)是前辈们经过相当长的一段时间的试验和错误总结出来的,是软件开发过程中面临的通用问题的解决方案。这些解决方案使用设计模式是为了可重用代码、让代码更容易被他人理解、保证代码可靠性。

设计模式的优点:

  • ① 可以提高程序员的思维能力、编程能力和设计能力。
  • ② 使程序设计更加标准化、代码编制更加工程化,使软件开发效率大大提高,从而缩短软件的开发周期。
  • ③ 使设计的代码可重用性高、可读性强、可靠性高、灵活性好、可维护性强。

有一些重要的设计原则在开篇和大家分享下,这些原则将贯通全文:

  • **针对接口编程,而不是针对实现编程。**这个很重要,也是优雅的、可扩展的代码的第一步。
  • 职责单一原则。每个类都应该只有一个单一的功能,并且该功能应该由这个类完全封装起来。
  • 对修改关闭,对扩展开放。对修改关闭是说,我们辛辛苦苦加班写出来的代码,该实现的功能和该修复的 bug 都完成了,别人可不能说改就改;对扩展开放就比较好理解了,也就是说在我们写好的代码基础上,很容易实现扩展。

设计模式是对大家实际工作中写的各种代码进行高层次抽象的总结,其中最出名的当属 Gang of Four(GoF)的分类了,他们将设计模式分类为 23 种经典的模式,根据用途我们又可以分为三大类,分别为创建型模式(5种)结构型模式(7种)行为型模式(11种)

(1)创建型模式

  1. 简单(静态)工厂模式:一个工厂类根据传入的参量决定创建出那一种产品类的实例。(该模式不属于23种GOF设计模式之一
  2. 工厂方法:定义一个创建对象的接口,让子类决定实例化那个类。
  3. 抽象工厂:创建相关或依赖对象的家族,而无需明确指定具体类。
  4. 单例模式:某个类只能有一个实例,提供一个全局的访问点。
    1. 饿汉模式
    2. 懒汉模式
  5. 建造者模式:封装一个复杂对象的构建过程,并可以按步骤构造。
  6. 原型模式:通过复制现有的实例来创建新的实例。

(2)结构型模式

  1. 外观模式:对外提供一个统一的方法,来访问子系统中的一群接口。
  2. 桥接模式:将抽象部分和它的实现部分分离,使它们都可以独立的变化。
  3. 组合模式:将对象组合成树形结构以表示""部分-整体""的层次结构。
  4. 装饰模式:动态的给对象添加新的功能。
  5. 代理模式:为其他对象提供一个代理以便控制这个对象的访问。
  6. 适配器模式:将一个类的方法接口转换成客户希望的另外一个接口。
  7. 亨元(蝇量)模式:通过共享技术来有效的支持大量细粒度的对象。

(3)行为型模式

  1. 模板模式:定义一个算法结构,而将一些步骤延迟到子类实现。
  2. 解释器模式:给定一个语言,定义它的文法的一种表示,并定义一个解释器。
  3. 策略模式:定义一系列算法,把他们封装起来,并且使它们可以相互替换。
  4. 状态模式:允许一个对象在其对象内部状态改变时改变它的行为。
  5. 观察者模式:对象间的一对多的依赖关系。
  6. 备忘录模式:在不破坏封装的前提下,保持对象的内部状态。
  7. 中介者模式:用一个中介对象来封装一系列的对象交互。
  8. 命令模式:将命令请求封装为一个对象,使得可以用不同的请求来进行参数化。
  9. 访问者模式:在不改变数据结构的前提下,增加作用于一组对象元素的新功能。
  10. 责任链模式:将请求的发送者和接收者解耦,使的多个对象都有处理这个请求的机会。
  11. 迭代器模式:一种遍历访问聚合对象中各个元素的方法,不暴露该对象的内部结构。

2. 创建型模式

创建型模式的作用就是创建对象,说到创建一个对象,最熟悉的就是 new 一个对象,然后 set 相关属性。但是,在很多场景下,我们需要给客户端提供更加友好的创建对象的方式,尤其是那种我们定义了类,但是需要提供给其他开发者用的时候。

2.1 简单(静态)工厂模式

2.1.1 介绍

① 一句话来说就是:一个工厂类根据传入的参量决定创建出那一种产品类的实例。因为逻辑实现简单,所以称为简单工厂模式,也因为工厂中的方法一般设置为静态,所以也称为静态工厂,它不属于23种模式。

② 简单工厂模式专门定义一个工厂类来负责创建其他类的实例,被创建的实例通常都具有共同的父类,在工厂类中,可以根据参数的不同返回不同类的实例。升级版本简单工厂模式,通过反射根据类的全路径名生成对象。

③ 简单工厂模式就是将这部分创建对象语句分离出来,由工厂类来封装实例化对象的行为,修改时只需要修改类中的操作代码,使用时调用该类不需要考虑实例化对象的行为,使得后期代码维护升级更简单方便,有利于代码的可修改性与可读性。

④ 但是如果增加新的产品的话,需要修改工厂类的判断逻辑,违背开闭原则。

2.1.2 实现

直接上代码:

java 复制代码
public class FoodFactory {
    public static 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;
        }
    }
}

其中,LanZhouNoodle 和 HuangMenChicken 都继承自 Food。

简单地说,简单工厂模式通常就是这样:一个工厂类 XxxFactory,里面有一个静态方法,根据我们不同的参数,返回不同的派生自同一个父类(或实现同一接口)的实例对象

我们强调职责单一原则,一个类只提供一种功能,FoodFactory 的功能就是只要负责生产各种 Food。

2.2 工厂模式

简单工厂模式很简单,如果它能满足我们的需要,我觉得就不要折腾了。之所以需要引入工厂模式,是因为我们往往需要使用两个或两个以上的工厂。

java 复制代码
public interface FoodFactory {
    Food makeFood(String name);
}
public class ChineseFoodFactory implements FoodFactory {
    @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 AmericanFoodFactory implements FoodFactory {
    @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。

客户端调用:

java 复制代码
public class APP {
    public static void main(String[] args) {
        // 先选择一个具体的工厂
        FoodFactory factory = new ChineseFoodFactory();
        // 由第一步的工厂产生具体的对象,不同的工厂造出不一样的对象
        Food food = factory.makeFood("A");
    }
}

虽然都是调用 makeFood("A") 制作 A 类食物,但是,不同的工厂生产出来的完全不一样。

第一步,我们需要选取合适的工厂,然后第二步基本上和简单工厂一样。

核心在于,我们需要在第一步选好我们需要的工厂。比如,我们有 LogFactory 接口,实现类有 FileLogFactory 和 KafkaLogFactory,分别对应将日志写入文件和写入 Kafka 中,显然,我们客户端第一步就需要决定到底要实例化 FileLogFactory 还是 KafkaLogFactory,这将决定之后的所有的操作。

虽然简单,不过我也把所有的构件都画到一张图上,这样看着比较清晰:

2.3 抽象工厂模式

当涉及到产品族的时候,就需要引入抽象工厂模式了。

一个经典的例子是造一台电脑。我们先不引入抽象工厂模式,看看怎么实现。

因为电脑是由许多的构件组成的,我们将 CPU 和主板进行抽象,然后 CPU 由 CPUFactory 生产,主板由 MainBoardFactory 生产,然后,我们再将 CPU 和主板搭配起来组合在一起,如下图:

这个时候的客户端调用是这样的:

java 复制代码
// 得到 Intel 的 CPU
CPUFactory cpuFactory = new IntelCPUFactory();
CPU cpu = intelCPUFactory.makeCPU();

// 得到 AMD 的主板
MainBoardFactory mainBoardFactory = new AmdMainBoardFactory();
MainBoard mainBoard = mainBoardFactory.make();

// 组装 CPU 和主板
Computer computer = new Computer(cpu, mainBoard);

单独看 CPU 工厂和主板工厂,它们分别是前面我们说的工厂模式。这种方式也容易扩展,因为要给电脑加硬盘的话,只需要加一个 HardDiskFactory 和相应的实现即可,不需要修改现有的工厂。

但是,这种方式有一个问题,那就是如果 Intel 家产的 CPU 和 AMD 产的主板不能兼容使用,那么这代码就容易出错,因为客户端并不知道它们不兼容,也就会错误地出现随意组合。

下面就是我们要说的产品族的概念,它代表了组成某个产品的一系列附件的集合:

当涉及到这种产品族的问题的时候,就需要抽象工厂模式来支持了。我们不再定义 CPU 工厂、主板工厂、硬盘工厂、显示屏工厂等等,我们直接定义电脑工厂,每个电脑工厂负责生产所有的设备,这样能保证肯定不存在兼容问题

这个时候,对于客户端来说,不再需要单独挑选 CPU厂商、主板厂商、硬盘厂商等,直接选择一家品牌工厂,品牌工厂会负责生产所有的东西,而且能保证肯定是兼容可用的。

java 复制代码
public static void main(String[] args) {
    // 第一步就要选定一个"大厂"
    ComputerFactory cf = new AmdFactory();
    // 从这个大厂造 CPU
    CPU cpu = cf.makeCPU();
    // 从这个大厂造主板
    MainBoard board = cf.makeMainBoard();
    // 从这个大厂造硬盘
    HardDisk hardDisk = cf.makeHardDisk();

    // 将同一个厂子出来的 CPU、主板、硬盘组装在一起
    Computer result = new Computer(cpu, board, hardDisk);
}

当然,抽象工厂的问题也是显而易见的,比如我们要加个显示器,就需要修改所有的工厂,给所有的工厂都加上制造显示器的方法。这有点违反了对修改关闭,对扩展开放这个设计原则

2.4 单例模式

2.4.1 饿汉模式

饿汉模式最简单

java 复制代码
public class Singleton {
    /**
     * static:
     * ①表示共享变量,语意符合
     * ②使得该变量能在getInstance()静态方法中使用
     * final:
     * ①final修饰的变量值不会改变即常量,语意也符合,当然不加final也是可以的
     * ②保证修饰的变量必须在类加载完成时就已经进行赋值。
     * final修饰的变量,前面一般加static
     */
    private static final Singleton singleton = new Singleton();

    /**
     * 私有化构造方法,使外部无法通过构造方法构造除singleton外的类实例
     * 从而达到单例模式控制类实例数目的目的
     */
    private Singleton(){}

    /**
     * 类实例的全局访问方法
     * 因为构造方法以及被私有化,外部不可能通过new对象来调用其中的方法
     * 加上static关键词使得外部可以通过类名直接调用该方法获取类实例
     * @return
     */
    public static Singleton getSingleton() {
        return singleton;
    }
}

说明:

① 优点:一般使用static和final修饰变量(具体作用已经在代码里描述了),只在类加载时才会初始化,以后都不会,线程绝对安全,无锁,效率高

② 缺点:类加载的时候就初始化,不管用不用,都占用空间,会消耗一定的性能(当然很小很小,几乎可以忽略不计,所以这种模式在很多场合十分常用而且十分简单)。

2.4.2 懒汉模式

java 复制代码
public class Singleton {
    private static Singleton singleton = null;
    private Singleton(){}
    public static Singleton getSingleton() {
        if(singleton == null){
            singleton = new Singleton();
        }
        return singleton;
    }
}

说明:

① 优点:在外部需要使用的时候才进行实例化,不使用的时候不会占用空间

② 缺点:线程不安全。看上去,这段代码没什么明显问题,但它不是线程安全的。假设当前有N个线程同时调用getInstance()方法,由于当前还没有对象生成,所以一部分同时都进入if语句new Singleton(),那么就会由多个线程创建多个多个user对象

2.4.3 线程安全的懒汉式

java 复制代码
public class Singleton {
    private static Singleton singleton;
    private Singleton(){};
    private static synchronized Singleton getSingleton(){
        if(singleton == null){
            singleton = new Singleton();
        }
        return singleton;
    }
}

说明:

① 优点:解决了懒汉式线程不安全的问题

② 缺点:线程阻塞,影响性能

2.4.4 DCL单例 - 高性能的懒汉式

java 复制代码
public class Singleton {
 /*volatile在这里发挥的作用是:禁止指令重排序(编译器和处理器为了优化程序性能
    * 而对指令序列进行排序的一种手段。)
    * singleton = new Singleton();这句代码是非原子性操作可分为三行伪代码
    * a:memory = allocate() //分配内存,在jvm堆中分配一段区域
    * b:ctorInstanc(memory) //初始化对象,在jvm堆中的内存中实例化对象
    * c:instance = memory //赋值,设置instance指向刚分配的内存地址
    * 上面的代码在编译运行时,可能会出现重排序从a-b-c排序为a-c-b。
    * 重排序是为了优化性能,但是不管怎么重排序,在单线程下程序的执行结果不能被改变
    * 保证最终一致性。而在多线程环境下,可能发生重排序,会影响结果。
    * ①若A线程执行到代码singleton = new Singleton()时;
    * ②同时若B线程进来执行到代码到第一层检查if (singleton == null)
    * ③当cpu切换到A线程执行代码singleton = new Singleton();时发生了指令重排序,
    * 执行了a-b,没有执行c,此时的singleton对象只有地址,没有内容。然后cpu又切换到了B线程,
    * 这时singleton == null为false(==比较的是内存地址),
    * 则代码会直接执行到了return,返回一个未初始化的对象(只有地址,没有内容)。
    * */
    private volatile static Singleton singleton;

    private Singleton() {
    }

    public static Singleton getSingleton() {
        /*第一层检查,检查是否有引用指向对象,高并发情况下会有多个线程同时进入
        * ①当多个线程第一次进入,所有线程都进入if语句
        * ②当多个线程第二次进入,因为singleton已经不为null,因此所有线程都不会进入if语句,
        * 即不会执行锁,从而也就不会因为锁而阻塞,避免锁竞争*/
        if (singleton == null) {
            /*第一层锁,保证只有一个线程进入,
            * ①多个线程第一次进入的时候,只有一个线程会进入,其他线程处于阻塞状态
            * 当进入的线程创建完对象出去之后,其他线程又会进入创建对象,所以有了第二次if检查
            * ②多个线程第二次是进入不到这里的,因为已被第一次if检查拦截*/
            synchronized (Singleton.class) {
                /*第二层检查,防止除了进入的第一个线程的其他线程重复创建对象*/
                if (singleton == null) {
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }
}

说明:代码注释已详细讲解volatile在该单例模式的作用,已经双重锁的作用。

① 优点:解决了线程阻塞的问题。

② 缺点:**多个线程第一次进入的时候会造成大量的线程阻塞,**代码不够优雅。

静态内部类的方式

java 复制代码
public class Singleton {
    private Singleton(){}
    private static class LayzInner{
        private static Singleton singleton = new Singleton();
    }
    public static Singleton getSingleton(){
        return LayzInner.singleton;
    }
}

说明:

① 优点:第一次类创建的时候加载,避免了内存浪费,不存在阻塞问题,线程安全,唯一性

② 缺点:序列化-漏洞:反射,会破坏内部类单例模式

2.5 建造者模式

经常碰见的 XxxBuilder 的类,通常都是建造者模式的产物。建造者模式其实有很多的变种,但是对于客户端来说,我们的使用通常都是一个模式的:

java 复制代码
Food food = new FoodBuilder().a().b().c().build();
Food food = Food.builder().a().b().c().build();

套路就是先 new 一个 Builder,然后可以链式地调用一堆方法,最后再调用一次 build() 方法,我们需要的对象就有了。

来一个中规中矩的建造者模式:

java 复制代码
class User {
    // 下面是"一堆"的属性
    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() 看上去就没那么好
    public static UserBuilder builder() {
        return new UserBuilder();
    }

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

        private UserBuilder() {
        }

        // 链式调用设置各个属性值,返回 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,然后 build() 方法的时候,将这些属性复制给实际产生的对象。

看看客户端的调用:

java 复制代码
public class APP {
    public static void main(String[] args) {
        User d = User.builder().name("foo").password("pAss12345").age(25).build();
    }
}

说实话,建造者模式的链式写法很吸引人,但是,多写了很多"无用"的 builder 的代码,感觉这个模式没什么用。不过,当属性很多,而且有些必填,有些选填的时候,这个模式会使代码清晰很多。我们可以在 Builder 的构造方法中强制让调用者提供必填字段,还有,在 build() 方法中校验各个参数比在 User 的构造方法中校验,代码要优雅一些。

题外话,强烈建议读者使用 lombok,用了 lombok 以后,上面的一大堆代码会变成如下这样:

java 复制代码
@Builder
class User {
    private String  name;
    private String password;
    private String nickName;
    private int age;
}

当然,如果你只是想要链式写法,不想要建造者模式,有个很简单的办法,User 的 getter 方法不变,所有的 setter 方法都让其 return this 就可以了,然后就可以像下面这样调用:

java 复制代码
User user = new User().setName("").setPassword("").setAge(20);

很多人是这么用的,但是这种写法不太优雅,不是很推荐使用。

2.6 原型模式

在某些情况下,需要创建对象的副本,但复制一个对象的成本可能很高,或者希望避免与对象的具体类耦合。例如,当创建对象的过程较为复杂,或者对象包含大量共享的状态时,使用常规的创建方法可能会导致性能下降。

原型模式的解决方案是通过复制现有对象来创建新对象,而不是从头开始构建。这允许我们以更高效的方式创建新对象,同时避免了与对象类的直接耦合。核心概念是在原型对象的基础上进行克隆,使得新对象具有与原型相同的初始状态。

原型模式很简单:有一个原型实例,基于这个原型实例产生新的实例,也就是"克隆"了。

Object 类中有一个 clone() 方法,它用于生成一个新的对象,当然,如果我们要调用这个方法,java 要求我们的类必须先实现 Cloneable 接口,此接口没有定义任何方法,但是不这么做的话,在 clone() 的时候,会抛出 CloneNotSupportedException 异常。

java 复制代码
protected native Object clone() throws CloneNotSupportedException;

注意事项:

  • 深克隆问题:原型模式默认进行浅克隆,即复制对象本身和其引用。如果对象内部包含其他对象的引用,可能需要实现深克隆来复制整个对象结构。
  • 克隆方法的实现:某些对象可能不容易进行克隆,特别是涉及到文件、网络连接等资源的情况。

Java 的克隆是浅克隆,碰到对象引用的时候,克隆出来的对象和原对象中的引用将指向同一个对象。通常实现深克隆的方法是将对象进行序列化,然后再进行反序列化。

总之,原型模式是一种在需要创建对象副本时非常有用的设计模式,它提供了一种灵活且高效的方法来处理对象的复制需求。

2.7 创建型模式总结

创建型模式总体上比较简单,它们的作用就是为了产生实例对象,算是各种工作的第一步了,因为我们写的是面向对象的代码,所以我们第一步当然是需要创建一个对象了。

总结:

  • 工厂模式在简单工厂模式的基础上增加了选择工厂的维度,需要第一步选择合适的工厂;
  • 抽象工厂模式有产品族的概念,如果各个产品是存在兼容性问题的,就要用抽象工厂模式;
  • 单例模式为了保证全局使用的是同一对象,一方面是安全性考虑,一方面是为了节省资源;
  • 建造者模式专门对付属性很多的那种类,为了让代码更优美;
  • 原型模式用得比较少,了解和 Object 类中的 clone() 方法相关的知识即可。

++后续待更新......++

相关推荐
哪 吒1 小时前
最简单的设计模式,抽象工厂模式,是否属于过度设计?
设计模式·抽象工厂模式
Theodore_10221 小时前
4 设计模式原则之接口隔离原则
java·开发语言·设计模式·java-ee·接口隔离原则·javaee
转世成为计算机大神4 小时前
易考八股文之Java中的设计模式?
java·开发语言·设计模式
小乖兽技术5 小时前
23种设计模式速记法
设计模式
小白不太白9507 小时前
设计模式之 外观模式
microsoft·设计模式·外观模式
小白不太白9507 小时前
设计模式之 原型模式
设计模式·原型模式
澄澈i7 小时前
设计模式学习[8]---原型模式
学习·设计模式·原型模式
小白不太白95014 小时前
设计模式之建造者模式
java·设计模式·建造者模式
菜菜-plus16 小时前
java 设计模式 模板方法模式
java·设计模式·模板方法模式
萨达大16 小时前
23种设计模式-模板方法(Template Method)设计模式
java·c++·设计模式·软考·模板方法模式·软件设计师·行为型设计模式