23种设计模式

  • Post author:
  • Post category:其他




引言:什么是设计模式

设计模式是解决软件开发某些特定问题而提出的一些解决方案, 也可以理解为

解决特定问题的思路

, 通过设计模式可以增强代码的可重用性、可扩充性、可维护性、灵活性等等. 使用设计模式的目的是实现代码的

解耦



高内聚


设计模式的三大类以及关键点


在这里插入图片描述


设计模式概述 以及 23种设计模式的介绍




1.创建型模式

对象实例化的模式,创建型模式用于解耦对象的实例化过程。

  • 单例模式:某个类智能有一个实例,提供一个全局的访问点。
  • 工厂模式:一个工厂类根据传入的参量决定创建出哪一种产品类的实例。
  • 抽象工厂模式:创建相关或依赖对象的家族,而无需明确指定具体类。
  • 建造者模式:封装一个复杂对象的创建过程,并可以按步骤构造。
  • 原型模式:通过复制现有的实例来创建新的实例。



1.1 单例模式


一、单例模式常见的三种实现方法

  • 饿汉式
  • 懒汉式


二、单例模式的特点

  1. 单例类只能有一个实例
  2. 单例类必须

    自己创建

    这个实例
  3. 单例类必须向整个系统提供这个实例


三、单例模式的应用场景


单例模式确保某个类有且只有一个实例, 且自行实例化后向整个系统提供这个实例.

在计算机系统中, 线程池、缓存、日志对象、对话框、打印机、显卡的驱动程序对象常被设计为单例. 这些应用都或多或少的具有资源管理器的功能, 每台计算机可以有若干个打印机, 但只能有一个PrinterSpooler, 避免两个打印作业同时输出到打印机中. 总而言之, 单例模式就是为了避免不一致的状态.




1.1.1 饿汉式


一、优点:


没有任何锁, 执行效率高, 用户体验比懒汉式更好


二、缺点:


类加载的时候就初始化, 不管用不用都占用内存空间


三、建议:

适用于单例模式较少的场景

如果我们在项目启动后, 一定会加载到类, 那么用饿汉式既简单又实用,

如果我们是写一些工具类, 则优先考虑懒汉模式, 可以避免提前被加载到内存, 占用系统资源


四、代码案例:

public class HungryMan {
    /**
     * static保证类加载时成员变量, 静态成员变量存放于方法区
     *      *局部变量:在方法内部声明的变量, 只在这个方法内部有效, 出了这个方法就无法使用了, 可以理解为销毁了
     *      *实例变量:没有使用static修饰的成员变量, 它在每一个实例化对象中都是独立的, 每个对象修改其值后, 只是修改了自己的实例变量, 其他对象
     *          中的实例变量不受影响(每个对象的实例对象都有独立的内存, 它们互不影响)
     *      *类变量(静态变量):使用static修饰的成员变量, 所有实例对象共享此变量(所有实例对象使用的类变量, 都指向同一个内存)
     * final保证此实例对象不支持被重新赋值
     */
    private final static HungryMan hungryMan = new HungryMan();

    static {
        System.out.println(hungryMan);
    }

    /**
     * 对外提供获取单例对象的静态方法(因为成员属性也是静态的)
     */
    public static HungryMan getInstance() {
        return hungryMan;
    }

    public static void main(String[] args) {
        HungryMan instance_1 = HungryMan.getInstance();
        HungryMan instance_2 = HungryMan.getInstance();

        System.out.println(instance_1 == instance_2);
        System.out.println(instance_1.equals(instance_2));
    }
}


问题一:为什么使用static?


保证只初始化一次, 且在类加载的时候进行初始化(与对象无关)

java程序运行时, 必须经过

编译

这个过程

  • 首先编译器将后缀名为.java的源文件进行编译, 最终生成后缀名为.class的字节码文件
  • 然后JVM将编译好的字节码文件加载到内存中(这个过程被称为

    类加载

    , 通过类加载器完成), 而静态成员就是在类加载的过程中被初始化的
  • JVM对每个类只会加载一次, 也就是说静态成员也只会被初始化一次
public class Singleton {
    private final static Singleton singleton = new Singleton();

    public Singleton getInstance() {
        return singleton;
    }
}


问题二:为什么使用final?


既然是单例, 就不支持被重新赋值

在这里插入图片描述




1.1.2 懒汉式


一、线程不安全的懒汉式


为什么要将实例设置为static, 因为getInstance方法是static(静态方法直接调用)

public class Singleton {

	private Singleton() {}
	
    private static Singleton singleton;

    public static Singleton getInstance() {
        if (singleton == null) {			// 代码1
            singleton = new Singleton();	// 代码2
        }
        return singleton;
    }
}

Singleton通过将构造方法限定为private避免了类在外部被实例化, Singleton的唯一实例只能通过getInstance()方法访问

但是上述的懒汉式并没有考虑线程安全问题, 它是线程不安全的

  1. 线程A执行到了代码1的位置, 还未进行判断
  2. 线程B通过了判断, 执行到了代码2的位置, 正在new Singleton(), 但还没有new结束
  3. 线程A看到single还未被实例化, 也会执行代码2, 再次实例化对象
  4. 两个线程都实例化对象, 无法保证单例模式


二、在getInstance方法上加同步(性能差)

public class Singleton {

	private Singleton() {}
    
    private static Singleton single;

    //静态工厂方法
    public synchronized  static Singleton getInstance() {
        if (single == null) {
            single = new Singleton();
        }
        return single;
    }
}

存在的问题:

  1. 由于对getInstance()方法做了同步处理,synchronized将导致性能开销。
  2. 如果getInstance()方法被多个线程频繁的调用,将会导致程序执行性能的下降。
  3. 如果getInstance()方法不会被多个线程频繁的调用,那么这个延迟初始化方案将能提供令人满意的性能。


三、引入双重检查锁定(无法规避指令重排)


由于synchronized存在巨大的性能开销。因此,人们想出了一个“聪明”的技巧:双重检查锁定【Double-Checked Locking】。人们想通过双重检查锁定来降低同步的开销。

public class Singleton {
    
    private static Singleton single = null;

    //静态工厂方法
    public static Singleton getInstance() {
        if (single == null) {							// 1处、第一次检查
        	// 只有当单例对象为空时, 才实例化对象(同步锁)
        	synchronized(Singleton.class) {				// 2处、加锁
        		if (single == null) {					// 3处、第二次检查
        			single = new Singleton();			// 4处、实例化对象【这里会出问题的】
        		}
        	}
        }
        return single;
    }
}

在1处,如果是第一次检查instance不为null,那么就不需要执行下面的加锁和初始化操作。因此,可以大幅降低synchronized带来的性能开销。

在2处,如果多个线程试图在同一时间创建对象时,这里有同步代码块,会通过加锁来保证只有一个线程能创建对象。

在3处,获得锁的线程,会二次检查这个对象是否已经被初始化。

在4处,对象创建好之后,执行getInstance()方法将不需要获取锁,直接返回已创建好的对象。

问题所在:

一切都是那么的美好,但是有一种情况,在线程执行到1处,读取到instance不为null时;在4处的线程正在初始化实例instance,但是instance引用的对象有可能还没有完成初始化,因为发生了指令重排。

4处因为指令重排,引发的1处拿到的实例在使用的时候发生空指针的问题。(拿到的对象是还未完成初始化的对象)


四、最终的懒汉式(synchronized + 双重检查锁定 + volatile)

public class Singleton {
    
    private static volatile Singleton single = null;

    //静态工厂方法
    public static Singleton getInstance() {
        if (single == null) {							// 1处、第一次检查
        	// 只有当单例对象为空时, 才实例化对象(同步锁)
        	synchronized(Singleton.class) {				// 2处、加锁
        		if (single == null) {					// 3处、第二次检查
        			single = new Singleton();			// 4处、实例化对象【这里会出问题的】
        		}
        	}
        }
        return single;
    }
}

volatile 可以禁止single = new Singleton();过程中的指令重排,从而实现线程的安全。

参考:

项目中用的双重检查锁定是怎么回事




1.2 工厂方法模式

参考:

工厂模式


一、什么是工厂模式:


工厂模式的目的是将创建对象的具体过程屏蔽隔离起来, 从而达到更高的灵活度, 工厂模式可以分为三类:

  • 简单工厂模式
  • 工厂方法模式
  • 抽象工厂模式

此处我们先了解简单工厂模式和工厂方法模式

情况一:在没有工厂的时代, 如果客户需要一款宝马, 就需要客户去创建一款宝马(new Bmw()), 然后拿来用

情况二:简单工厂模式:后来出现了工厂, 用户不再需要去创建宝马, 由工厂进行创建, 想要什么, 直接通过工厂创建就可以了. 用户只要从工厂里面拿就行了

情况三:工厂方法模式:为了满足客户, 宝马车系列越来越多, 例如320i、523i等系列, 一个工厂无法创建所有的宝马系列, 于是单独分出来多个具体的工厂, 每个具体的工厂创建一种系列, 即具体工厂只能创建一个具体产品.




1.2.1 简单工厂模式


简单工厂模式的核心是定义一个创建对象的接口, 将对象的创建和本身的业务逻辑分离

, 降低系统的耦合度, 当以后实现改变时, 只要修改工厂类即可.


一、不使用工厂模式时, 创建对象

如果不适用工厂, 用户自己创建宝马车, 具体的UML图如下

在这里插入图片描述

public class BMW320 {
	public BMW320(){
		System.out.println("制造-->BMW320");
	}
}
 
public class BMW523 {
	public BMW523(){
		System.out.println("制造-->BMW523");
	}
}
 
public class Customer {
	public static void main(String[] args) {
		BMW320 bmw320 = new BMW320();
		BMW523 bmw523 = new BMW523();
	}
}


二、将业务与创建对象的过程解耦


创建车的过程和业务紧密耦合到一起, 为了降低耦合度, 就出现了简单工厂模式

在简单工厂模式中, 将创建宝马的具体操作都放到了工厂中, 而客户只要知道工厂的创建方法, 传入想要的宝马车型号就行了, 而不用关心创建的细节

在这里插入图片描述

  • 工厂类角色: 该模式的核心, 用来创建产品, 并对外提供获取产品实例的方法, 含有一定的商业逻辑和判断逻辑
  • 抽象产品角色:一般是具体产品继承的父类或实现的接口
  • 具体产品角色:工厂类创建的对象就是此角色的实例


三、简单工厂模式的代码实现


产品类:

abstract class BMW {
	public BMW(){}
}
 
public class BMW320 extends BMW {
	public BMW320() {
		System.out.println("制造-->BMW320");
	}
}
public class BMW523 extends BMW{
	public BMW523(){
		System.out.println("制造-->BMW523");
	}
}


工厂类:

public class Factory {
	public BMW createBMW(int type) {
		switch (type) {
		
		case 320:
			return new BMW320();
 
		case 523:
			return new BMW523();
 
		default:
			break;
		}
		return null;
	}
}


用户类:

public class Customer {
	public static void main(String[] args) {
		Factory factory = new Factory();
		BMW bmw320 = factory.createBMW(320);
		BMW bmw523 = factory.createBMW(523);
	}
}


四、简单工厂模式的缺点:


不符合

开闭原则

, 每次添加新的产品都需要修改工厂类, 在产品类型较多时, 会造成工厂逻辑过于复杂, 不利于系统的扩展维护, 并且工厂类集中了所有产品的创建逻辑, 一旦不能正常工作, 整个系统都会受到影响,为了解决简单工厂模式的问题,出现了工厂方法模式




1.2.2 工厂方法模式

工厂方法模式将工厂抽象化, 并定义一个创建对象的接口, 每增加一项产品, 只需增加该产品以及对应的具体实现工厂类, 由具体工厂类决定要实例化的产品是哪个.

将对象的创建与实例化延迟到子类, 这样工厂的设计就符合开闭原则, 扩展时不需要改动原来的代码


产品类:

abstract class BMW {
	public BMW(){}
}
public class BMW320 extends BMW {
	public BMW320() {
		System.out.println("制造-->BMW320");
	}
}
public class BMW523 extends BMW{
	public BMW523(){
		System.out.println("制造-->BMW523");
	}
}


工厂类:

interface FactoryBMW {
	BMW createBMW();
}
 
public class FactoryBMW320 implements FactoryBMW{
 
	@Override
	public BMW320 createBMW() {
		return new BMW320();
	}
 
}
public class FactoryBMW523 implements FactoryBMW {
	@Override
	public BMW523 createBMW() {
		return new BMW523();
	}
}


客户类:

public class Customer {
	public static void main(String[] args) {
		FactoryBMW320 factoryBMW320 = new FactoryBMW320();
		BMW320 bmw320 = factoryBMW320.createBMW();
 
		FactoryBMW523 factoryBMW523 = new FactoryBMW523();
		BMW523 bmw523 = factoryBMW523.createBMW();
	}
}


工厂方法模式的缺点:


缺点在于:每增加一个产品都需要增加一个具体的产品类和实现工厂类, 在一定程度上增加了系统的复杂度,同时也增加了系统具体类的依赖




1.3 抽象工厂模式

抽象工厂模式, 是围绕一个超级工厂创建其他工厂. 该超级工厂又称为其他工厂的工厂. 它提供了一种创建对象的最佳方式


一、产品族和产品等级

  • 产品族:一个品牌下面的所有产品:例如华为下面的手机、路由器、电脑, 这些统称为华为的产品族
  • 产品等级:多个品牌下面的同种产品:例如华为和小米下面的手机, 都称为一个产品等级

在这里插入图片描述


二、需求引入


以下图为例: 有手机和路由器两种产品, 有华为和小米两种品牌, 两种品牌都可以生产手机和路由器

  • 有手机和路由器两种产品, 定义两个接口
  • 小米和华为都可以生产这两种产品, 所有有4个实现类

    • 小米手机、小米路由器、华为手机、华为路由器
  • 现在需要创建华为和小米的工厂类, 先将工厂类进行抽象, 里面有创建这两个产品的方法, 返回的是产品的接口类
  • 创建华为和小米的工厂实现类, 继承工厂类接口, 实现创建各自产品的方法
  • 客户调用时, 直接用工厂接口类创建的需要的工厂, 拿到对应的产品

在这里插入图片描述


三、抽象工厂模式代码实现


产品接口类

//手机产品接口
public interface IPhoneProduct {
}

//路由器产品接口
public interface IRouterProduct {
}


产品实现类

//华为手机实现类
public class HuaweiPhone implements IPhoneProduct {
}

//华为路由器实现类
public class HuaweiRouter implements IRouterProduct {
}

//小米手机实现类
public class XiaomiPhone implements IPhoneProduct {
}

//小米路由器实现类
public class XiaomiRouter implements IRouterProduct {
}


工厂接口类

//产品工厂接口
public interface IProductFactory {

    //生产手机
    IPhoneProduct phoneProduct();

    //生成路由器
    IRouterProduct routerProduct();
}


工厂实现类

//华为工厂实现类
public class HuaweiFactory implements IProductFactory {

    @Override
    public IPhoneProduct phoneProduct() {
	return new HuaweiPhone();
    }

    @Override
    public IRouterProduct routerProduct() {
	return new HuaweiRouter();
    }
}

//小米工厂实现类
public class XiaomiFactory implements IProductFactory {

    @Override
    public IPhoneProduct phoneProduct() {
    	return new XiaomiPhone();
    }

    @Override
    public IRouterProduct routerProduct() {
	return new XiaomiRouter();
    }
}


客户端

public class Client {
    
    public static void main(String[] args) {
        
	System.out.println("============小米产品============");
	//创建小米工厂
	IProductFactory xiaomiFactory = new XiaomiFactory();
	//生产小米手机
	IPhoneProduct xiaomiPhone = xiaomiFactory.phoneProduct();
	//生产小米路由器
	IRouterProduct xiaomiRouter = xiaomiFactory.routerProduct();

	System.out.println("============华为产品============");
	//创建华为工厂
	IProductFactory huaweiFactory = new HuaweiFactory();
	//生产华为手机
	IPhoneProduct huaweiPhone = huaweiFactory.phoneProduct();
	//生产华为路由器
	IRouterProduct huaweiRouter = huaweiFactory.routerProduct();
    }
}


四、开闭原则


开闭原则:对扩展开放, 对修改关闭

抽象工厂模式是开闭原则的一个很好的应用, 不过它也是有局限性的, 分别有以下两种情况

  1. 扩展一个产品族, 完全符合开闭原则
  2. 扩展一个产品等级, 完全不符合开闭原则

在这里插入图片描述

“抽象工厂方法模式”是针对“产品族”进行生产产品,具体如下图所示。

在这里插入图片描述


情况一:拓展一个产品族


在产品等级结构不变的情况下, 拓展一个产品族是非常容易的, 例如我们新增一个品牌, 如下图所示(黄色字体为新增一个产品族需要做的事),新增一个产品族不用修改原来的代码,符合OCP原则,这是非常舒服的;

1.新增一个工厂实现类

2.新增手机实现类、新增路由器接口实现类

在这里插入图片描述


情况二:拓展一个产品等级


我们会发现,拓展一个产品等级是非常困难的,例如新增一个笔记本电脑,也就是说华为和小米现在可以生产电脑了,如下图所示(黄色字体为新增一个产品等级需要做的事),对顶层的工厂接口类也要修改,这是非常麻烦的;

1.在工厂接口中新增生产电脑的方法

2.修改所有的工厂实现类, 新增生产电脑的方法

在这里插入图片描述


五、抽象工厂模式的优缺点

  • 优点:

    一个产品族中的多个对象被设计成一起工作时,它能保证客户端始终只使用同一个产品族中的对象(将一个系列的产品统一一起创建);
  • 缺点:

    • 产品族扩展非常困难,要增加一个系列的某一产品,既要修改工厂抽象类里加代码,又修改具体的实现类里面加代码;
    • 增加了系统的抽象性和理解难度;


六、适用场景


一系列相关产品对象(属于同一产品族)一起创建时需要大量的重复代码;

提供一个产品类的库,所有的产品以同样的接口出现,从而使得客户端不依赖于具体的实现;


抽象工厂模式符合依赖抽象原则

  • 创建对象实例时,不要直接 new一个对象, 而是把创建对象的动作放在一个工厂的方法中;
  • 不要让类继承具体类,而是继承抽象类或者是实现接口;
  • 不要覆盖基类中已经实现的方法;


抽象工厂模式




1.4 建造者模式


一、建造者模式流程图


在这里插入图片描述

从图中我们主以看出建造者主要分为4种角色:

  • Product(产品类) :我们具体需要生成的类对象
  • Builder(抽象建造者类):为我们需要生成的类对象,构建不同的模块属性,即:公开构建产品类的属性,隐藏产品类的其他功能
  • ConcreteBuilder(具体建造者类):实现我们要生成的类对象
  • Director(导演类):确定构建我们的类对象具体有哪些模块属性,在实际应用中可以不需要这个角色,直接通过client处理


举例


在电商中有多种不同类型的商品, 例如实物商品、电子卡券、虚拟学习视频等等, 他们都是商品, 但是他们的属性却不一样, 例如电子卡券:独有券码、学习视频:独有视频链接等等.

那么我们怎么实现创建这类商品呢?



1.4.1 普通的创建方案

商品类, 这个商品类中包含所有商品的字段

@Data
public class Item {
    private String itemName;
    private Integer type;
    private String code;
    private String url;
}

创建商品, 根据实际的商品类型来设置属性

public static void main(String[] args) {
    Item item = new Item();
    String type = "创建实体商品";
    
    if ("创建实体商品".equals(type)) {
        item.setItemName("普通商品");
        item.setType(1);
    }
    if ("创建电子卡券".equals(type)) {
        item.setItemName("电子卡券");
        item.setCode("1");
        item.setType(2);
    }
    if ("创建学习视频".equals(type)) {
        item.setItemName("学习视频");
        item.setUrl("www.baidu.com");
        item.setType(3);
    }
}

问题:随着商品种类的增加, 商品的属性会越来越多, 之后也不好维护



1.4.2 建造者模式的解决方案

步骤一:创建我们的抽象建造者类, 这里面我们看到有三个抽象方法. 来确定不同的商品类型, 我们调用不同的方法, 达到解耦的思想

public abstract class ItemBuilder {
    // 创建商品对象
    protected Item item = new Item();

    // 创建普通商品
    public abstract void buildNormal();

    // 创建电子卡券
    public abstract void buildCard();

    // 创建学习视频
    public abstract void buildVideo();

    // 返回产品对象
    public Item getResult() {
        return item;
    }
}

步骤二:创建具体建造类, 对抽象建造类的抽象方法进行实现赋值, 达到我们需要的结果

public class ItemConcreteBuilder extends ItemBuilder {
    @Override
    public void buildNormal() {
        item.setItemName("普通商品");
        item.setType(1);
    }

    @Override
    public void buildCard() {
        item.setItemName("卡券");
        item.setCode("123456");
        item.setType(2);
    }

    @Override
    public void buildVideo() {
        item.setItemName("视频商品");
        item.setType(3);
        item.setUrl("www.baidu.com");
    }
}

第三步:创建我们的导演类。指导我们怎么去创建对象,这个我们是可以简化的,视具体使用场景确定吧!

public class ItemDirector {
    private ItemBuilder itemBuilder;

    public ItemDirector(ItemBuilder itemBuilder) {
        this.itemBuilder = itemBuilder;
    }

    public Item normal() {
        itemBuilder.buildNormal();
        return itemBuilder.getResult();
    }

    public Item card() {
        itemBuilder.buildCard();
        return itemBuilder.getResult();
    }

    public Item video() {
        itemBuilder.buildVideo();
        return itemBuilder.getResult();
    }
}


秒懂设计模式之建造者模式(Builder pattern)




2.结构型模式



2.1 适配器模式

将一个类的接口转换成客户希望的另一个接口。Adapter模式使得原本由于接口不兼容而不能一起工作的那些类可以一起工作。

  • 包装类, 即适配器Adapter
  • 被包装类, 即适配者Adaptee(被适配类)



2.2



版权声明:本文为qq_24099547原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。