常用的设计模式和使用案例汇总

【一】常用的设计模式介绍

常用的七种设计模式:单例模式、工厂方法模式、抽象工厂模式、代理模式、装饰器模式、观察者模式、责任链模式。

【1】设计模式分类

设计模式根据工作的目的,分为创建型模式、结构型模式和行为型模式三类。
(1)创建型模式:单例模式、工厂方法模式、抽象工厂模式、创建者模式、原型模式。
(2)结构型模式:适配器模式、代理模式、装饰器模式、外观模式、桥接模式、组合模式、享元模式。
(3)行为型模式:策略模式、模板方法模式、观察者模式、迭代子模式、责任链模式、命令模式、备忘录模式、状态模式、访问者模式、中介者模式、解释器模式。

【2】软件设计七大原则(OOP原则)

(1)开闭原则:对扩展开放,对修改关闭。
(2)里氏替换原则:不要破坏继承体系,子类重写方法功能发生改变,不应该影响父类方法的含义。
(3)依赖倒置原则:要面向接口编程,不要面向实现编程。
(4)单一职责原则:控制类的粒度大小、将对象解耦、提高其内聚性。
(5)接口隔离原则:要为各个类建立它们需要的专用接口。
(6)迪米特法则:一个类应该保持对其它对象最少的了解,降低耦合度。
(7)合成复用原则:尽量先使用组合或者聚合等关联关系来实现,其次才考虑使用继承关系来实现。

实际上,七大原则的目的只有一个:降低对象之间的耦合,增加程序的可复用性、可扩展性和可维护性。

【二】单例模式

【1】介绍

一个类只有一个实例,且该类能自行创建这个实例的一种模式
(1)单例类只有一个实例对象
(2)该单例对象必须由单例类自行创建
(3)单例类对外提供一个访问该单例的全局访问点
(4)优点
单例模式可以保证内存里只有一个实例,减少了内存的开销。
可以避免对资源的多重占用。
单例模式设置全局访问点,可以优化和共享资源的访问。
(5)缺点
单例模式一般没有接口,扩展困难。
单例模式的功能代码通常写在一个类中,如果功能设计不合理,则很容易违背单一职责原则

【2】饿汉式单例

类一旦加载就创建一个单例,保证在调用getInstance方法之前单例已经存在,这种饿汉式单例会造成空间浪费。

public class Hungry {
    private Hungry(){}
    private final static Hungry HUNGRY = new Hungry();
    public static Hungry getInstance(){
        return HUNGRY;
    }
}

【3】懒汉式单例

为了避免内存空间浪费,采用懒汉式单例,即用到该单例对象的时候再创建。

public class LazyMan {
    private LazyMan(){};
    public static LazyMan lazyMan;
    public static LazyMan getInstance(){
        if (lazyMan==null){
            lazyMan = new LazyMan();
        }
        return lazyMan;
    }
}

这是最简单的懒汉式,但是,存在很大问题,单线程下这段代码没有问题,但是在多线程下有很大问题。

public class LazyMan {
    private LazyMan(){
        System.out.println(Thread.currentThread().getName()+"");
    }
 
    public static LazyMan lazyMan;
 
    public static LazyMan getInstance(){
        if (lazyMan==null){
            lazyMan = new LazyMan();
        }
        return lazyMan;
    }
 
    public static void main(String[] args) {
        for(int i=0;i<10;i++){
            new Thread(()->{
                lazyMan.getInstance();
            }).start();
        }
    }
}

在这里插入图片描述
会发现结果都不一样,因此,并发情况下,这段代码是有问题的。我们需要进行两端检测,进行“加锁”:synchronized (Singleton.class)。

public class LazyMan {
    private LazyMan(){
        System.out.println(Thread.currentThread().getName()+"");
    }
 
    public static LazyMan lazyMan;
 
    public static LazyMan getInstance(){
        if (lazyMan==null){        //第一层检查,检查是否有引用指向对象,高并发情况下会有多个线程同时进入
            synchronized (LazyMan.class){    //第一层锁,保证只有一个线程进入
                //双重检查,防止多个线程同时进入第一层检查(因单例模式只允许存在一个对象,故在创建对象之前无引用指向对象,所有线程均可进入第一层检查)
                //当某一线程获得锁创建一个LazyMan对象时,即已有引用指向对象,lazyMan不为空,从而保证只会创建一个对象
                //假设没有第二层检查,那么第一个线程创建完对象释放锁后,后面进入对象也会创建对象,会产生多个对象
                if(lazyMan==null){    //第二层检查
                    //synchronized关键字作用为禁止指令重排,保证返回Singleton对象一定在创建对象后
                    lazyMan = new LazyMan();        //这行代码存在的问题,不能保证原子性
实际上会执行以下内容:
                    //(1)在堆上开辟空间;(2)属性初始化;(3)引用指向对象
                    //假设以上三个内容为三条单独指令,因指令重排可能会导致执行顺序为1->3->2(正常为1->2->3),当单例模式中存在普通变量需要在构造方法中进行初始化操作时,单线程情况下,顺序重排没有影响;但在多线程情况下,假如线程1执行lazyMan = new LazyMan()语句时先1再3,由于系统调度线程2的原因没来得及执行步骤2,但此时已有引用指向对象也就是lazyMan!=null,故线程2在第一次检查时不满足条件直接返回lazyMan,此时lazyMan为null
                    //synchronized关键字可保证lazyMan = new LazyMan()语句执行顺序为123,因其为非原子性依旧可能存在系统调度问题(即执行步骤时被打断),但能确保的是只要lazyMan!=0,就表明一定执行了属性初始化操作;而若在步骤3之前被打断,此时lazyMan依旧为null,其他线程可进入第一层检查向下执行创建对象
                }
            }
        }
        return lazyMan;
    }
 
    public static void main(String[] args) {
        for(int i=0;i<10;i++){
            new Thread(()->{
                lazyMan.getInstance();
            }).start();
        }
    }
}

在这里插入图片描述可以看到结果都是只有一个,按理来说是没有问题,实际上不是,上述标注行代码存在问题的,不是一个原子性操作。

【4】静态内部类单例

是不安全,存在问题的,是不安全,存在问题的(修改过的懒汉式单例也是如此)。

public class Inner {
    private Inner(){}
    public static Inner getInstance(){
        return InnerClass.INNER;
    }
    public static class InnerClass{
        private static final Inner INNER = new Inner();
    }
}
class Singleton06{
    //1-构造器私有化
    private Singleton06(){ }
    //2-本类内部创建对象实例(加上了volatile)
    private static volatile Singleton06 instance;
    //3-写一个静态内部类,该类中有一个静态属性
    private static class SingletonInstance{
        private static final Singleton06 INSTANCE=new Singleton06();
    }
    //4-提供一个静态的公有方法,直接返回SingletonInstance.INSTANCE
    public static synchronized Singleton06 getInstance(){
        return SingletonInstance.INSTANCE;
    }
}

(1)使用静态内部类完成对象实例化,这种方式采用类装载的机制来保证初始化实例时只有一个线程
(2)静态内部类方式在Singleton06类被装载时并不会立即实例化,而是在需要实例化时,才调用getInstance方法
(3)类的静态属性只会在第一次加载类的时候初始化,所以在这里,JVM帮助我们保证了线程的安全性,在类进行初始化的时候,别的线程是无法进入的
(4)所以说:避免了线程不安全,利用静态内部类特点实现延迟加载,效率高,推荐使用

【5】枚举(懒汉式)

enum Singleton07{
    INSTANCE;
    public void sayOK(){
        System.out.println("ok-");
    }
}

public class SingletonTest07 {
    public static void main(String[] args) {
        Singleton07 instance01=Singleton07.INSTANCE;
        Singleton07 instance02=Singleton07.INSTANCE;
        System.out.println(instance01==instance02);
        System.out.println("instance01-hashCode="+instance01.hashCode());
        System.out.println("instance02-hashCode="+instance02.hashCode());
        instance01.sayOK();
    }
}

【三】工厂方法模式

实例化对象不是用new,用工厂方法替代。将选择实现类,创建对象统一管理和控制。从而将调用者跟我们的实现类解耦。

以往博客
工厂模式

【1】简单工厂模式:

用来生产同一等级架构中的任意产品(对于增加新的产品,需要修改已有代码)
在简单工厂模式中,可以根据参数的不同返回不同类的实例。简单工厂模式专门定义一个类来负责创建其他类的实例。

接下来创建一个接口,两个实现类,一个工厂,一个测试类

(1)创建手机接口
(2)创建两个不同手机的实现类
(3)创建对象工厂,传参为手机品牌,工厂内部通过if判断品牌,返回对应品牌的手机对象
比较简单暴力,所以叫简单工厂模式

//创建手机接口
public interface Phone {
    void name();
}
//创建华为实现类
public class HuaWei implements Phone{
    @Override
    public void name() {
        System.out.println("华为手机");
    }
}
//创建小米实现类
public class XiaoMi implements Phone{
    @Override
    public void name() {
        System.out.println("小米手机");
    }
}
//创建工厂
public class PhoneFactory {
    public static Phone getPhone(String phone){
        if(phone.equals("华为")){
            return new HuaWei();
        }else if(phone.equals("小米")){
            return  new XiaoMi();
        }else {
            return null;
        }
    }
}
//测试类
public class Consumer {
    public static void main(String[] args) {
        Phone p1= PhoneFactory.getPhone("华为");
        Phone p2= PhoneFactory.getPhone("小米");
        p1.name();
        p2.name();
    }
}

我们通过创建一个PhoneFactory类,成功的完成工厂的创建。我们在创建对象时,也就不需要直接创建对象,而是可以通过创建工厂,这样大大的降低了代码的耦合性。但是,静态工厂模式是不能添加数据的。比如说,我们想添加一个“Oppo”手机类,你不直接修改PhoneFactory工厂代码,是不能实现的。所以,就有了第二种的工厂方法模式。

【2】工厂方法模式

用来生产同一等级架构中的固定产品,一个工厂等级结构可以负责多个不同产品等级结构中的产品对象的创建 。(支持增加任意产品)

//创建手机接口
public interface Phone {
    void name();
}
//创建华为实现类
public class HuaWei implements Phone{
    @Override
    public void name() {
        System.out.println("华为手机");
    }
}
//创建手机工厂接口
public interface PhoneFactory {
    Phone getPhone();
}
//创建华为工厂
public class HuaWeiFactory implements PhoneFactory{
    @Override
    public Phone getPhone() {
        return new HuaWei();
    }
}
//测试类
public class Consumer {
    public static void main(String[] args) {
        Phone phone = new HuaWeiFactory().getPhone();
        phone.name();
    }
}

我们创建了手机工厂接口PhoneFactory,再创建华为工厂HuaWeiFactory实现工厂,这样就可以通过HuaWeiFactory创建对象。增加新的具体工厂和产品族很方便,比如说,我们想要增加小米,只需要创建一个小米工厂XiaoMiFactory实现手机工厂接口PhoneFactory,合理的解决的简单工厂模式不能修改代码的缺点。但是,在现实使用中,简单工厂模式占绝大多数。

【3】抽象工厂模式

抽象工厂模式提供了一个创建一系列相关或者相互依赖对象的接口,无需指定它们具体的类(围绕一个超级工厂创建其他工厂,该超级工厂称为工厂的工厂)

抽象工厂模式得主要角色:
(1)抽象工厂(Abstract Factory)提供了创建产品的接口,它包含多个创建产品的方法 newProduct(),可以创建多个不同等级的产品。
(2)具体工厂(Concrete Factory)主要是实现抽象工厂中的多个抽象方法,完成具体产品的创建。
(3)抽象产品(Product)定义了产品的规范,描述了产品的主要特性和功能,抽象工厂模式有多个抽象产品。
(4)具体产品(ConcreteProduct)实现了抽象产品角色所定义的接口,由具体工厂来创建,它同具体工厂之间是多对一的关系。

//电脑接口
public interface Computer {
    void play();
    void watch();
}
//创建华为电脑对象
public class HuaWeiComputer implements Computer{
    @Override
    public void play() {
        System.out.println("HuaWei's play!");
    }
 
    @Override
    public void watch() {
        System.out.println("HuaWei's watch!");
    }
}
//手机接口
public interface Phone {
    void send();
    void call();
}
//创建华为手机对象
public class HuaWeiPhone implements Phone{
    @Override
    public void send() {
        System.out.println("HuaWei's send");
    }
 
    @Override
    public void call() {
        System.out.println("HuaWei's call");
    }
}
//抽象工厂
public interface IProductFactory {
    //生产手机
    Phone phone();
    //生产电脑
    Computer computer();
}
//创建华为工厂
public class HuaWeiFactory implements IProductFactory{
    @Override
    public Phone phone() {
        return new HuaWeiPhone();
    }
 
    @Override
    public Computer computer() {
        return new HuaWeiComputer();
    }
}
//测试类
public class Consumer {
    public static void main(String[] args) {
        HuaWeiFactory huaWeiFactory = new HuaWeiFactory();
        Phone phone = huaWeiFactory.phone();
        phone.call();
        phone.send();
        Computer computer = huaWeiFactory.computer();
        computer.play();
        computer.watch();
    }
}

我们通过创建一个抽象工厂完成了对具体工厂的创建,只需要传入参数就可以实例化对象。具体产品在应用层的代码隔离,无需关心创建的细节将一个系列的产品统一到一起创建。将一系列产品规划到一起创建。但是,抽象工厂模式也存在着缺点。规定了所有可能被创建的产品集合,产品簇中扩展新的产品困难,不可以增加产品,只能增加品牌。

【4】抽象工程模式的使用案例

(1)需求描述

公司早期介入七牛云OSS(对象存储服务)上传图片与视频,后因业务调整,巩乃斯要求额外支持阿里云、腾讯云等其他云服务商,并且可以对客户提供外界访问。

设计要求:允许在不破坏原有代码逻辑的情况下,实现对任意三方云厂商的接入。

(2)实现逻辑

(1)创建工厂的工厂

public interface AbstractOssFactory {
    public OssImage uploadImage(byte[] bytes);
    public OssVideo uploadVideo(byte[] bytes);
}

public interface OssImage {
    public String getThumb();
    public String getWatermark();
    public String getEnhance();
}

public interface OssVideo {
    public String get720p();
    public String get1080p();
}

(2)创建阿里云工厂的实现类

/**
 * @ClassName: AliyunOssFacotry
 * @Author: AllenSun
 * @Date: 2023/2/12 下午1:41
 */
public class AliyunOssFacotry implements AbstractOssFactory {
    @Override
    public OssImage uploadImage(byte[] bytes) {
        return new AliyunOssImage(bytes,"Allen",true);

    }

    @Override
    public OssVideo uploadVideo(byte[] bytes) {
        return new AliyunOssVideo(bytes,"Allen");
    }
}

/**
 * @ClassName: AliyunOssImage
 * @Author: AllenSun
 * @Date: 2023/2/12 下午1:43
 */
public class AliyunOssImage implements OssImage {

    private byte[] bytes;

    public AliyunOssImage(byte[] bytes, String watermark, boolean transparent) {
        this.bytes = bytes;
        System.out.println("【阿里云】图片已上传至阿里云OSS");
        System.out.println("【阿里云】已生成缩略图,尺寸640*480像素");
        System.out.println("【阿里云】已为图片新增水印,水印文本:"+watermark+",文本透明度:"+transparent);
        System.out.println("【阿里云】已将图片AI增强为4K极清画质");
    }

    @Override
    public String getThumb() {
        return "获取阿里云缩略图";
    }

    @Override
    public String getWatermark() {
        return "获取阿里云水印";
    }

    @Override
    public String getEnhance() {
        return "获取阿里云画质增强图";
    }
}

/**
 * @ClassName: AliyunOssImage
 * @Author: AllenSun
 * @Date: 2023/2/12 下午1:43
 */
public class AliyunOssVideo implements OssVideo {
    private byte[] bytes;

    public AliyunOssVideo(byte[] bytes, String watermark) {
        this.bytes = bytes;
        System.out.println("【阿里云】视频已上传至阿里云南OSS");
        System.out.println("【阿里云】720p转码成功,码率:5000K");
        System.out.println("【阿里云】1080p转码成功,码率:7000K");
    }

    @Override
    public String get720p() {
        return "返回阿里云720p视频";
    }

    @Override
    public String get1080p() {
        return "返回阿里云1080p视频";
    }
}

(3)创建七牛云工厂的实现类

/**
 * @ClassName: AliyunOssFacotry
 * @Author: AllenSun
 * @Date: 2023/2/12 下午1:41
 */
public class QiniuyunOssFacotry implements AbstractOssFactory {
    @Override
    public OssImage uploadImage(byte[] bytes) {
        return new QiniuyunOssImage(bytes,"Allen",true);
    }

    @Override
    public OssVideo uploadVideo(byte[] bytes) {
        return new QiniuyunOssVideo(bytes,"Allen");
    }
}

/**
 * @ClassName: AliyunOssImage
 * @Author: AllenSun
 * @Date: 2023/2/12 下午1:43
 */
public class QiniuyunOssImage implements OssImage {

    private byte[] bytes;

    public QiniuyunOssImage(byte[] bytes, String watermark, boolean transparent) {
        this.bytes = bytes;
        System.out.println("【七牛云】图片已上传至阿里云OSS");
        System.out.println("【七牛云】已生成缩略图,尺寸640*480像素");
        System.out.println("【七牛云】已为图片新增水印,水印文本:"+watermark+",文本透明度:"+transparent);
        System.out.println("【七牛云】已将图片AI增强为4K极清画质");
    }

    @Override
    public String getThumb() {
        return "获取七牛云缩略图";
    }

    @Override
    public String getWatermark() {
        return "获取七牛云水印";
    }

    @Override
    public String getEnhance() {
        return "获取七牛云画质增强图";
    }
}

/**
 * @ClassName: AliyunOssImage
 * @Author: AllenSun
 * @Date: 2023/2/12 下午1:43
 */
public class QiniuyunOssVideo implements OssVideo {
    private byte[] bytes;

    public QiniuyunOssVideo(byte[] bytes, String watermark) {
        this.bytes = bytes;
        System.out.println("【七牛云】视频已上传至阿里云南OSS");
        System.out.println("【七牛云】720p转码成功,码率:5000K");
        System.out.println("【阿里云】1080p转码成功,码率:7000K");
    }

    @Override
    public String get720p() {
        return "返回七牛云720p视频";
    }

    @Override
    public String get1080p() {
        return "返回七牛云1080p视频";
    }
}

(4)测试类

/**
 * @ClassName: Client
 * @Author: AllenSun
 * @Date: 2023/2/12 下午2:04
 */
public class Client {
    public static void main(String[] args) {
        AbstractOssFactory factory = new AliyunOssFacotry();
        OssImage ossImage = factory.uploadImage(new byte[1024]);
        OssVideo ossVideo = factory.uploadVideo(new byte[1024]);

        System.out.println(ossImage.getThumb());
        System.out.println(ossImage.getWatermark());
        System.out.println(ossImage.getEnhance());
        System.out.println(ossVideo.get720p());
        System.out.println(ossVideo.get1080p());
    }
}

【四】代理模式

以往博客
代理模式

由于某些原因需要给某对象提供一个代理以控制对该对象的访问。这时,访问对象不适合或者不能直接引用目标对象,代理对象作为访问对象和目标对象之间的中介。

【1】静态代理模式

角色分析:
(1)抽象角色:一般会使用接口或抽象类来解决
(2)真实角色:被代理的角色
(3)代理角色:代理真实角色,代理真实角色后我们会进行一些附属操作
(4)访问角色:访问代理对象的人

//租房
public interface Rent {
    void rent();
}
//房东
public class Master implements Rent{
    @Override
    public void rent() {
        System.out.println("Master rent");
    }
}
//中介
public class Proxy implements Rent{
    private Master master;
 
    public Proxy() {
    }
 
    public Proxy(Master master) {
        this.master = master;
    }
 
    @Override
    public void rent() {
        see();
        master.rent();
        fare();
    }
    //看房
    public void see(){
        System.out.println("see");
    }
    //收费
    public void fare(){
        System.out.println("fare");
    }
}
//测试类
public class Consumer {
    public static void main(String[] args) {
        Master master = new Master();
        //进行代理
        Proxy proxy = new Proxy(master);
        //不需要通过对象,直接通过代理完成响应业务
        proxy.rent();
    }
}

得到测试结果:see——》Master rent——》fare

可以从上述代码看出,我们通过创建中介这一代理完成了租房,并且还有看房、收费的附属操作。我们不需要使用房东对象,通过使用代理中介就可以完成操作。
(1)代理模式优点:可以使真实角色的操作更加纯粹!不用去关注一些公共的业务,公共也就可以交给代理角色,实现了业务的分工,公共业务发生扩展的时候,方便集中管理!
(2)代理模式缺点:一个真实角色就会产生一个代理角色;代码量会翻倍开发效率会变低,也许,这样无法理解到代理模式的好处。举个例子也许能更好理解,比如说我们想要在原有固定功能上新增业务,按照开闭原则我们是不能对原有代码进行修改的。但是我们可以通过代理模式,增加代理,在实现原有功能的情况下写入新的功能,创建对象时也就可以使用代理,完成操作。

【2】动态代理模式

虽然静态代理模式可以很好的解决开闭原则,但是每有一个真实角色就会产生一个代理,代码量翻倍过于臃肿,开发效率较低。因此,我们就使用动态代理模式进行设计。

//接口
public interface IUserService {
    void add();
    void delete();
    void update();
    void query();
 
}
//实现类
public class UserServiceImpl implements IUserService {
    @Override
    public void add() {
        System.out.println("add");
    }
 
    @Override
    public void delete() {
        System.out.println("delete");
    }
 
    @Override
    public void update() {
        System.out.println("update");
    }
 
    @Override
    public void query() {
        System.out.println("query");
    }
}
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
//自动生成动态代理类模板
public class ProxyInvocationHandler implements InvocationHandler {
    //被代理接口
    private Object target;
   
    public void setTarget(Object target) {
        this.target = target;
    }
     //得到代理类
    public Object getProxy() {
        return Proxy.newProxyInstance(getClass().getClassLoader(), target.getClass().getInterfaces(), this);
    }
    public void log(String s) {
        System.out.println("[debug]:" + s);
    }
    //得到代理类
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        log(method.getName());
        Object result = method.invoke(target, args);
        return result;
    }
}
//测试类
public class Consumer {
    public static void main(String[] args) {
        UserServiceImpl userService = new UserServiceImpl();
        ProxyInvocationHandler handler = new ProxyInvocationHandler();
        //设置代理对象
        handler.setTarget(userService);
        //生成代理类
        IUserService proxy = (IUserService)handler.getProxy();
        proxy.add();
        proxy.query();
    }
}

通过测试我们可以顺利的使用动态代理模式完成一系列操作,当我们想要添加附属操作时,我们只需要在模板中进行添加。优点:
(1)可以使真实角色的操作更加纯粹!不用去关注一些公共的业务。
(2)公共也就可以交给代理角色!实现了业务的分工。
(3)公共业务发生扩展的时候,方便集中管理。
(4)一个动态代理类代理的是一个接口,一般就是对应的一类业务。
(5)一个动态代理类可以代理多个类,只要是实现了同一个接口即可!

【五】装饰者模式

以往博客
装饰者模式

动态的将新功能附加到对象上。在对象功能的拓展方面,比继承更有弹性。同时装饰者模式也体现了开闭原则。

角色分析:
(1)抽象构件(Component)角色:定义一个抽象接口以规范准备接收附加责任的对象。
(2)具体构件(ConcreteComponent)角色:实现抽象构件,通过装饰角色为其添加一些职责。
(3)抽象装饰(Decorator)角色:继承抽象构件,并包含具体构件的实例,可以通过其子类扩展具体构件的功能。
(4)具体装饰(ConcreteDecorator)角色:实现抽象装饰的相关方法,并给具体构件对象添加附加的责任。

//定义抽象类
public abstract class Drink {
    public abstract double cost();
}
//定义两个抽象类的实现类
public class Juice extends Drink{
    @Override
    public double cost() {
        System.out.println("juice: "+16);
        return 16;
    }
}
public class Milk extends Drink{
    @Override
    public double cost() {
        System.out.println("milk: "+12);
        return 12;
    }
}
//定义装饰抽象类
public abstract class Decorator extends Drink {
    public abstract double cost();
}
//定义两个装饰具体实现类
public class Chocolate extends Decorator{
    private final static double COST = 4;
    private Drink drink;
 
    public Chocolate(Drink drink) {
        this.drink = drink;
    }
 
    @Override
    public double cost() {
        System.out.println("chocolate:"+4);
        return COST+drink.cost();
    }
}
public class Pudding extends Decorator{
    private final static double COST = 5;
    private Drink drink;
 
    public Pudding(Drink drink) {
        this.drink = drink;
    }
 
    @Override
    public double cost() {
        System.out.println("pudding:"+5);
        return COST+drink.cost();
    }
}
//测试类
public class Test {
    public static void main(String[] args) {
        Drink milk = new Milk();
        milk = new Pudding(milk);
        milk = new Chocolate(milk);
        System.out.println(milk.cost());
    }
}

得到测试结果
chocolate:4
pudding:5
milk: 12
21.0

可以看到非常简单的就能够完成具体构件和具体装饰的组合。也可以看到结构也非常简洁明,具体构件在自己的package中,具体装饰也在自己的package中。我们不管是想要增加具体构件还是具体配饰,都可以在各自的package中添加。对已有的代码不需要进行任何操作,就算新加的有bug也不会影响原有代码的操作。

【六】观察者模式

以往博客
观察者模式

对象间的一种一对多依赖关系,使得每当一个对象状态发生改变时,其相关依赖对象皆得到通知并被自动更新。这种模式有时又称作发布-订阅模式、模型-视图模式,它是对象行为型模式。

观察者模式优点:
(1)降低了目标与观察者之间的耦合关系,两者之间是抽象耦合关系。符合依赖倒置原则。
(2)目标与观察者之间建立了一套触发机制。

//定义观察者接口
public interface Observer {
    void response();
}
//具体化观察者1
public class Observer1 implements Observer{
    @Override
    public void response() {
        System.out.println("Observer1 action");
    }
}
//具体化观察者2
public class Observer2 implements Observer{
    @Override
    public void response() {
        System.out.println("Observer2 action");
    }
}
//抽象目标
public abstract class Subject {
    //创建观察者集合
    protected ArrayList<Observer> array = new ArrayList<Observer>();
    //增加观察者
    public void add(Observer observer){
        array.add(observer);
    }
    //删除观察者
    public void remove(Observer observer){
        array.remove(observer);
    }
    //通知观察者方法
    public abstract void notifyObserver();
}
//具体化目标
public class Subject1 extends Subject{
    @Override
    public void notifyObserver() {
        for (Observer observer :array){
            observer.response();
        }
    }
}
//测试类
public class Test {
    public static void main(String[] args) {
        Subject subject = new Subject1();
        Observer obs1 = new Observer1();
        Observer obs2 = new Observer2();
        subject.add(obs1);
        subject.add(obs2);
        subject.notifyObserver();
    }
}

通过测试我们可以看到,我们不需要建立太多观察者和具体目标之间的联系,大大降低了目标与观察者之间的耦合关系。并且结构也十分简单明了,观察者和目标分别在各自的package包下。当我们想要添加观察者时,只需要在观察者包下进行添加,实现Observer接口就可以了。

【七】责任链模式

以往博客
责任链模式

【1】介绍

一种处理请求的模式,它让多个处理器都有机会处理该诘求,直到其中某个处理成功为止。责任链模式把多个处理器串成链,然后让请求在链上传递。

责任链模式的主要角色
(1)抽象处理者(Handler)角色:定义一个处理请求的接口,包含抽象处理方法和一个后继连接。
(2)具体处理者(Concrete Handler)角色:实现抽象处理者的处理方法,判断能否处理本次请求,如果可以处理请求则处理,否则将该请求转给它的后继者。
(3)客户类(Client)角色:创建处理链,并向链头的具体处理者对象提交请求,它不关心处理细节和请求的传递过程。

责任链模式优点
(1)降低了对象之间的耦合度。处理者不需要知道客户的任何信息,客户也不要知道处理者是如何实现方法的。
(2)提高了系统的灵活性。当我们想要新增处理器到整个链条中时,所付出的代价是非常小的

责任链模式缺点
(1)降低了系统的性能。对比较长的职责链,请求的处理可能涉及多个处理对象
不能保证每个请求一定被处理。由于一个请求没有明确的接收者,所以不能保证它一定会被处理,该请求(2)可能一直传到链的末端都得不到处理。

【2】使用案例一

//抽象处理者
public abstract class Handler {
    private Handler next;
    public void setNext(Handler next) { this.next = next; }
    public Handler getNext() { return next; }
 
    //处理请求
    public abstract void handleRequest(int info);
}
//具体处理者1
public class Handler1 extends Handler{
    @Override
    public void handleRequest(int info) {
        if (info <10){
            System.out.println("Handler1完成处理");
        }else {
            if (getNext()!=null){
                getNext().handleRequest(info);
            }else {
                System.out.println("没有处理者进行处理");
            }
        }
    }
}
//具体处理者2
public class Handler2 extends Handler{
    @Override
    public void handleRequest(int info) {
        if (info <20&&info>10){
            System.out.println("Handler2完成处理");
        }else {
            if (getNext()!=null){
                getNext().handleRequest(info);
            }else {
                System.out.println("没有处理者进行处理");
            }
        }
    }
}
测试类
public class Test {
    public static void main(String[] args) {
        Handler handler1 = new Handler1();
        Handler handler2 = new Handler2();
        handler1.setNext(handler2);
        handler1.handleRequest(5);
        handler1.handleRequest(15);
        handler1.handleRequest(25);
    }
}

得到测试结果:

Handler1完成处理
Handler2完成处理
没有处理者进行处理

通过测试结果我们看到,5交给了Handler1处理,15交给了Handler2处理,而25则没有处理者处理。请求者根本不需要参与处理,只需要提交数据就可以完成功能的处理,完全不需要管是哪个处理者进行处理的。当我们想要继续添加处理者时,这只需要再次添加就可以了,也不会对之前的代码造成影响。

【3】责任链模式解决登录过程层层校验问题

(1)传统方式的登录校验代码

public class MemberService {
    public static void main(String[] args) {
        MemberService service = new MemberService();
        service.login("tom", "666");
    }

    public void login(String loginName, String loginPass) {
        if (StringUtils.isEmpty(loginName) ||
                StringUtils.isEmpty(loginPass)) {
            System.out.println("用户名和密码为空");
            return;
        }
        System.out.println("用户名和密码不为空,可以往下执行");
        Member member = checkExists(loginName, loginPass);
        if (null == member) {
            System.out.println("用户不存在");
            return;
        }
        System.out.println("登录成功!");
        if (!"管理员".equals(member.getRoleName())) {
            System.out.println("您不是管理员,没有操作权限");
            return;
        }
        System.out.println("允许操作");
    }

    private Member checkExists(String loginName, String loginPass) {
        Member member = new Member(loginName, loginPass);
        member.setRoleName("管理员");
        return member;
    }
}

(2)创建抽象的处理类Handler

public abstract class Handler {
    protected Handler next;
    public void next(Handler next){ this.next = next;}
    public abstract void doHandler(Member member);
}

(3)创建扩展了处理类的实体类
我们分别创建非空校验ValidateHandler类、登录校验LoginHandler类和权限校验AuthHandler类

public class ValidateHandler extends Handler {
    public void doHandler(Member member) {
        if (StringUtils.isEmpty(member.getLoginName()) ||
                StringUtils.isEmpty(member.getLoginPass())) {
            System.out.println("用户名和密码为空");
            return;
        }
        System.out.println("用户名和密码不为空,可以往下执行");
        next.doHandler(member);
    }
}

public class LoginHandler extends Handler {
    public void doHandler(Member member) {
        System.out.println("登录成功!");
        member.setRoleName("管理员");
        next.doHandler(member);
    }
}

public class AuthHandler extends Handler {
    public void doHandler(Member member) {
        if (!"管理员".equals(member.getRoleName())) {
            System.out.println("您不是管理员,没有操作权限");
            return;
        }
        System.out.println("允许操作");
    }
}

(4)在服务层设置链

public class MemberService {

    public void login(String loginName, String loginPass) {
        Handler validateHandler = new ValidateHandler();
        Handler loginHandler = new LoginHandler();
        Handler authHandler = new AuthHandler();
        validateHandler.next(loginHandler);
        loginHandler.next(authHandler);
        validateHandler.doHandler(new Member(loginName, loginPass));
    }
}

(5)测试代码

public class Test {
    public static void main(String[] args) {
        MemberService memberService = new MemberService();
        memberService.login("tom","666");
    }
}

【4】责任链模式+建造者模式结合使用优化

因为责任链模式具备链式结构,而上面代码中,我们看到,负责组装链式结构的角色是
MemberService ,当链式结构较长时,MemberService的工作会非常繁琐,并且MemberService代码相对臃肿,且后续更改处理者或消息类型时,都必须在MemberService中进行修改,,不符合开闭原则。产生这些问题的原因就是因为链式结构的组装过于复杂,而对于复杂结构的创建,很自然我们就会想到建造者模式,使用建造者模式,我们完全可以MemberService指定的处理节点对象进行自动链式组装,客户只需指定处理节点对象,其他任何事情都无需关心,并且客户指定的处理节点对象顺序不同 ,构造出来的链式结构也随之不同。我们来改造一下,修改Handler的代码:

public abstract class Handler<T> {
    protected Handler next;
    //public void next(Handler next){ this.next = next;}
    private void next(Handler next){ this.next = next;}

    public abstract void doHandler(Member member);

    public static class Builder<T>{
        private Handler<T> head;
        private Handler<T> tail;

        public Builder<T> addHandler(Handler handler){
           // do {
                if (this.head == null) {
                    this.head = this.tail = handler;
                    return this;
                }
                this.tail.next(handler);
                this.tail = handler;
           // }while (false);//真正框架中,如果是双向链表,会判断是否已经到了尾部
            return this;
        }

        public Handler<T> build(){
            return this.head;
        }
    }
}

然后 ,修改MemberService的代码:

public class MemberService {
    public void login(String loginName,String loginPass){
        Handler.Builder builder = new Handler.Builder();
        builder.addHandler(new ValidateHandler())
               .addHandler(new LoginHandler())
               .addHandler(new AuthHandler());
        builder.build().doHandler(new Member(loginName,loginPass));
        //用过Netty的人,肯定见过
    }
}

【八】策略模式

以往博客
策略模式

【1】策略模式的使用过程?

1-定义策略的接口interface,将策略抽象化提取出来
2-定义各种策略类,实现策略接口,在springboot中给每个策略类加上@Service(“WechatPay”)注解,将策略类放入IOC容器管理
3-使用策略的时候,直接根据参数PaymentType,到IOC容器中找到对应的策略类Bean对象,然后调用对应策略类的方法。

IPayment payment = applicationContext.getBean(order.getPaymentType(), IPayment.class);

参数是WechatPay,会到IOC容器中获取名称为WechatPay的Bean对象,也就是有注解@Service(“WechatPay”)的策略类,实例化对象后就可以调用对应的方法了。

【2】抽离出来的策略类如何管理?如何取消if-else的判断?

(1)如果是springboot,可以给策略类加上注解交给IOC容器管理,可以根据bean的名称直接获取到对应的类,也就不用if-else判断了
(2)还可以使用工厂模式

【3】案例一:使用不同的支付方式

public interface IPayment {
    /**
     * 支付
     * @param order
     * @return
     */
    PayResult pay(Order order);
}


@Service("WechatPay")
public class WechatPay implements IPayment {

    @Override
    public PayResult pay(Order order) {
        return new PayResult("微信支付成功");
    }

}

@Service("Alipay")
public class Alipay implements IPayment {

    @Override
    public PayResult pay(Order order) {
        return new PayResult("支付宝支付成功");
    }
}

@Service("UnionPay")
public class UnionPay implements IPayment {

    @Override
    public PayResult pay(Order order) {
        return new PayResult("云闪付支付成功");
    }
}

@RestController
public class PayService {

    @Autowired
    private ApplicationContext applicationContext;

    /**
     * 支付接口
     * @param amount
     * @param paymentType
     * @return
     */
    @RequestMapping("/pay")
    public PayResult pay(@RequestParam("amount") int amount,
                    @RequestParam("paymentType") String paymentType) {
        Order order = new Order();
        order.setAmount(amount);
        order.setPaymentType(paymentType);

        // 根据支付类型获取对应的策略 bean
        IPayment payment = applicationContext.getBean(order.getPaymentType(), IPayment.class);

        // 开始支付
        PayResult payResult = payment.pay(order);

        return payResult;
    }

}

【4】案例二:策略模式+工厂模式来解决请假情况判断问题

看看用if-else怎么处理

/**
 * @ClassName: LeaveServiceOld
 * @Author: AllenSun
 * @Date: 2023/2/11 下午9:40
 */
public class LeaveServiceOld {
    public void audit(LeaveForm leaveForm) {
        if(leaveForm.getDays()<=3 && leaveForm.getType()==1){
            System.out.println("三天以下婚丧假无需审批,自动通过");
        }
        if(leaveForm.getDays()>3 && leaveForm.getType()==1){
            System.out.println("三天以上婚丧假,进入上级审批流程");
        }
        if(leaveForm.getEmployee().getLevel()==9){
            System.out.println("总经理请假无需审批自动通过");
        }
        if(leaveForm.getDays()==1 && leaveForm.getType()==0){
            System.out.println("一天病假无需审批自动通过");
        }
        if(leaveForm.getDays()>1 && leaveForm.getType()==0){
            System.out.println("一天以上病假,进入上级审批流程");
        }
    }
}

开始使用策略模式进行改写,定义策略接口

public interface AuditStrategy {
    // 判断条件是否匹配
    public boolean isSupport(LeaveForm form);
    // 审核业务逻辑
    public void  audit(LeaveForm leaveForm);
    // 规则冲突时的优先级
    public int getPriority();
    // 规则名称
    public String getName();
}

/** 三天以下婚丧假审批规则
 * @ClassName: AuditStrategyImpl_01
 * @Author: AllenSun
 * @Date: 2023/2/11 下午10:16
 */
public class AuditStrategyImpl_01 implements AuditStrategy {
    @Override
    public boolean isSupport(LeaveForm form) {
        return form.getDays()<=3 && form.getType()==1;
    }

    @Override
    public void audit(LeaveForm leaveForm) {
        System.out.println(leaveForm);
        System.out.println("三天以下婚丧假无需审批,自动通过");
    }

    @Override
    public int getPriority() {
        return 0;
    }

    @Override
    public String getName() {
        return "三天以下婚丧假审批规则";
    }
}

/** 三天以上婚丧假审批规则
 * @ClassName: AuditStrategyImpl_01
 * @Author: AllenSun
 * @Date: 2023/2/11 下午10:16
 */
public class AuditStrategyImpl_02 implements AuditStrategy {
    @Override
    public boolean isSupport(LeaveForm form) {
        return form.getDays()>3 && form.getType()==1;
    }

    @Override
    public void audit(LeaveForm leaveForm) {
        System.out.println(leaveForm);
        System.out.println("三天以上婚丧假,进入上级审批流程");
    }

    @Override
    public int getPriority() {
        return 0;
    }

    @Override
    public String getName() {
        return "三天以上婚丧假审批规则";
    }
}

开始去除if-else,使用工厂模式,创建单例工厂

/**
 * @ClassName: AuditStrategyFactory
 * @Author: AllenSun
 * @Date: 2023/2/11 下午10:25
 */
public class AuditStrategyFactory {
    // 单例工厂
    private final static AuditStrategyFactory FACTORY = new AuditStrategyFactory();

    // 创建一个list存放所有的策略类
    private List<AuditStrategy> auditStrategyList = new ArrayList<>();

    // 在构造器初始化里将所有策略类放入list中存放
    private AuditStrategyFactory() {
        auditStrategyList.add(new AuditStrategyImpl_01());
        auditStrategyList.add(new AuditStrategyImpl_02());
        auditStrategyList.add(new AuditStrategyImpl_03());
        auditStrategyList.add(new AuditStrategyImpl_04());
        auditStrategyList.add(new AuditStrategyImpl_05());
    }

    // 暴露一个方法,供外界获取单例工厂
    public static AuditStrategyFactory getInstance(){
        return FACTORY;
    };

    // 根据请假单的信息,返回对应的策略方法
    public AuditStrategy getAuditStrategy(LeaveForm leaveForm) {
        AuditStrategy auditStrategy = null;
        // 遍历list里的所有策略类,判断条件是否匹配
        for (AuditStrategy strategy : auditStrategyList) {
            // 如果策略的条件跟请假单的条件相符
            if(strategy.isSupport(leaveForm)){
                if(auditStrategy==null){
                    // 获得目标策略类
                    auditStrategy=strategy;
                } else {
                    // 判断策略的优先级
                    if(strategy.getPriority()==auditStrategy.getPriority()){
                        // 如果两个策略类的优先级相同,则抛出异常
                        throw new RuntimeException("["+auditStrategy.getName()+"]:["+strategy.getName()+"]规则冲突,请尽快解决。");
                    } else if(strategy.getPriority()>auditStrategy.getPriority()){
                        auditStrategy=strategy;
                    } else {
                        //do nothing
                    }
                }
            }
        }
        if(auditStrategy==null){
            throw new RuntimeException("没有匹配的请假审核规则");
        } else {
            // 返回最终获取到的策略类
            return auditStrategy;
        }
    }
}


【九】其他常用设计模式

(1)模板模式
模板模式

(2)组合模式
组合模式

(3)命令模式
命令模式

(4)适配器模式

适配器模式

【十】设计模式的组合使用

【1】策略模式+工厂模式

(1)场景描述

假设有两种指标:指标A和指标B,在对指标进行固化存储的时候,指标A和指标B的固化逻辑和处理不一样,如何用策略模式和工厂模式组合来设计

(2)思路

最简单最笨的方式就是直接ifelse判断指标类型indexType,但是如果使用设计模式,可以使用接口把两种类型指标的共同之处抽象出来,然后在各自的实现类里实现,并且实现类的对象通过工厂模式来生成。

(3)实现过程

(1)定义抽象接口,定义对象的所有抽象方法

public interface IndexStorageService {
	// 获取支持的指标类型集合
    Set<IndexConstant.IndexTypeEnum> supportIndexType();
    String getServiceCode();
    // 指标固化。指标计算结果不为空
    StorageResult indexStore(StorageContext storageContext);
    // 指标固化成功后置操作
    void indexStoreAfterSuccess(StorageContext storageContext);
    // 指标固化失败后置操作
    void indexStoreAfterFail(StorageContext storageContext);
}

(2)定义实现类的父类,实现共用的方法
这里的方法不用区分指标类型,真正要区分指标类型的方法放到具体的实现子类中实现

public abstract class AbstractStorageService implements IndexStorageService {
    public void indexCalcRsToStorage(StorageContext storageContext) {
    	log.info("指标计算结果开始转移到数仓计算结果表");
    }
}

(3)指标A的固化实现类

@Slf4j
@Service
@AllArgsConstructor
public class IndexBasicStorageService extends AbstractStorageService {
    // 定义好类型A支持的类型集合
    @Override
    public Set<IndexConstant.IndexTypeEnum> supportIndexType() {
        return new HashSet<>(
                Arrays.asList(
                        IndexConstant.IndexTypeEnum.ATOMIC_INDEX,
                        IndexConstant.IndexTypeEnum.DERIVED_INDEX,
                        IndexConstant.IndexTypeEnum.COMPOSITE_INDEX
                )
        );
    }

    @Override
    public String getServiceCode() {
        return null;
    }

    @Override
    public StorageResult indexStore(StorageContext storageContext) {

        StorageResult result = new StorageResult();
        try {
            indexCalcRsToStorage(storageContext);
            log.info("基于模型的原子、衍生、复合指标计算结果固化完成");
            result.success();
        } catch (Exception e) {
            result.setReadNum(0);
            result.setStorageNum(0);
            result.setCanRetry(false);
            result.setErrorMsg(e.getMessage());
            log.error("指标固化失败:{}", e.getMessage(), e);
        }
        return result;
    }

	@Override
    public void indexStoreAfterSuccess(StorageContext storageContext) {
    }

	@Override
    public void indexStoreAfterFail(StorageContext storageContext) {
    }
}

(4)指标B的固化实现类

public class IndexCustomStorageService extends AbstractStorageService {
    @Override
    public Set<IndexConstant.IndexTypeEnum> supportIndexType() {
        return new HashSet<>(
                Arrays.asList(
                        IndexConstant.IndexTypeEnum.CUSTOM_INDEX,
                        IndexConstant.IndexTypeEnum.CUSTOM_INDEX_DEFINITION
                )
        );
    }


    @Override
    public String getServiceCode() {
        return null;
    }


    @Override
    public StorageResult indexStore(StorageContext storageContext) {
    
    }
    
	@Override
    public void indexStoreAfterSuccess(StorageContext storageContext) {
    
    }

	@Override
    public void indexStoreAfterFail(StorageContext storageContext) {
    
    }
}

(5)定义指标固化的工厂类
通过工厂的方法,传参指标的类型,就会创建并返回对应类型的固化实现类对象

1-定义一个静态的map,key为指标类型,value为固化对象
2-定义一个init方法,取出来接口的所有实现类的bean名称放进map,SpringUtil是hutool里的工具类
3-定义一个getByIndexType方法,根据传参的indexType,从map里取出来对应的对象返回

public class IndexStorageServiceHelper {
    private static final Map<IndexConstant.IndexTypeEnum, IndexStorageService> INDEX_TYPE_INDEX_STORAGE_MAP = new ConcurrentHashMap<>();

    public static void init() {
        Map<String, IndexStorageService> serviceMap = SpringUtil.getBeansOfType(IndexStorageService.class);
        for (Map.Entry<String, IndexStorageService> entry : serviceMap.entrySet()) {
            IndexStorageService storageService = entry.getValue();
            if (CollectionUtil.isNotEmpty(storageService.supportIndexType())) {
                for (IndexConstant.IndexTypeEnum indexType : storageService.supportIndexType()) {
                    INDEX_TYPE_INDEX_STORAGE_MAP.put(indexType, storageService);
                }
            }
        }
    }

    public static IndexStorageService getByIndexType(IndexConstant.IndexTypeEnum indexType) {
        if (ObjectUtil.isNull(indexType)) {
            return null;
        }
        return INDEX_TYPE_INDEX_STORAGE_MAP.get(indexType);
    }
}

(6)业务使用

//固化service
IndexConstant.IndexTypeEnum indexType = IndexConstant.IndexTypeEnum.getInstance(indIndex.getIndexType());
IndexStorageService storageService = IndexStorageServiceHelper.getByIndexType(indexType);
Assert.notNull(storageService, "暂不支持数据库:{} 入库操作", dbTypeStr);
StorageResult result = storageService.indexStore(storageContext);

if (result.isExec()) {
    try {
        storageService.indexStoreAfterSuccess(storageContext);
        log.info("指标固化成功,后置处理完毕");
        indInstanceLogRepo.createInstanceLog(taskInstance, InstanceTypeEnum.INDEX.getCode(),"指标固化成功,后置处理完毕", null);
    } catch (Exception e) {
        log.info("指标固化成功,后置操作出现异常:{}", e.getMessage(), e);
        indInstanceLogRepo.createInstanceLog(taskInstance, InstanceTypeEnum.INDEX.getCode(),"指标固化成功,后置操作出现异常:"+e.getMessage(), e.toString());
    }
} else {
    try {
        storageService.indexStoreAfterFail(storageContext);
        log.info("指标固化失败,后置处理完毕");
        indInstanceLogRepo.createInstanceLog(taskInstance, InstanceTypeEnum.INDEX.getCode(),"指标固化失败,后置处理完毕", null);
    } catch (Exception e) {
        log.info("指标固化失败,后置操作出现异常:{}", e.getMessage(), e);
        indInstanceLogRepo.createInstanceLog(taskInstance, InstanceTypeEnum.INDEX.getCode(),"指标固化失败,后置操作出现异常:" + e.getMessage(), e.toString());
    }
}

【2】策略模式+工厂模式

相关推荐

  1. 常用设计模式(单例模式工厂模式

    2024-07-14 19:48:02       37 阅读
  2. 常用设计模式

    2024-07-14 19:48:02       25 阅读
  3. 前端常用设计模式

    2024-07-14 19:48:02       62 阅读
  4. pytorch常用模块函数汇总(2)

    2024-07-14 19:48:02       35 阅读
  5. 常见设计模式

    2024-07-14 19:48:02       22 阅读
  6. js设计模式汇总

    2024-07-14 19:48:02       41 阅读

最近更新

  1. docker php8.1+nginx base 镜像 dockerfile 配置

    2024-07-14 19:48:02       70 阅读
  2. Could not load dynamic library ‘cudart64_100.dll‘

    2024-07-14 19:48:02       74 阅读
  3. 在Django里面运行非项目文件

    2024-07-14 19:48:02       62 阅读
  4. Python语言-面向对象

    2024-07-14 19:48:02       72 阅读

热门阅读

  1. 11.FreeRTOS_事件组

    2024-07-14 19:48:02       21 阅读
  2. linux常用命令

    2024-07-14 19:48:02       18 阅读
  3. Linux 软件工具安装

    2024-07-14 19:48:02       21 阅读
  4. C/C++指针&智能指针一

    2024-07-14 19:48:02       17 阅读
  5. Spring Boot

    2024-07-14 19:48:02       15 阅读
  6. pnpm 如何安装指定版本

    2024-07-14 19:48:02       26 阅读