跳转至

设计模式

1. 你知道那些设计模式呢?⭐

总体来说设计模式分为三大类 :

  • 创造型模式 : 解决“对象的创建”问题。常用的有单例模式、工厂方法模式、抽象工厂模式、建造者模式(Builder)。

  • 结构型模型 : 解决"类或对象的组合”问题。适配器模式、装饰器模式、代理模式、外观模式、桥接模式、组合模s式、享元模式。

  • 行为型模式 : 解决“对象之间的通信”问题。它定义了对象在运行时如何交互,使复杂的逻辑流程变得清晰、可维护。策略模式(消除 if-else)、观察者模式、模板方法模式。

2. 简单介绍一下单例模式, 以及它的优缺点⭐

单例模式是创建型模式中最简单也最常用的一种。它的核心目标是:保证一个类在整个系统中只有一个实例,并提供一个全局访问点。

优点

  • 资源节约:由于整个系统只存在一个实例,减少了频繁创建和销毁对象带来的内存消耗和 CPU 开销。

  • 全局访问:提供了一个统一的访问入口,方便对全局状态或配置进行管理(如数据库连接池、读取配置文件)。

  • 避免冲突:对于某些必须具有唯一性的资源(如打印机句柄、序列号生成器),可以有效防止多个实例导致的资源争用。

缺点

  • 扩展困难:单例模式通常没有接口,且构造方法私有化,这导致它很难通过继承来扩展。
  • 违背单一职责原则:单例类既要负责自己的业务逻辑,还要负责“实例的创建与自我管理”,职责较重。
  • 对测试不友好:在单元测试中,由于单例是全局的,很难模拟出一个“干净”的初始状态,可能会导致测试用例之间产生相互影响。
  • 垃圾回收风险:如果单例持有长生命周期的对象(如 Context),容易造成内存泄漏;同时,如果长时间不使用,某些框架可能会将其回收,导致状态丢失。

3. 为什么 DCL 需要用 volatile 关键字呢? ⭐

“因为 instance = new Singleton(); 并不是一个原子操作,它分为:分配空间、初始化对象、引用指向空间这三步。如果不加 volatile,由于 JVM 的指令重排,可能会先执行引用指向空间。此时另一个线程进来执行第一重 null 检查,发现引用不为空,直接拿走了一个还没初始化完成的半成品对象,从而导致程序崩溃。”

4. 单例模式的实现方式

饿汉式(简单,但费内存)

类加载时就创建实例。虽然浪费点内存,但没有锁,执行最快,天生线程安全。

public class Singleton {
    // 类加载时就初始化,由于是 static,JVM 保证线程安全
    private static final Singleton INSTANCE = new Singleton();

    private Singleton() {}

    public static Singleton getInstance() {
        return INSTANCE;
    }
}

懒汉式

“临阵磨枪”。只有调用时才创建。为了安全在方法上加了 synchronized 大锁,导致并发性能极差,不推荐。

public class Singleton {
    private static Singleton instance;

    private Singleton() {}

    // 给整个方法加锁,性能太低,不推荐
    public static synchronized Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

DCL⭐⭐⭐

“按需取货+精准锁”。利用 volatile 和双重判断,既实现了延迟加载,又保证了高并发下的高性能,是面试最标准的写法。

public class Singleton {
    // 1. volatile 关键字:禁止指令重排,确保多线程下变量的可见性
    private static volatile Singleton instance;

    // 2. 私有构造方法:防止外部通过 new 创建实例
    private Singleton() {}

    public static Singleton getInstance() {
        // 3. 第一重检查:提高效率,如果已经创建了就不用再进入同步块
        if (instance == null) {
            synchronized (Singleton.class) {
                // 4. 第二重检查:防止多个线程同时通过了第一重检查后重复创建
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}
静态内部类方法⭐⭐

静态内部类“优雅托付”。利用 JVM 加载机制,只有第一次调用方法时才会加载内部类并创建实例,代码最简洁,推荐在项目中使用。

public class Singleton {
    private Singleton() {}

    // 静态内部类只有在被 getInstance() 调用时才会被加载
    private static class Holder {
        private static final Singleton INSTANCE = new Singleton();
    }

    public static Singleton getInstance() {
        return Holder.INSTANCE;
    }
}
枚举⭐⭐

天生防反射、防序列化,是《Effective Java》作者最推崇的写法。

public enum Singleton {
    INSTANCE; // 本身就是单例

    public void businessMethod() {
        System.out.println("执行业务逻辑");
    }
}
// 调用方式:Singleton.INSTANCE.businessMethod();

5. 枚举以外的其他方式如何避免序列化破坏单例模式呢?

你只需要在单例类里加一个魔术方法readResolve()

private Object readResolve() {
    // 反序列化时,JVM 会自动调用这个方法
    // 直接返回我们现有的单例实例,而不让它创建新对象
    return instance; 
}

6. 工厂设计模式

工厂模式的核心就是 : 把“对象的创建”和“对象的使用”分离开。也就是达到了解耦的效果。

6.1 简单工厂模式

一个工厂类,根据你传的参数(如字符串 "A"),用 if-elseswitch 返回对应的实例。它不属于 23 种设计模式,但最常用。缺点是增加新产品必须改工厂代码,不符合“开闭原则”。

public class PayFactory {
    public static PayStrategy getPay(String type) {
        if ("WX".equals(type)) return new WxPay();
        if ("ALI".equals(type)) return new AliPay();
        throw new IllegalArgumentException("未知支付类型");
    }
}

6.2 工厂方法模式⭐

不直接创建产品,而是定义一个创建对象的接口,让子类决定实例化哪一个类。把实例化推迟到子类中进行。

// 抽象工厂
public interface PayFactory {
    PayStrategy createPay();
}
// 具体工厂
public class WxPayFactory implements PayFactory {
    public PayStrategy createPay() { return new WxPay(); }
}

优缺点:增加新产品只需加个工厂类,不改老代码;但类会成倍增加,变复杂。

6.3 抽象工厂模式 (Abstract Factory)⭐产品族

介绍:如果说工厂方法是产一个零件,抽象工厂就是产一整套流水线。比如“苹果工厂”既产 iPhone 也产 iPad,“华为工厂”既产华为手机也产华为平板。

public interface AbstractFactory {
    PayStrategy createPay();     // 创建支付产品
    RefundStrategy createRefund(); // 创建退款产品
}
模式 核心关注点 复杂度
简单工厂 一个工厂管所有,简单但死板
工厂方法 每个产品配一个工厂,灵活好扩展
抽象工厂 一个工厂管一整套产品线
6.4 总结

1、对于简单工厂和工厂方法来说,两者的使用方式实际上是一样的,如果对于产品的分类和名称是确定的,数量是相对固定的,推荐使用简单工厂模式;

2、抽象工厂用来解决相对复杂的问题,适用于一系列、大批量的对象生产。

6.5 哪些场景用到了工厂设计模式呢? 你能说一些吗?⭐

Spring 框架的 BeanFactory (最经典) ⭐️

这是典型的简单工厂。Spring 容器就是一个巨大的工厂,它负责生产、管理所有的 Bean。你不需要 new,只需要通过 getBean() 就能拿到对象。

JDBC 的 DriverManager

根据你提供的数据库 URL(如 jdbc:mysql://...),工厂会自动判断并加载对应的驱动程序。

多渠道支付系统 ⭐️

系统支持微信、支付宝、银联、Paypal。 前端传一个 payType,后端通过 PayFactory 根据类型生成对应的 PayService

7. 代理模式

代理模式是 Spring 框架的灵魂。简单来说,就是中介

7.1 定义

代理模式给某一个对象提供一个代理对象,并由代理对象控制对原对象的引用。

7.2 代理模式的分类

静态代理 : 手动编写代理类,和目标类实现同一个接口。每一个目标类都要配一个代理类,如果接口加了方法,代理和目标类都要改,非常麻烦,实际开发基本不用

动态代理 (Dynamic Proxy) ⭐⭐⭐

特性 JDK 动态代理 CGLIB 代理
原理 利用反射机制生成一个实现接口的匿名类。 利用 ASM 字节码技术生成目标类的子类。
限制 目标类必须实现接口 目标类不能是 final(因为要继承)。
性能 较轻量,创建对象快,执行效率略低。 较重,创建对象慢,但执行效率高。
Spring 默认 如果目标实现了接口,用 JDK。 如果没实现接口,强行用 CGLIB。
7.3 代理模式的应用场景(面试必背)
  • Spring AOP:这是最典型的。你在方法上加个 @Transactional,Spring 就给你的类生成一个代理。执行前开启事务,执行后提交事务。

  • Seata 分布式事务:Seata 的 AT 模式会代理你的 DataSource(数据源)。当你的代码执行 SQL 时,代理对象会拦截它,自动生成 undo_log 回滚日志。

  • MyBatis:你只写了 Mapper 接口,没写实现类,为什么能直接调?因为 MyBatis 在运行时用动态代理帮你生成了实现类,并去执行 SQL。

  • 远程调用 (RPC):比如 Dubbo。你调用远程服务像调本地方法一样,其实是代理对象在底层帮你跑了网络请求。

7.4 为什么需要代理模式

为了在不修改原始代码的前提下,做功能增强 / 增加额外逻辑,并实现对目标对象的访问控制。

在软件设计中,我们追求“开闭原则”(对扩展开放,对修改关闭)。当你发现原有的类功能不够用,但你又不敢(或者没权限)去改它的源码时,代理模式就是你的“救命稻草”。

总结一下, 其实就是三个核心原因:

1) 功能增强(最核心理由:AOP 的基石)

2) 控制访问(中介保护): 代理类就像一个“挡箭牌”或者“中介”,它决定了调用者能不能访问目标对象,或者以什么方式访问。

3) 延迟加载(虚假繁荣): 如果一个对象非常庞大(比如加载一张超大高清图,或者初始化 一个复杂的数据库连接),初始化它需要消耗很多时间和内存。

8. 策略模式

8.1 定义⭐

策略模式定义了一系列算法,并将每一个算法封装起来,使它们可以相互替换。它让算法的变化独立于使用算法的客户。

通俗理解:你要从 A 地去 B 地,可以选择走路、骑车、开车。这三种方式就是三种“策略”。无论你 选哪种,目的地不变,但你可以根据天气(运行时条件)随时切换。

8.2 核心角色⭐

策略接口 (Strategy):定义所有支持算法的公共接口。 具体策略 (Concrete Strategy):实现具体算法的类(如:微信支付、支付宝支付)。

8.3 应用场景⭐⭐⭐

我的项目: 在我们的音频平台中, 存在 3 种不同的商品, 分别是专辑, 音频, vip, 这三种商品的结算和发货逻辑是不一样的, 如果没有策略模式, 那么相对应的 service 的实现类代码会存在很多 if-else, 但是通过策略模式, 我可以将这个结算与发货提取出一个抽象策略类 | 接口 PaymentAndXXX, 之后, 不同商品只需要实现这个接口, 然后写自己的结算和发货逻辑, 在我们的 service 中, 我们可以结合工厂模式, 根据不同的上下文返回不同的具体策略实现类, 这样就极大的精简了代码, 在之后就算有新的结算和发货逻辑, 我们也不需要修改原有代码, 只需要写一个新的具体策略即可.

8.4 策略模式的优势

引入策略模式不仅仅是为了“消除 if-else”,它在系统架构层面带来了以下三个核心加分点:

1) 彻底实现“开闭原则” (Open-Closed Principle) : 增加新商品(如“有声书”)必须修改 OrderService 的源码,这会带来巨大的回归测试压力,因为你可能在改动时无意间破坏了“VIP”或“专辑”的既有逻辑。

2) 逻辑隔离与单一职责 (Single Responsibility Principle): 传统方式:一个大 Service 承担了所有商品的结算和发货逻辑,职责太重,代码量可能达到几千行。策略模式:每种商品的业务逻辑都在各自的策略类里。

8.5 你是如何实现“零 if-else”动态获取策略的?

通过Spring 的自动注入来实现的:

@Autowired 
private Map<String, ProductStrategy> strategyMap;

9. 适配器模式

适配器模式(Adapter Pattern)通过一个中间件(适配器)将一个类的接口转换成客户期望的另一个接口,使原本不能一起工作的类能够协同工作。

经典例子Java 集合框架中的 Arrays.asList()

  • 你手里的“设备”(Adaptee):一个普通的 Java 数组(比如 String[])。
  • 你需要的“插座”(Target接口):Java 的 List 接口
  • 问题:数组虽然和 List 很像,但数组没有 List 接口定义的 contains()indexOf() 等丰富的方法。你不能直接把 String[] 当作 List 传给一个只接收 List 参数的方法。
String[] names = {"Gemini", "ChatGPT", "Claude"}; // 这是一个数组

// 使用适配器转换
List<String> list = Arrays.asList(names); 

// 现在你可以像操作 List 一样操作它了
list.contains("Gemini");

你会发现Arrays.asList()内部其实定义了一个适配器类(叫 ArrayList,注意这不是 java.util.ArrayList,而是 Arrays 类的一个内部私有类):

  • 这个内部类实现了 List 接口(满足了“插座”的要求)。
  • 它内部持有那个原始数组的引用(连接了“吹风机”)。
  • 当你调用 list.get(0),适配器内部其实是在执行 return array[0]