设计模式的相关面试题
GQ 设计模式
什么是设计模式?请简述其作用。
什么是设计模式?
设计模式是前人总结出的可复用解决方案,用于在面对常见设计问题时,减少试错成本、提升开发效率和代码可维护性。
23 种设计模式分为哪三大类?
23 种设计模式可以分为创建型、行为型、结构型三大类:
1)创建者模式
用于对象的创建,同时隐藏对象创建的逻辑。避免在代码中出现大量的 new 操作和创建对象的逻辑。目的是实现对象创建的解耦;
常见的有单例模式、工厂模式、建造者模式;
2)结构型模式
用于处理对象组合的结构,主要用于类和对象的组合。目的是通过类或对象的组合,实现更大的结构;
常见的有桥接模式、适配器模式、组合模式、代理模式、装饰器模式;
3)行为型模式
用于定义对象如何相互协作实现单个对象无法单独完成的任务,目的是定义类和对象之间的通信方式;
常见的有策略模式、模板模式、状态模式、责任链模式、观察者模式、迭代器模式
请解释什么是单例模式,并给出一个使用场景
单例模式就是保证一个类在整个系统中只有一个实例,并且提供一个全局访问点。它在像数据库连接池、线程池、配置中心这种场景中非常常见。
单例模式有哪几种实现?如何保证线程安全?
单例模式就是保证一个类在整个系统中只有一个实例,并且提供一个全局访问点。它在像数据库连接池、线程池、配置中心这种场景中非常常见。
单例常见有几种实现方式:
- 饿汉式:类加载时就创建实例,线程安全,但可能会浪费资源。
- 懒汉式:用到的时候才创建实例,节省资源,但要注意线程安全问题。
- 双重检查锁(DCL):在懒汉的基础上加锁优化,只在第一次创建时加锁,提高性能,实际开发中最常用。
- 静态内部类:利用类加载机制来实现延迟加载和线程安全,代码优雅,也很推荐。
- 枚举单例:Java中最安全、最简单的写法,可以防止反射和反序列化破坏,是面试中经常提到的最佳实践。
如何保证线程安全:
主要就是控制实例创建过程的并发访问:
- Java 里可以用 synchronized、volatile 关键字,或者静态内部类;
- C++ 用 std::call_once;
- Python 用 new 或锁;
- Go 用 sync.Once。
为什么说枚举是实现单例最好的方式
究其原因,主要有以下三个好处:
1、枚举实现的单例写法简单
2、枚举实现的单例天然是线程安全的
3、枚举实现的单例可避免被反序列化破坏
工厂模式和抽象工厂模式有什么区别?
工厂模式和抽象工厂模式其实都是用来“创建对象”的,只是它们关注的范围不一样。
简单来说,工厂模式是创建某一类产品,而抽象工厂模式是创建一整套相关产品。
工厂模式
工厂模式更像是“一个老板开了很多工厂”,每个工厂负责生产一种产品。
-
举个例子,比如黄老板负责做显卡,有华硕 5090、七彩虹 5090 等不同型号。那我们就可以定义一个“显卡工厂接口”,再让不同品牌的工厂去实现它,各自生产自己的显卡。
-
这种模式的好处是:新增一种显卡,只要新增一个工厂类就行,不用动原来的逻辑,符合开闭原则。
-
使用场景一般是:当创建过程复杂、想让对象创建和使用解耦时,比如数据库连接对象、日志对象等。
抽象工厂模式
抽象工厂更像是“一个集团开了多个配套工厂”,它不仅造显卡,还顺带造配套的主板、电源和散热器。
- 比如说我们要生产一个“二次元风格套装”和一个“赛博风格套装”,每个套装里都包含主机显卡电源等多个关联对象。那就可以让“抽象工厂”定义生产整套组件的接口,每个具体工厂负责生产自己那一套。
- 所以,抽象工厂主要用于创建一系列相关或相互依赖的对象,并保证这些对象之间能搭配使用,比如游戏开发、UI 风格统一、跨平台组件等。
简单来说
- 工厂模式:创建单一类型的对象,像“显卡工厂”。
- 抽象工厂模式:创建一整套相关对象,像“整机制造厂”。
- 前者更关注“一个产品的多种实现”,后者更关注“一系列产品的整体搭配”。
什么是享元模式?一般用在什么场景?
什么是享元模式?
享元模式其实很好理解,它的核心思想就是一句话:
“能共享就共享,别重复创建没必要的对象。”
这样做的目的就是节省内存、提升性能。
为什么要共享?
因为有时候我们系统里会出现成千上万、内容完全一样的小对象,如果统统 new 出来,内存就爆了。
那享元模式就会把“不会变的部分”抽出来做成一份共享,多个地方一起用,而“会变化的部分”则由外部传进来。
简单理解:
- 内部状态(可共享):不会变化、大家都一样的部分
- 外部状态(不共享):每次不同的部分,由调用方传入
这样就能把对象数量大幅减少。
一般用在什么场景?
享元模式其实在我们日常开发里随处可见,只是你可能没注意到:
① Java 的 String 常量池(String Pool)
相同的字符串不会重复创建,比如:
1 | String a = "abc"; |
这俩其实是同一个对象。
这是最经典的享元模式。
② 各种包装类的缓存(Integer.valueOf 等)
像 Integer、Long、Byte 这些基本类型包装类,用 valueOf() 都会复用 -128~127 范围内的对象。
比如:
1 | Integer a = Integer.valueOf(100); |
这是典型的享元。
③ 数据库连接池 / 线程池(池化技术)
池化技术本质上也是享元模式:
- 连接对象创建成本大
- 线程创建成本也大
因此都不会每次 new,而是通过池来复用已有对象。
像 Druid、HikariCP、JedisPool,本质都在用享元思想(共享连接资源)。
④ 游戏开发里的精灵/图元共享
比如一万个同样树木、一万个同样子弹,不可能每个都 new 一个对象,会耗死内存。
享元模式会把不变的模型、纹理共享,只存储变化的位置、角度等状态。
一句话总结
享元模式就是为了减少对象数量、节省内存,把不变的部分做成共享的对象,让大量相似对象共用一份数据。常见应用场景包括 Java 字符串池、包装类缓存、池化技术(连接池、线程池)以及游戏中的模型共享等。
什么是代理模式?一般用在什么场景?
代理模式是一种结构设计模式,通过引入一个代理对象来控制对原始对象的访问。它允许客户端通过代理对象间接访问原始对象,可以在访问过程中加入额外的逻辑或控制。常见的代理模式有以下几种应用场景:
-
远程代理:在分布式系统中,代理模式常用于代理远程对象。例如,Dubbo框架就使用了代理模式来实现远程方法调用,使客户端像调用本地方法一样访问远程服务。
-
动态代理:动态代理通过在运行时创建代理对象并将方法调用分发给不同的处理器。Spring的AOP就是使用动态代理来实现通用的代理逻辑,不需要为每个对象单独创建代理。
-
缓存代理:代理对象可以在访问真实对象前,先查询缓存,避免重复计算或访问资源。如果缓存中没有结果,再去查询真实对象。
-
日志代理:可以在代理对象中统一处理日志记录,简化代码,并且保证日志的一致性。
-
异常代理:统一处理异常的捕获和转换,例如通过代理对象来捕获异常并进行相应的处理。
这些应用场景可以帮助我们在软件开发中更灵活、可扩展地处理一些通用逻辑,如缓存、日志记录和异常处理等。
什么是观察者模式?一般用在什么场景?
观察者模式:
- 核心思想:建立“一对多”的依赖关系,当一个对象状态改变时,自动通知所有观察者更新。
- 应用场景:
- GUI 事件监听(按钮点击通知多个监听器)
- 消息推送系统(发布-订阅)
- 股票价格更新、缓存失效广播等
- 优点:实现对象间的低耦合通信。
什么是模板方法模式?一般用在什么场景?
什么是模板方法模式?
模板方法模式是一种行为型设计模式,它的核心思想就是:在父类中定义算法的骨架(流程),把其中可变的部分交给子类去实现。
- 即父类定流程,子类定细节。这样代码的复用和扩展性更好
举例说明
比如我们有一个优惠券系统,不同类型的优惠券(满减、折扣)都有相同的申请流程,比如:
- 校验优惠券是否有效;
- 判断用户是否符合条件;
- 执行优惠逻辑;
- 显示确认信息。
这些步骤的流程是固定的,但每种优惠券的具体实现不同。
我们就可以把整个流程定义在父类的模板方法里,而让不同的优惠券类去实现自己的细节逻辑。
这样一来,公共流程就能复用,差异部分也能灵活扩展。
模板方法模式的核心特点
- 流程固定、细节可变 —— 把算法流程定义好,不同子类决定具体实现。
- 复用性高 —— 公共逻辑在父类实现,减少重复代码。
- 可扩展性强 —— 新增子类就能新增新逻辑,不影响旧代码。
一般应用场景:
- 有固定流程但部分步骤需要自定义的业务,比如数据处理、报表生成、文件导入导出;
- 多个类之间有公共逻辑但部分细节不同时,比如支付流程、审批流程;
- 框架类库中非常常见,比如:
- Java 的 JdbcTemplate 处理数据库操作;
- HttpServlet 的 service() 方法就是模板方法,doGet、doPost 是子类实现的具体逻辑。
什么是策略模式?一般用在什么场景?
什么是策略模式
策略模式是一种行为型设计模式,它的核心思想是:定义一系列算法,把它们一个个封装起来,让它们可以互相替换,而不影响客户端的使用。
简单来说,就是用不同的策略类去封装不同的算法逻辑,这样我们就不用在代码里堆满 if-else 或 switch,而是让系统在运行时动态地选择具体的算法
举个例子:
比如我们做一个支付系统,用户可以选择微信、支付宝、信用卡等不同支付方式。
传统写法会是:
1 | if (payType == "wechat") { ... } |
1.用策略模式的话,我们可以给每种支付方式都建一个独立的策略类,比如:
- WechatPayStrategy
- AlipayStrategy
- CreditCardPayStrategy
2.然后统一定义一个 PayStrategy 接口,客户端只要调用统一的接口就行,底层自动根据用户选择去调用对应的策略类。
3.这样增加一种新的支付方式时,只需要加一个新策略类,不用改原来的代码,符合“开闭原则”。
策略模式的核心优点:
- 消灭 if-else:让代码更清晰、可维护。
- 易于扩展:新增算法只要新增类,不改旧逻辑。
- 更高复用性:策略类可以被不同场景共用。
一般用在什么场景:
- 多算法可互换:比如排序策略、压缩算法、支付方式;
- 业务逻辑多分支:替代大量 if-else;
- 客户端不关心算法细节:只需要知道用哪个策略就行。
小结:策略模式其实就是把一堆 if-else 拆成不同的策略类,让系统可以灵活切换算法,同时让代码更优雅、更容易扩展。
什么是责任链模式?一般用在什么场景?
责任链模式是一种行为型设计模式,它通过将多个处理对象串联成一条链,依次传递请求,直到某个处理对象处理完请求为止。每个处理对象会检查自己是否能处理该请求,如果不能,它会把请求转发给下一个处理对象,直到链中的某个对象处理了请求,或者整个链无法处理请求。
常见应用场景包括:
- 过滤器链:在Web开发中,Spring框架中的FilterChain就是一个责任链,它允许多个过滤器依次处理请求。
- 日志记录:日志记录系统可以使用责任链模式将多个日志处理器串联起来,灵活选择不同的记录方式。
- 异常处理:在应用程序中,异常处理器链可以按顺序处理各种类型的异常。
- 授权认证:可以通过责任链模式实现多个认证过程,比如检查用户身份、权限等。
例如,在一个在线商店中,订单处理可以通过责任链模式实现:
- 检查订单信息是否完整。
- 检查库存是否足够。
- 检查用户余额是否足够。
- 最后确认订单并更新库存和余额。
每个步骤封装成一个处理器对象,通过链式调用依次执行,确保每个步骤按照顺序处理订单。如果某个步骤失败,整个链会终止并返回错误信息。
这种模式的优势是能够使代码更加灵活、扩展性强,同时避免了请求发送者与接收者之间的紧密耦合。
谈谈你了解的最常见的几种设计模式,说说他们的应用场景
什么是设计模式?
设计模式是前人总结出的可复用解决方案,用于在面对常见设计问题时,减少试错成本、提升开发效率和代码可维护性。
常见的几种设计模式及其应用如下:
1.单例模式:
- 核心思想:保证一个类在系统中只有一个实例,并提供全局访问点。
- 应用场景:
- 日志管理器(全局唯一)
- 数据库连接池(避免重复连接)
- 线程池、配置中心、缓存管理等
- 优点:节省系统资源,控制全局状态一致性。
- 注意点:多线程场景要注意加锁或双重检查机制(Double-Check Locking)。
2.策略模式:
- 核心思想:定义一系列算法,将它们封装起来,使它们可以互相替换,且客户端不必关心具体实现。
- 应用场景:
- 电商支付方式(支付宝、微信、银行卡)
- 推荐算法、压缩算法、日志输出策略等
- 优点:避免大量 if-else/switch 判断,扩展性好;
3.模板方法模式:
- 核心思想:在父类中定义算法的骨架(流程),把其中可变的部分交给子类去实现。
- 应用场景:
- 数据处理(统一流程但不同实现)
- 支付前置后置工作,优惠卷校验等等
- 抽象类定义框架,如爬虫、编译器、Hook 机制
- 优点:复用固定流程,提高一致性,子类只需重写差异部分。
4.工厂模式:
- 核心思想:定义创建对象的接口,由子类决定实例化哪一个类。
- 分类:简单工厂、工厂方法、抽象工厂。
- 应用场景:
- 数据库驱动加载(MySQLFactory、OracleFactory)
- UI 组件创建、日志输出实例化等
- 优点:解耦对象创建和使用逻辑,方便扩展和维护。
5.观察者模式:
- 核心思想:建立“一对多”的依赖关系,当一个对象状态改变时,自动通知所有观察者更新。
- 应用场景:
- GUI 事件监听(按钮点击通知多个监听器)
- 消息推送系统(发布-订阅)
- 股票价格更新、缓存失效广播等
- 优点:实现对象间的低耦合通信。
你认为好的代码应该是什么样的?
我觉得好的代码首先要清晰易懂,逻辑直观、命名规范,让别人一看就能理解。
其次是高内聚低耦合,模块内部功能集中、模块之间尽量独立,这样修改或扩展时影响最小。
同时要易测试、易扩展,遵循像 SOLID、KISS 这些设计原则,保持结构简单又灵活。
在性能和错误处理上,选择合适的算法和数据结构,异常要有预判和日志记录,避免系统出错。
最后,保持规范和一致性,该有的注释和文档要完善。
简单来说,好的代码就是——简单、清晰、稳定、可维护。
补充:
SOLID、KISS 设计原则
SOLID是面向对象设计的五大基础原则,由Robert C. Martin提出,旨在提高代码的可维护性和扩展性:
1.单一职责原则(SRP)
一个类只负责一个职责,避免功能混杂。例如,用户信息类若包含地址、登录等属性,可能需拆分为多个类。
2.开闭原则(OCP)
对扩展开放,对修改关闭。新功能应通过新增代码实现,而非修改现有代码。
3.里氏替换原则(LSP)
子类应能替换父类而不破坏程序行为。例如,子类实现的接口需符合父类定义的“协议”。
4.接口隔离原则(ISP)
接口应保持职责单一,避免强制依赖未使用的接口。
5.依赖倒置原则(DIP)
高层模块不依赖低层模块,二者均依赖抽象接口。
KISS原则
KISS(Keep It Simple, Stupid)强调代码应简洁易懂,避免过度设计: 优先使用简单解决方案,减少复杂逻辑。例如,避免冗余的嵌套条件判断。 与SOLID互补:SOLID解决结构问题,KISS确保实现简洁。