青梅煮酒设计模式之美

设计模式之美

重要性
1应付面试
2告别被别人吐槽

如果说数据结构和算法是教你如何写出高效代码,那设计模式讲的是如何写出可扩展、可读、可维护的高质量代码,所以,它们跟平时的编码会有直接的关系,也会直接影响到你的开发能力。

当然,在这些年的工作经历中,我也看到过很多让我眼前一亮的代码。每当我看到这样的好代码,都会立刻对作者产生无比的好感和认可。且不管这个人处在公司的何种级别,从代码就能看出,他是一个基础扎实的高潜员工,值得培养,前途无量!因此,代码写得好,能让你在团队中脱颖而出。

3提高复杂代码设计和开发能力
4让读源码,学框架事半功倍
优秀的开源项目、框架、中间件,代码量、类的个数都会比较多,类结构、类之间的关系极其复杂,常常调用来调用去。所以,为了保证代码的扩展性、灵活性、可维护性等,代码中会使用到很多设计模式、设计原则或者设计思想。如果你对设计模式、原则、思想非常了解,一眼就能参透作者的设计思路、设计初衷,很快就可以把脑容量释放出来,重点思考其他问题,代码读起来就会变得轻松了。
​
实际上,除了看不懂、看不下去的问题,还有一个隐藏的问题,你可能自己都发现不了,那就是你自己觉得看懂了,实际上,里面的精髓你并没有 get 到多少!

因此,学好设计模式相关的知识,不仅能让你更轻松地读懂开源项目,还能更深入地参透里面的技术精髓,做到事半功倍。

5为职场发展做铺垫
再者,在技术这条职场道路上,当成长到一定阶段之后,你势必要承担一些指导培养初级员工、新人,以及 code review 的工作。这个时候,如果你自己都对“什么是好的代码?如何写出好的代码?”不了解,那又该如何指导别人,如何让人家信服呢?
​
除此之外,代码质量低还会导致线上 bug 频发,排查困难。整个团队都陷在成天修改无意义的低级 bug、在烂代码中添补丁的事情中。而一个设计良好、易维护的系统,可以解放我们的时间,让我们做些更加有意义、更能提高自己和团队能力的事情。
投资要趁早,这样我们才能尽早享受复利。同样,有些能力,要早点锻炼;有些东西,要早点知道;有些书,要早点读。这样在你后面的生活、工作、学习中,才能一直都发挥作用。
如何写出高质量代码
可维护,可读性,可扩展性,灵活性,简洁性,可复用性,可测试性。

image-20230129142812368

image-20230129142924548

image-20230129143032412

image-20230129143757423

设计模式

创建型

创建型模式主要解决对象的创建问题,封装复杂的创建过程,解耦对象的创建代码和使用代码。

其中,单例模式用来创建全局唯一的对象。工厂模式用来创建不同但是相关类型的对象(继承同一父类或者接口的一组子类),由给定的参数来决定创建哪种类型的对象。建造者模式是用来创建复杂对象,可以通过设置不同的可选参数,“定制化”地创建不同的对象。原型模式针对创建成本比较大的对象,利用对已有对象进行复制的方式进行创建,以达到节省创建时间的目的。

单例模式

单例设计模式(Singleton Design Pattern)理解起来非常简单。一个类只允许创建一个对象(或者实例),那这个类就是一个单例类,这种设计模式就叫作单例设计模式,简称单例模式。

如何实现

构造函数需要是 private 访问权限的,这样才能避免外部通过 new 创建实例;

考虑对象创建时的线程安全问题;public static final

考虑是否支持延迟加载;

考虑 getInstance() 性能是否高(是否加锁)。

饿汉式

不支持延迟加载

类加载过程中 instance静态实例就已经创建并初始化好了,因此时线程安全的

public class IdGenerator {
    private AtomicLong id = new AtomicLong(0); 
​
    private static final IdGenerator instance = new IdGenerator(); // 静态初始化单例对象
​
    private IdGenerator() {} // 私有构造函数,防止外部创建实例
​
    public static IdGenerator getInstance() { // 获取单例对象的方法
        return instance;
    }
​
    public long getId() { 
        return id.incrementAndGet(); 
    }
}
懒汉式

支持延迟加载

线程安全,加锁

public class IdGenerator {
    private AtomicLong id = new AtomicLong(0);  
​
    private static IdGenerator instance;  // 声明一个静态的类成员变量,用于持有单例对象
​
    private IdGenerator() {}  // 私有构造函数,防止外部直接创建实例
​
    public static synchronized IdGenerator getInstance() {  // 静态方法,用于获取单例对象,添加了synchronized关键字保证线程安全
        if (instance == null) {  
            instance = new IdGenerator();  // 创建单例对象
        }
        return instance;  
    }
​
    public long getId() {  
        return id.incrementAndGet();  
    }
}

缺点明显,加锁了,并发度很低,恰好是单例使用期间,一直会被使用,如果频繁使用则不可取。

双重检测

饿汉式不支持延迟加载,懒汉式有性能问题,不支持高并发。那我们再来看一种既支持延迟加载、又支持高并发的单例实现方式,也就是双重检测实现方式。

在这种实现方式中,只要 instance 被创建之后,即便再调用 getInstance() 函数也不会再进入到加锁逻辑中了。所以,这种实现方式解决了懒汉式并发度低的问题。具体的代码实现如下所示:

public class IdGenerator {
    private AtomicLong id = new AtomicLong(0);  
​
    private static IdGenerator instance;  
​
    private IdGenerator() {}  
​
    public static IdGenerator getInstance() {  
        if (instance == null) {  
            synchronized (IdGenerator.class) {  
                if (instance == null) {  
                    instance = new IdGenerator();  
                }
            }
        }
        return instance;  
    }
​
    public long getId() {  
        return id.incrementAndGet();  
    }
}
网上有人说,这种实现方式有些问题。因为指令重排序,可能会导致 IdGenerator 对象被 new 出来,并且赋值给 instance 之后,还没来得及初始化(执行构造函数中的代码逻辑),就被另一个线程使用了。
​
要解决这个问题,我们需要给 instance 成员变量加上 volatile 关键字,禁止指令重排序才行。
​
实际上,只有很低版本的 Java 才会有这个问题。我们现在用的高版本的 Java 已经在 JDK 内部实现中解决了这个问题(解决的方法很简单,只要把对象 new 操作和初始化操作设计为原子操作,就自然能禁止重排序)。关于这点的详细解释,跟特定语言有关,我就不展开讲了,感兴趣的同学可以自行研究一下。
静态内部类

我们再来看一种比双重检测更加简单的实现方法,那就是利用 Java 的静态内部类。它有点类似饿汉式,但又能做到了延迟加载。具体是怎么做到的呢?我们先来看它的代码实现。

SingletonHolder 是一个静态内部类,当外部类 IdGenerator 被加载的时候,并不会创建 SingletonHolder 实例对象。只有当调用 getInstance() 方法时,SingletonHolder 才会被加载,这个时候才会创建 instance。insance 的唯一性、创建过程的线程安全性,都由 JVM 来保证。所以,这种实现方法既保证了线程安全,又能做到延迟加载。

枚举

INSTANCE 是枚举类型的一个实例,确保全局只有一个 IdGenerator 实例。

延迟初始化:枚举实例在类加载时就初始化,因此ID生成器是延迟初始化的。

public enum IdGenerator {
    INSTANCE; // 枚举单例

    private AtomicLong id = new AtomicLong(0); // 声明一个线程安全的原子长整型变量,初始值为0

    public long getId() { // 公共方法,用于获取下一个递增的ID
        return id.incrementAndGet(); // 原子操作,递增并返回当前值
    }
}
  • INSTANCE 是一个枚举实例的名称,它是自定义的。在枚举类型中,每个枚举常量都是该枚举类型的一个实例。因此,INSTANCEIdGenerator 枚举类型的唯一实例

  • 枚举类型在Java中是单例的,每个枚举常量在整个程序运行期间只有一个实例。这意味着无论何时访问枚举常量,都将获得相同的实例。

  • 线程安全:枚举实例的创建是线程安全的,不需要额外的同步机制。

    枚举实例作为类的一部分,其访问受到JMM的保护。
    Java语言规范确保了在任何线程中,类的初始化阶段(即枚举实例的创建)是单线程执行的。这意味着枚举实例的创建在多线程环境中是安全的。

  • 防止反序列化创建新实例:枚举类型的序列化和反序列化机制保证了枚举实例的唯一性,防止了通过反序列化创建新实例的问题。

  • 在这种实现中,INSTANCE 是用来提供对这个单例对象的全局访问点。您可以使用 IdGenerator.INSTANCE 来访问这个枚举实例,并调用它的方法,比如 getId()。每次调用 IdGenerator.INSTANCE.getId() 都会生成一个唯一的递增ID、用处

     

用处

从业务概念上,有些数据在系统中只应该保存一份,就比较适合设计为单例类。比如,系统的配置信息类。除此之外,我们还可以使用单例解决资源访问冲突的问题。

日志类,全局id增加类。
  • 在Spring框架中,ApplicationContext 是单例的,因为它代表了一个应用程序的全局配置和依赖注入。

  • ClassLoader 类负责加载类文件。默认的类加载器是单例的,因为它需要在整个应用程序中保持一致性。

  • Math 类提供了一系列静态方法,用于执行基本的数学运算。由于数学运算是通用的,不需要多个实例,因此 Math 类是单例的。

单例缺点与替代方案

单例对 OOP 特性的支持不友好

  IdGenerator 的使用方式违背了基于接口而非实现的设计原则,也就违背了广义上理解的 OOP 的抽象特性。如果未来某一天,我们希望针对不同的业务采用不同的 ID 生成算法。比如,订单 ID 和用户 ID 采用不同的 ID 生成器来生成。为了应对这个需求变化,我们需要修改所有用到 IdGenerator 类的地方,这样代码的改动就会比较大。

  除此之外,单例对继承、多态特性的支持也不友好。这里我之所以会用“不友好”这个词,而非“完全不支持”,是因为从理论上来讲,单例类也可以被继承、也可以实现多态,只是实现起来会非常奇怪,会导致代码的可读性变差。不明白设计意图的人,看到这样的设计,会觉得莫名其妙。所以,一旦你选择将某个类设计成到单例类,也就意味着放弃了继承和多态这两个强有力的面向对象特性,也就相当于损失了可以应对未来需求变化的扩展性。

单例会隐藏类之间的依赖关系

通过构造函数、参数传递等方式声明的类之间的依赖关系,我们通过查看函数的定义,就能很容易识别出来。但是,单例类不需要显示创建、不需要依赖参数传递,在函数中直接调用就可以了。如果代码比较复杂,这种调用关系就会非常隐蔽。在阅读代码的时候,我们就需要仔细查看每个函数的代码实现,才能知道这个类到底依赖了哪些单例类。

单例对代码的扩展性不友好

  我们知道,单例类只能有一个对象实例。如果未来某一天,我们需要在代码中创建两个实例或多个实例,那就要对代码有比较大的改动。你可能会说,会有这样的需求吗?既然单例类大部分情况下都用来表示全局类,怎么会需要两个或者多个实例呢?

  在系统设计初期,我们觉得系统中只应该有一个数据库连接池,这样能方便我们控制对数据库连接资源的消耗。所以,我们把数据库连接池类设计成了单例类。但之后我们发现,系统中有些 SQL 语句运行得非常慢。这些 SQL 语句在执行的时候,长时间占用数据库连接资源,导致其他 SQL 请求无法响应。为了解决这个问题,我们希望将慢 SQL 与其他 SQL 隔离开来执行。为了实现这样的目的,我们可以在系统中创建两个数据库连接池,慢 SQL 独享一个数据库连接池,其他 SQL 独享另外一个数据库连接池,这样就能避免慢 SQL 影响到其他 SQL 的执行。
  
  如果我们将数据库连接池设计成单例类,显然就无法适应这样的需求变更,也就是说,单例类在某些情况下会影响代码的扩展性、灵活性。所以,数据库连接池、线程池这类的资源池,最好还是不要设计成单例类。实际上,一些开源的数据库连接池、线程池也确实没有设计成单例类。

单例对代码的可测试性不友好

单例不支持有参数的构造方法

为了保证全局唯一,除了使用单例,我们还可以用静态方法来实现。不过,静态方法这种实现思路,并不能解决我们之前提到的问题。如果要完全解决这些问题,我们可能要从根上,寻找其他方式来实现全局唯一类了。比如,通过工厂模式、IOC 容器(比如 Spring IOC 容器)来保证,由过程序员自己来保证(自己在编写代码的时候自己保证不要创建两个类对象)。

集群环境分布式单例
单例模式唯一性
是进程内唯一,进程间不唯一

单例唯一性的作用范围是进程,实际上,对于 Java 语言来说,单例类对象的唯一性的作用范围并非进程,而是类加载器(Class Loader),你能自己研究并解释一下为什么吗?
线程唯一的实例

刚刚我们讲了单例类对象是进程唯一的,一个进程只能有一个单例对象。那如何实现一个线程唯一的单例呢?

尽管概念理解起来比较复杂,但线程唯一单例的代码实现很简单,如下所示。在代码中,我们通过一个 HashMap 来存储对象,其中 key 是线程 ID,value 是对象。这样我们就可以做到,不同的线程对应不同的对象,同一个线程只能对应一个对象。实际上,Java 语言本身提供了 ThreadLocal 工具类,可以更加轻松地实现线程唯一单例。不过,ThreadLocal 底层实现原理也是基于下面代码中所示的 HashMap。

import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;

public class IdGenerator {
    private AtomicLong id = new AtomicLong(0); // 使用AtomicLong确保线程安全的递增操作

    private static final ConcurrentHashMap<Long, IdGenerator> instances = new ConcurrentHashMap<>();

    private IdGenerator() {
        // 私有构造函数,防止外部直接创建实例
    }

    public static IdGenerator getInstance() {
        Long currentThreadId = Thread.currentThread().getId(); // 获取当前线程的ID
        instances.putIfAbsent(currentThreadId, new IdGenerator()); // 如果当前线程ID没有对应的实例,则创建一个并放入映射
        return instances.get(currentThreadId); // 返回当前线程ID对应的IdGenerator实例
    }

    public long getId() {
        return id.incrementAndGet(); // 返回递增后的ID值
    }
}
实现集群环境下单例

集群相当于多个进程构成的一个集合。

进程内唯一,进程间也唯一。不同的进程间共享同一个对象,对能创建同一个类的对各对象。

具体来说,我们需要把这个单例对象序列化并存储到外部共享存储区(比如文件)。进程在使用这个单例对象的时候,需要先从外部共享存储区中将它读取到内存,并反序列化成对象,然后再使用,使用完成之后还需要再存储回外部共享存储区。

为了保证任何时刻,在进程间都只有一份对象存在,一个进程在获取到对象之后,需要对对象加锁,避免其他进程再将其获取。在进程使用完这个对象之后,还需要显式地将对象从内存中删除,并且释放对对象的加锁。

伪代码:

public class IdGenerator {
    private AtomicLong id = new AtomicLong(0);
    private static IdGenerator instance;
    private static SharedobjectStorage storage = FileSharedobjectstorage(/*参数省略*/);
    private static DistributedLock lock = new DistributedLock();

    private IdGenerator() {
    }

    public synchronized static IdGenerator getInstance() {
        if (instance == null) {
            lock.lock();
            instance = storage.load(IdGenerator.class);
        }
        return instance;
    }

    public synchronized void freeInstance() {
        storage.save(this, IdGenerator.class);
        instance = null; // 释放对象
        lock.unlock();
    }

    public long getId() {
        return id.incrementAndGet();
    }

    // IdGenerator使用举例
    IdGenerator idGenerator = IdGenerator.getInstance();
    long id = idGenerator.getId();
    IdGenerator.freeInstance();
}
如何实现多例模式

“单例”指的是,一个类只能创建一个对象。对应地,“多例”指的就是,一个类可以创建多个对象,但是个数是有限制的,比如只能创建 3 个对象。如果用代码来简单示例一下的话,就是下面这个样子:

public class BackendServer {
    private long serverNo;
    private String serverAddress;

    private static final int SERVER_COUNT = 3;
    private static final Map<Long, BackendServer> serverInstances = new HashMap<>();

    static {
        serverInstances.put(1L, new BackendServer(1L, "192.134.22.138:8080"));
        serverInstances.put(2L, new BackendServer(2L, "192.134.22.139:8080"));
        serverInstances.put(3L, new BackendServer(3L, "192.134.22.140:8080"));
    }

    private BackendServer(long serverNo, String serverAddress) {
        this.serverNo = serverNo;
        this.serverAddress = serverAddress;
    }

    public static BackendServer getInstance(long serverNo) {
        return serverInstances.get(serverNo);
    }

    public static BackendServer getRandomInstance() {
        Random r = new Random();
        int no = r.nextInt(SERVER_COUNT) + 1;
        return serverInstances.get(no);
    }
}

实际上,对于多例模式,还有一种理解方式:同一类型的只能创建一个对象,不同类型的可以创建多个对象。这里的“类型”如何理解呢?

我们还是通过一个例子来解释一下,具体代码如下所示。在代码中,logger name 就是刚刚说的“类型”,同一个 logger name 获取到的对象实例是相同的,不同的 logger name 获取到的对象实例是不同的。

import java.util.concurrent.ConcurrentHashMap;

public class Logger {
    private static final ConcurrentHashMap<String, Logger> instances = new ConcurrentHashMap<>();

    private Logger() {
    }

    public static Logger getInstance(String loggerName) {
        instances.putIfAbsent(loggerName, new Logger());
        return instances.get(loggerName);
    }

    public void log() {
        // log method implementation (not provided in the original content)
    }

    // Example usage
    Logger li = Logger.getInstance("User.class");
    Logger l2 = Logger.getInstance("User.class");
    Logger l3 = Logger.getInstance("order.class");
}
工厂模式

工厂模式(Factory Design Pattern)

封装对象的创建过程,将对象的创建和使用相分离。

一般情况下,工厂模式分为三种更加细分的类型:简单工厂、工厂方法和抽象工厂。不过,在 GoF 的《设计模式》一书中,它将简单工厂模式看作是工厂方法模式的一种特例,所以工厂模式只被分成了工厂方法和抽象工厂两类。实际上,前面一种分类方法更加常见,所以,在今天的讲解中,我们沿用第一种分类方法。

简单工厂、工厂方法原理比较简单,在实际的项目中也比较常用。而抽象工厂的原理稍微复杂点,在实际的项目中相对也不常用。所以,我们今天讲解的重点是前两种工厂模式。对于抽象工厂,你稍微了解一下即可。

简单工厂

简单工厂(Simple Factory)

使用某个实例时,不在使用类中创建,而在工厂类中创建。

使用时,调用工厂的某个方法, new 实例的过程在这个工厂中。

大部分工厂类都是以“Factory”这个单词结尾的,但也不是必须的,比如 Java 中的 DateFormat、Calender。除此之外,工厂类中创建对象的方法一般都是 create 开头,比如代码中的 createParser(),但有的也命名为 getInstance()、createInstance()、newInstance(),有的甚至命名为 valueOf()(比如 Java String 类的 valueOf() 函数)等等,这个我们根据具体的场景和习惯来命名就好。

简单工厂第一中实现方式, 用new .

image-20240719022251008

简单工厂第二中实现方式,类似结合单例模式 ,如果可以复用,可以将实例事先创建好缓存起来,调用获得实例的方法时,从缓存中取出直接使用。

image-20240719022203513

总结一下,尽管简单工厂模式的代码实现中,有多处 if 分支判断逻辑,违背开闭原则,但权衡扩展性和可读性,这样的代码实现在大多数情况下(比如,不需要频繁地添加 parser,也没有太多的 parser)是没有问题的。

工厂方法

工厂方法(Factory Method)

如果非要将上面的if分支逻辑去掉呢 利用多态:

image-20240719023201863

实际上,这就是工厂方法模式的典型代码实现。这样当我们新增一种 parser 的时候,只需要新增一个实现了 IRuleConfigParserFactory 接口的 Factory 类即可。所以,工厂方法模式比起简单工厂模式更加符合开闭原则。

这种方法使用时,又耦合进了load方法中,与最初使用工厂前非常相似:没解决问题,反而设计变的复杂。

image-20240719023424126

解决方法,接着给这些工厂类创建一个工厂

image-20240719024504027

使用时也变得简单:

image-20240719024532013

当我们需要添加新的规则配置解析器的时候,我们只需要创建新的 parser 类和 parser factory 类,并且在 RuleConfigParserFactoryMap 类中,将新的 parser factory 对象添加到 cachedFactories 中即可。代码的改动非常少,基本上符合开闭原则。

实际上,对于规则配置文件解析这个应用场景来说,工厂模式需要额外创建诸多 Factory 类,也会增加代码的复杂性,而且,每个 Factory 类只是做简单的 new 操作,功能非常单薄(只有一行代码),也没必要设计成独立的类,所以,在这个应用场景下,简单工厂模式简单好用,比工方法厂模式更加合适。

那什么时候该用工厂方法模式,而非简单工厂模式呢?

之所以将某个代码块剥离出来,原因是这个代码块的逻辑过于复杂,但是,如果代码块本身并不复杂,几行代码而已,我们完全没必要将它拆分成单独的函数或者类。

基于这个设计思想,当对象的创建逻辑比较复杂,不只是简单的 new 一下就可以,而是要组合其他类对象,做各种初始化操作的时候,我们推荐使用工厂方法模式,将复杂的创建逻辑拆分到多个工厂类中,让每个工厂类都不至于过于复杂。而使用简单工厂模式,将所有的创建逻辑都放到一个工厂类中,会导致这个工厂类变得很复杂。

除此之外,在某些场景下,如果对象不可复用,那工厂类每次都要返回不同的对象。如果我们使用简单工厂模式来实现,就只能选择第一种包含 if 分支逻辑的实现方式。如果我们还想避免烦人的 if-else 分支逻辑,这个时候,我们就推荐使用工厂方法模式。

抽象工厂

抽象工厂(Abstract Factory)

抽象工厂模式的应用场景比较特殊,没有前两种常用,你简单了解一下就可以了。

假设之前的例子中,解析器不仅根据格式分,还根据配置规则分,每种规则下都有这些格式,相当于多了一个维度...

我们可以让一个工厂负责创建多个不同类型的对象(IRuleConfigParser、ISystemConfigParser 等),而不是只创建一种 parser 对象。这样就可以有效地减少工厂类的个数。具体的代码实现如下所示:

image-20240719025933999

抽象哦工厂应用场景特殊,很少用到。

总结

第一种情况:类似规则配置解析的例子,代码中存在 if-else 分支判断,动态地根据不同的类型创建不同的对象。针对这种情况,我们就考虑使用工厂模式,将这一大坨 if-else 创建对象的代码抽离出来,放到工厂类中。

还有一种情况,尽管我们不需要根据不同的类型创建不同的对象,但是,单个对象本身的创建过程比较复杂,比如前面提到的要组合其他类对象,做各种初始化操作。在这种情况下,我们也可以考虑使用工厂模式,将对象的创建过程封装到工厂类中。

当每个对象的创建逻辑都比较简单的时候,我推荐使用简单工厂模式,将多个对象的创建逻辑放到一个工厂类中。

当每个对象的创建逻辑都比较复杂的时候,为了避免设计一个过于庞大的简单工厂类,我推荐使用工厂方法模式,将创建逻辑拆分得更细,每个对象的创建逻辑独立到各自的工厂类中。

如果创建对象的逻辑并不复杂,那我们就直接通过 new 来创建对象就可以了,不需要使用工厂模式。

需要用自己的语言,精简。

image-20240719030612996

工厂模式是一种非常常用的设计模式,在很多开源项目、工具类中到处可见,比如 Java 中的 Calendar、DateFormat 类。除此之外,你还知道哪些用工厂模式实现类?可以留言说一说它们为什么要设计成工厂模式类?

实际上,简单工厂模式还叫作静态工厂方法模式(Static Factory Method Pattern)。之所以叫静态工厂方法模式,是因为其中创建对象的方法是静态的。那为什么要设置成静态的呢?设置成静态的,在使用的时候,是否会影响到代码的可测试性呢?
工厂模式实现DI

如何设计实现一个Dependency Injection框架?

建造者模式

Builder模式,建造者或者构建者模式,也有人叫它生成器模式。

假设有这样一个类,资源池:

image-20240719041904184

image-20240719041919573

现在,ResourcePoolConfig 只有 4 个可配置项,对应到构造函数中,也只有 4 个参数,参数的个数不多。但是,如果可配置项逐渐增多,变成了 8 个、10 个,甚至更多,那继续沿用现在的设计思路,构造函数的参数列表会变得很长,代码在可读性和易用性上都会变差。在使用构造函数的时候,我们就容易搞错各参数的顺序,传递进错误的参数值,导致非常隐蔽的 bug。

解决这个问题的办法你应该也已经想到了,那就是用 set() 函数来给成员变量赋值,以替代冗长的构造函数。我们直接看代码,具体如下所示。其中,配置项 name 是必填的,所以我们把它放到构造函数中设置,强制创建类对象的时候就要填写。其他配置项 maxTotal、maxIdle、minIdle 都不是必填的,所以我们通过 set() 函数来设置,让使用者自主选择填写或者不填写。

image-20240719042332077

接下来,我们来看新的 ResourcePoolConfig 类该如何使用。我写了一个示例代码,如下所示。没有了冗长的函数调用和参数列表,代码在可读性和易用性上提高了很多。

image-20240719042407273

增加要求

我们刚刚讲到,name 是必填的,所以,我们把它放到构造函数中,强制创建对象的时候就设置。如果必填的配置项有很多,把这些必填配置项都放到构造函数中设置,那构造函数就又会出现参数列表很长的问题。如果我们把必填项也通过 set() 方法设置,那校验这些必填项是否已经填写的逻辑就无处安放了。

除此之外,假设配置项之间有一定的依赖关系,比如,如果用户设置了 maxTotal、maxIdle、minIdle 其中一个,就必须显式地设置另外两个;或者配置项之间有一定的约束条件,比如,maxIdle 和 minIdle 要小于等于 maxTotal。如果我们继续使用现在的设计思路,那这些配置项之间的依赖关系或者约束条件的校验逻辑就无处安放了。


如果我们希望 ResourcePoolConfig 类对象是不可变对象,也就是说,对象在创建好之后,就不能再修改内部的属性值。要实现这个功能,我们就不能在 ResourcePoolConfig 类中暴露 set() 方法。

为了解决这些问题,建造者模式就派上用场了。

我们可以把校验逻辑放置到 Builder 类中,先创建建造者,并且通过 set() 方法设置建造者的变量值,然后在使用 build() 方法真正创建对象之前,做集中的校验,校验通过之后才会创建对象。除此之外,我们把 ResourcePoolConfig 的构造函数改为 private 私有权限。这样我们就只能通过建造者来创建 ResourcePoolConfig 类对象。并且,ResourcePoolConfig 没有提供任何 set() 方法,这样我们创建出来的对象就是不可变对象了。

仔细看:

image-20240719043004810

image-20240719043031512

实际上,使用建造者模式创建对象,还能避免对象存在无效状态。我再举个例子解释一下。比如我们定义了一个长方形类,如果不使用建造者模式,采用先创建后 set 的方式,那就会导致在第一个 set 之后,对象处于无效状态。具体代码如下所示:

image-20240719043230383

为了避免这种无效状态的存在,我们就需要使用构造函数一次性初始化好所有的成员变量。如果构造函数参数过多,我们就需要考虑使用建造者模式,先设置建造者的变量,然后再一次性地创建对象,让对象一直处于有效状态。

实际上,如果我们并不是很关心对象是否有短暂的无效状态,也不是太在意对象是否是可变的。比如,对象只是用来映射数据库读出来的数据,那我们直接暴露 set() 方法来设置类的成员变量值是完全没问题的。而且,使用建造者模式来构建对象,代码实际上是有点重复的,ResourcePoolConfig 类中的成员变量,要在 Builder 类中重新再定义一遍。

与工厂模式有何区别

实际上,工厂模式是用来创建不同但是相关类型的对象(继承同一父类或者接口的一组子类),由给定的参数来决定创建哪种类型的对象。建造者模式是用来创建一种类型的复杂对象,通过设置不同的可选参数,“定制化”地创建不同的对象。

回顾一下,内容基本是重复的:

image-20240719043801130

思考题

在下面的 ConstructorArg 类中,当 isRef 为 true 的时候,arg 表示 String 类型的 refBeanId,type 不需要设置;当 isRef 为 false 的时候,arg、type 都需要设置。请根据这个需求,完善 ConstructorArg 类。

image-20240719044008556

原型模式

如果对象的创建成本比较大,而同一个类的不同对象之间差别不大(大部分字段都相同),在这种情况下,我们可以利用对已有对象(原型)进行复制(或者叫拷贝)的方式,来创建新对象,以达到节省创建时间的目的。这种基于原型来创建对象的方式就叫作原型设计模式,简称原型模式。

如果要拷贝的对象是不可变对象,浅拷贝共享不可变对象是没问题的,但对于可变对象来说,浅拷贝得到的对象和原始对象会共享部分数据,就有可能出现数据被修改的风险,也就变得复杂多了。除非像我们今天实战中举的那个例子,需要从数据库中加载 10 万条数据并构建散列表索引,操作非常耗时,比较推荐使用浅拷贝,否则,没有充分的理由,不要为了一点点的性能提升而使用浅拷贝。

结构型

创建型模式主要解决对象的创建问题,封装复杂的创建过程,解耦对象的创建代码和使用代码。

结构型模式主要总结了一些类或对象组合在一起的经典结构,这些经典的结构可以解决特定应用场景的问题。

代理模式

(Proxy Design Pattern)它在不改变原始类(或叫被代理类)代码的情况下,通过引入代理类来给原始类附加功能。

代理类与原始类实现相同接口,

原始类只负责业务功能,代理类负责在业务代码执行前后附加其他逻辑代码,并通过委托的方式调用原始类来执行业务代码。

原始类和代理类实现相同的接口,是基于接口而非实现编程。

如果原始类并没有定义接口,或者并不是我们开发维护的,需要让代理类继承原始类,然后扩展附加功能。

如果有 50 个要添加附加功能的原始类,那我们就要创建 50 个对应的代理类。这会导致项目中类的个数成倍增加,增加了代码维护成本。并且,每个代理类中的代码都有点像模板式的“重复”代码,也增加了不必要的开发成本。那这个问题怎么解决呢?

动态代理Dynamic Proxy 不实现为每个原始类编写代理类,运行时动态地创建原始类对应的代理类。在系统中用代理类替换掉原始类。

动态代理底层依赖的就是Java反射语法。

实际上,Spring AOP 底层的实现原理就是基于动态代理。用户配置好需要给哪些类创建代理,并定义好在执行原始类的业务代码前后执行哪些附加功能。Spring 为这些类创建动态代理对象,并在 JVM 中替代原始类对象。原本在代码中执行的原始类的方法,被换作执行代理类的方法,也就实现了给原始类添加附加功能的目的。

应用

代理模式常用在业务系统中开发一些非功能性需求,比如:监控、统计、鉴权、限流、事务、幂等、日志。我们将这些附加功能与业务功能解耦,放到代理类统一处理,让程序员只需要关注业务方面的开发。除此之外,代理模式还可以用在 RPC、缓存等应用场景中。

假设我们要开发一个接口请求的缓存功能,对于某些接口请求,如果入参相同,在设定的过期时间内,直接返回缓存结果,而不用重新进行逻辑处理。比如,针对获取用户个人信息的需求,我们可以开发两个接口,一个支持缓存,一个支持实时查询。对于需要实时数据的需求,我们让其调用实时查询接口,对于不需要实时数据的需求,我们让其调用支持缓存的接口。那如何来实现接口请求的缓存功能呢?

最简单的实现方法就是刚刚我们讲到的,给每个需要支持缓存的查询需求都开发两个不同的接口,一个支持缓存,一个支持实时查询。但是,这样做显然增加了开发成本,而且会让代码看起来非常臃肿(接口个数成倍增加),也不方便缓存接口的集中管理(增加、删除缓存接口)、集中配置(比如配置每个接口缓存过期时间)。

针对这些问题,代理模式就能派上用场了,确切地说,应该是动态代理。如果是基于 Spring 框架来开发的话,那就可以在 AOP 切面中完成接口缓存的功能。在应用启动的时候,我们从配置文件中加载需要支持缓存的接口,以及相应的缓存策略(比如过期时间)等。当请求到来的时候,我们在 AOP 切面中拦截请求,如果请求中带有支持缓存的字段(比如 http://…?..&cached=true),我们便从缓存(内存缓存或者 Redis 缓存等)中获取数据直接返回。

桥接模式

实现简单,理解有难度,应用场景局限,实际项目不常用,简单了解,见到能认识,不是学习重点。

将抽象部分与它的实现部分分离,使他们都可以独立地变化。

继承机制将抽象部分与它的实现部分固定在一起,使得难以对抽象部分和实现部分独立地进行修改、扩充和重用。

抽象类型修改,继承类型要跟着修改。

如果去引用某个类作为自己的实例字段,那他们可以独立修改,互不影响。

比如很多if else逻辑。 根据日志的不同等级,判断发语言,发微信,发邮件。

现在将发语音,微信,发邮件全部独立出来,变为类型。

使用他们的类中有个字段,是对它们的接口的引用。

要调用时,使用它们的类在构造方法中传一个它们其中任意一个实例即可使用。

帮助理解:类似(只是类似)我们之前讲过的“组合优于继承”设计原则,通过组合关系来替代继承关系,避免继承层次的指数级爆炸。

装饰器模式

动态地给一个对象添加一些额外的职责。就增加功能来说, D e c o r a t o r模式相比生成子类更为灵活。

如果 InputStream 只有一个子类 FileInputStream 的话,那我们在 FileInputStream 基础之上,再设计一个孙子类 BufferedFileInputStream,也算是可以接受的,毕竟继承结构还算简单。但实际上,继承 InputStream 的子类有很多。我们需要给每一个 InputStream 的子类,再继续派生支持缓存读取的子类。

在这种情况下,如果我们继续按照继承的方式来实现的话,就需要再继续派生出 DataFileInputStream、DataPipedInputStream 等类。如果我们还需要既支持缓存、又支持按照基本类型读取数据的类,那就要再继续派生出 BufferedDataFileInputStream、BufferedDataPipedInputStream 等 n 多类。这还只是附加了两个增强功能,如果我们需要附加更多的增强功能,那就会导致组合爆炸,类继承结构变得无比复杂,代码既不好扩展,也不好维护。

使用组合

image-20240720235607440

装饰器模式相对于组合还有两个比较特殊的地方

1.装饰器类与原始类继承同样的父类,由于它们都是同样的类型,可以对原始类“嵌套”多个装饰器类

image-20240720235734535

2.装饰器类是对功能的增强,这也是装饰器模式应用场景的一个重要特点。

代理模式和装饰器模式相似,代理模式中,代理类附加的是跟原始类无关的功能,而在装饰器模式中,装饰器类附加的时跟原始类相关的功能。

image-20240721001144456

装饰器模式主要解决继承关系过于复杂的问题,通过组合来替代继承。它主要的作用是给原始类添加增强功能。这也是判断是否该用装饰器模式的一个重要的依据。除此之外,装饰器模式还有一个特点,那就是可以对原始类嵌套使用多个装饰器。为了满足这个应用场景,在设计的时候,装饰器类需要跟原始类继承相同的抽象类或者接口。

Decorator关注为对象动态的添加功能, Proxy关注对象的信息隐藏及访问控制. Decorator体现多态性, Proxy体现封装性.

适配器模式

Adapter Design Pattern

顾名思义,这个模式就是用来做适配的,它将不兼容的接口转换为可兼容的接口,让原本由于接口不兼容而不能一起工作的类可以一起工作。对于这个模式,有一个经常被拿来解释它的例子,就是 USB 转接头充当适配器,把两种不兼容的接口,通过转接变得可以一起工作。

适配器模式有两种实现方式:类适配器和对象适配器。其中,类适配器使用继承关系来实现,对象适配器使用组合关系来实现。

具体的代码实现如下所示。其中,ITarget 表示要转化成的接口定义。Adaptee 是一组不兼容 ITarget 接口定义的接口,Adaptor 将 Adaptee 转化成一组符合 ITarget 接口定义的接口。

类适配器:适配器继承要被改造的类,同时实现该提供规则的接口

image-20240721022030163

对象适配器:适配器仅实现提供规则的接口,同时引用一个要被改造的类型作为实例字段。

image-20240721022051808

如果 Adaptee 接口并不多,那两种实现方式都可以。

如果 Adaptee 接口很多,而且 Adaptee 和 ITarget 接口定义大部分都相同,那我们推荐使用类适配器,因为 Adaptor 复用父类 Adaptee 的接口,比起对象适配器的实现方式,Adaptor 的代码量要少一些。

如果 Adaptee 接口很多,而且 Adaptee 和 ITarget 接口定义大部分都不相同,那我们推荐使用对象适配器,因为组合结构相对于继承更加灵活。

重磅应用 Slf4j

应用

image-20240721023724188

4 种设计模式的区别

image-20240721023733511

(外观)门面模式

外观模式(Facade Pattern)也叫门面模式,是一种结构型设计模式,它的定义就是为子系统提供一组统一接口,隐藏内部的实现细节,方便子系统使用。

举个例子就清晰了。例如 A 系统有 a、b、c 三个接口,B 需要分别调用 a、b、c 三个接口,这样比较麻烦,所以 A 将 a、b、c 封装成一个接口给 B 调用,对 B 来说使用起来就方便了,这就是外观模式(门面模式)。

组合模式

将对象组合成树形结构以表示“部分 -整体”的层次结构。C o m p o s i t e使得用户对单个对象

和组合对象的使用具有一致性。

它必须体现嵌套,

典型的是EXt树,嵌套就是地区下面有子地区,最末尾才是叶子(学校)节点。

还有,目录下有子目录,最下层才是文件。

部门下有子部门,最下层才是人员。

这就需要类中有List 。可能装的是

享元模式

共享减少内存占用,提高内存效率。

意图共享不可变的对象。

在 Integer 中就采用了享元模式,即 integer 中缓冲池。

很多人都遇到过一个面试题,即 Integer -128 到 127 之内的相等,而超过这个范围用 == 就不对了,因为这个范围内采用了享元模式,本质就是同一个对象,所以用 == 判断当然一直了。

行为型

观察者模式

观察者模式其实也称为发布订阅模式,它定义了对象之间的一种一对多的依赖关系,让多个观察者对象同时监听某一个主题对象。当主题对象状态发生变化时,它会通知所有观察者对象。

它的目的就是将观察者和被观察者代码解耦,使得一个对象或者说事件的变更,让不同观察者可以有不同的处理,非常灵活,扩展性很强,是事件驱动编程的基础。

新闻发布这 有一个List 发布新闻时只发布给list中有的观察者。

订报纸的,新闻网站等。

与生产者消费者的区别,生产者消费者一般是异步,而且消费者之间有竞争关系。

模板模式

模板方法模式,它在一个抽象类中定义了一个算法(业务逻辑)的骨架,具体步骤的实现由子类提供,它通过将算法的不变部分放在抽象类中,可变部分放在子类中,达到代码复用和扩展的目的。

复用:所有子类可以直接复用父类提供的模板方法,即上面提到的不变的部分。 扩展:子类可以通过模板定义的一些扩展点就行不同的定制化实现。

子类只需要关心几个方法的内部实现,不需要关心模板方法的内部执行顺序,这就是我们所说的将通用的算法步骤放在抽象类中,不同的实现细节放在子类中,在多个子类中共享相同的代码,而不需要在每个子类中重复实现相同的逻辑。

在 Java 中有很多应用场景,例如 JdbcTemplate 就是使用了模板方法来处理数据库的操作。

再比如 HttpServlet 类的 service 方法也用了模板方法,doGet、doPost 等方法都是需要子类实现的。

模板方法模式在一个方法中定义一个算法骨架,并将某些步骤推迟到子类中实现。模板方法模式可以让子类在不改变算法整体结构的情况下,重新定义算法中的某些步骤。这里的“算法”,我们可以理解为广义上的“业务逻辑”,并不特指数据结构和算法中的“算法”。这里的算法骨架就是“模板”,包含算法骨架的方法就是“模板方法”,这也是模板方法模式名字的由来。

在模板模式经典的实现中,模板方法定义为 final,可以避免被子类重写。需要子类重写的方法定义为 abstract,可以强迫子类去实现。不过,在实际项目开发中,模板模式的实现比较灵活,以上两点都不是必须的。

模板模式有两大作用:复用和扩展。其中,复用指的是,所有的子类可以复用父类中提供的模板方法的代码。扩展指的是,框架通过模板模式提供功能扩展点,让框架用户可以在不修改框架源码的情况下,基于扩展点定制化框架的功能。

策略模式

定义一系列的算法,把它们一个个封装起来, 并且使它们可相互替换。本模式使得算法可独立于使用它的客户而变化。

它的主要目的是为了解耦多个策略,并方便调用方在针对不同场景灵活切换不同的策略。

从策略的定义,创建,使用上,三部分解耦策略。

不同策略实现同一个接口,实现具体的算法。

策略在策略工厂中,根据不同的type,放进工厂类中的Map中

使用时,根据type将策略取出使用。

可以避免 大量 if-else分支判断。拿到type去策略替代判断type现写策略。

除此之外,我们还可以通过策略模式来移除 if-else 分支判断。实际上,这得益于策略工厂类,更本质上点讲,是借助“查表法”,根据 type 查表替代根据 type 分支判断。
职责链模式

使多个对象都有机会处理请求,从而避免请求的发送者和接收者之间的耦合关系。将这些对象连成一条链,并沿着这条链传递该请求,直到有一个对象处理它为止。

增强了系统的灵活性和可扩展性。避免请求发送者与接收者之间的耦合关系。

在职责链模式中,多个处理器依次处理同一个请求。一个请求先经过 A 处理器处理,然后再把请求传递给 B 处理器,B 处理器处理完后再传递给 C 处理器,以此类推,形成一个链条。链条上的每个处理器各自承担各自的处理职责,所以叫作职责链模式。

在 GoF 的定义中,一旦某个处理器能处理这个请求,就不会继续将请求传递给后续的处理器了。当然,在实际的开发中,也存在对这个模式的变体,那就是请求不会中途终止传递,而是会被所有的处理器都处理一遍。

很多场景都能看到,日志处理,不同级别不同输出,Spring拦截器的Chain也是责任链模式。

比如请假天数少于 3 天的请求由部门经理审批,3 到 7 天的请求由总经理审批,超过 7 天的请求由 CEO 审批。

代码最大的特点,就是接口要引用一个与自己本身相同类型的字段,用来接收下一个类,可用来传递任务,调用这个字段引用的类型去继续执行。

状态模式

允许一个对象在其内部状态改变时改变它的行为。对象看起来似乎修改了它的类。

状态模式它允许对象在其内部状态发生改变时改变其行为,将状态的行为封装在独立的类中,这样就可以在状态改变时切换状态对象,从而改变对象的行为。

状态模式是状态机的实现方式之一。

一般预先定义一个接口,接口中定义一些时间。

然后各种状态继承这个接口,状态类实现接口并且组合状态机。

有一个状态机类,有状态装一与动作执行的代码逻辑,组合状态类型。

又是相互组合

在我们平日的业务场景中,订单的处理就是一个状态机,订单的创建、支付、发货、退款、关闭等等,使用状态模式可以更好地管理订单的状态转换和对应的行为。

再比如游戏中的一些行为,例如站立、行走、奔跑、跳跃等等,不同状态的角色需要有不同的行为和动画,也可以利用状态模式实现。

通过使用状态模式,可以将状态相关的行为局部化到不同的状态类中,减少上下文类的复杂性,使代码更清晰、易维护和扩展。

迭代器模式

提供一种方法顺序访问一个聚合(集合/容器)对象中各个元素 , 而又不需暴露该对象的内部表示。

迭代器模式(Iterator Design Pattern),也叫作游标模式(Cursor Design Pattern)。

“集合对象”也可以叫“容器”“聚合对象”,实际上就是包含一组对象的对象,比如数组、链表、树、图、跳表。

迭代器模式将集合对象的遍历操作从集合类中拆分出来,放到迭代器类中,让两者的职责更加单一。

结合刚刚的例子,我们来总结一下迭代器的设计思路。

迭代器中需要定义 hasNext()、currentItem()、next() 三个最基本的方法。待遍历的容器对象通过依赖注入传递到迭代器类中。容器通过 iterator() 方法来创建迭代器。

image-20240721232022365

迭代器中也组合了容器类型:

image-20240721233156222

迭代器模式的优势

对于类似数组和链表这样的数据结构,遍历方式比较简单,直接使用 for 循环来遍历就足够了。但是,对于复杂的数据结构(比如树、图)来说,有各种复杂的遍历方式。比如,树有前中后序、按层遍历,图有深度优先、广度优先遍历等等。如果由客户端代码来实现这些遍历算法,势必增加开发成本,而且容易写错。如果将这部分遍历的逻辑写到容器类中,也会导致容器类代码的复杂性。

前面也多次提到,应对复杂性的方法就是拆分。我们可以将遍历操作拆分到迭代器类中。比如,针对图的遍历,我们就可以定义 DFSIterator、BFSIterator 两个迭代器类,让它们分别来实现深度优先遍历和广度优先遍历。

其次,将游标指向的当前位置等信息,存储在迭代器类中,每个迭代器独享游标信息。这样,我们就可以创建多个不同的迭代器,同时对同一个容器进行遍历而互不影响

最后,容器和迭代器都提供了抽象的接口,方便我们在开发的时候,基于接口而非具体的实现编程。当需要切换新的遍历算法的时候,比如,从前往后遍历链表切换成从后往前遍历链表,客户端代码只需要将迭代器类从 LinkedIterator 切换为 ReversedLinkedIterator 即可,其他代码都不需要修改。除此之外,添加新的遍历算法,我们只需要扩展新的迭代器类,也更符合开闭原则。

访问者模式

允许一个或者多个操作应用到一组对象上,解耦操作和对象本身。

这真不是组合吗?
备忘录模式

备忘录模式指的是在不违背封装原则的前提下,捕获对象内部的状态,将其保存在外部,便于后面对象恢复之前的状态,使得系统更具灵活性和可维护性。

想恢复到某个状态是不利用set方法修改内部状态,因为set是一个常被其他业务使用修改的方法,而是利用这个restore方法。

Memento就是存储状态的备忘录。

而是给外面创建一个备份:

image-20240722032930791

命令模式

落实到编码实现,命令模式用到最核心的实现手段,就是将函数封装成对象。我们知道,在大部分编程语言中,函数是没法作为参数传递给其他函数的,也没法赋值给变量。借助命令模式,我们将函数封装成对象,这样就可以实现把函数像对象一样使用。

命令模式(Command Pattern)是一种行为设计模式,它将请求封装成对象,从而使得请求参数化,便于对请求排队或记录请求日志,以及支持可撤销的操作。

比如可以将每次请求开关灯的 command 存储在 list 中,最后遍历这个 list 执行命令,这样请求就排队了,同时也能记录日志,并且清空 list 的 command 就是所谓的支持撤回了。

请求,函数,就是指的动作,某些操作,封装到对象里。

解释器模式

翻译成中文就是:解释器模式为某个语言定义它的语法(或者叫文法)表示,并定义一个解释器用来处理这个语法。

实现根据语法规则解读句子的解释器。

比如加减乘除运算解释器,java解释器。

核心思想 ,讲语法解析的工作拆分到各个小类综。避免大而全的解析类。

语法规则拆分成一些小的独立单元,对每个单元进行解析,最终合并为对整个语法规则的解析。

中介模式

中介模式通过引入了一个中介对象,来封装一组对象之间的交互,来避免对象之间的直接交互。通过引入一个中介者对象,使对象之间的关系变得简单且易于维护。

航班调度,交易所。不能p2p而是 找中介。

设计原则

单一职责

单一职责原则是面向对象设计的五大原则之一(SOLID)中的 S,即(Single Responsibility Principle,SRP)。

它的核心思想:一个类或者模块只负责完成一个职责。

image-20240722045331684

开闭原则

开闭原则是面向对象设计的五大原则之一(SOLID)中的 O,即(Open/Closed Principle,OCP)。

对扩展开放、对修改关闭

比如我们需要新增一个功能,这时需要修改代码,如果实现这个功能是通过新增模块、方法、类的方式,而不是修改代码中已有的模块、方法和类,这就是所谓的对扩展开放,对修改关闭。

有圆形、矩形类。

有一个计算类分别有计算圆形、矩形面积的方法,

这时候新增三角形,还要增加三角形类,再往计算类中新增加一个三角形面积计算方法。

改动了计算类,就是违反开闭原则。

定义一个图形接口,接口中有计算方法,所有图形去实现图形接口。

计算类计算式,传入图形类型,多态调用该方法即可。

里氏替换

里氏替换原则是面向对象设计的五大原则之一(SOLID)中的 L,即(Liskov Substitution Principle,LSP)。

子类对象可以替换父类出现的任何地方,并且保证原来的程序逻辑正常且不被改变

子类需要继承父类设计的初衷,不能违背约定

在网上看过一个比较通俗的解释,供大家参考:里氏替换就是说父亲能干的事儿子也别挑,该怎么干就怎么干,儿子可以比父亲更有能力,但传统不能变。

比如父类是矩形,长款方法知识设置长宽,但是正方形,重写设置长时同样设置宽等于长。

矩形成立,长方形不成立:

image-20240722051108547

接口隔离原则

接口隔离原则是面向对象设计的五大原则之一(SOLID)中的 I,即(Interface Segregation Principle, ISP)。

客户端(指调用者或者使用者)不应该被迫依赖它不使用的方法,即一个类对另一个类的依赖应该建立在最小接口上

如果你定义了一个大而全的接口,那么就是强迫所有实现类都实现这些接口,要求使用者被动的依赖这部分接口。这就太不优雅了,如果调用者仅需用到部分接口,我们就需要隔离这部分接口,将一个接口拆分成多个细粒度的接口。

image-20240722052238460

正确的做法是将接口拆分成多个,让不同类型的打印机仅需实现自己需要的接口即可

定义精简、具体的接口,避免臃肿的接口和不必要的依赖,所以它的角度是从调用者是否需要这个角度出发的,而不是通过职责单一的角度去考虑。

依赖倒置原则

依赖倒置(反转)原则是面向对象设计的五大原则之一(SOLID)中的 D,即(Dependency Inversion Principle, DIP)。

高层模块不应该依赖低层模块。二者都应该依赖其抽象。抽象不应该依赖细节。细节应该依赖抽象。

image-20240722054025586

image-20240722054107962

image-20240722054129945

迪米特法则

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值