尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

设计模式(一) - 建造者模式、单例模式、工厂模式

设计模式(一) - 建造者模式、单例模式、工厂模式 一、单例、工厂、建造者模式速记介绍及对比1. 单例模式作用保证整个系统只有一个实例对象适用工具类、配置类、连接池优点节约内存全局唯一访问点缺点不利于单元测试容易隐藏依赖java// 懒汉简单示例 public class Singleton { private static Singleton instance; private Singleton(){} public static Singleton getInstance(){ if(instance null){ instance new Singleton(); } return instance; } }2. 工厂模式简单工厂 / 工厂方法作用封装对象创建直接给你完整成品不关心组装细节适用同一族对象根据条件创建不同实例简单工厂一个工厂类if‑else 判断创建工厂方法每个产品对应一个工厂利于扩展优点创建与使用分离新增产品不用改业务代码缺点工厂变多简单工厂修改会改动原有代码违反开闭一句话工厂直接把完整对象给你。3. 建造者模式 Builder作用分步组装复杂对象处理大量可选参数链式构建适用订单、DTO、配置字段多部分必填部分可选优点链式可读性好build 统一参数校验避开构造函数爆炸缺点代码多字段改动Builder 同步要改简单对象没必要用一句话工厂给成品建造者一步一步拼零件。三者核心对比表格模式核心目的关注点业务常见形态单例实例唯一数量配置、工具工厂封装对象创建得到完整对象根据类型产出不同对象建造者分步组装复杂对象组装过程链式 builderLombok Builder口诀单例控数量工厂造对象建造者拼零件。二、什么是建造者设计模式建造者模式Builder核心把一个复杂对象的构建和它的表示分离同样的构建流程可以造出不同的对象。适用对象构造很复杂参数多、可选参数多不想写一大堆重载构造函数。通俗例子造电脑CPU、显卡、内存、硬盘。装配流程固定但可以搭配不同配件组装出「游戏电脑」「办公电脑」。建造者负责一步步装配不需要使用者记住所有构造参数。角色Product 产品最终要生成的复杂对象电脑Builder 抽象建造者定义各个部件的构建抽象方法ConcreteBuilder 具体建造者实现抽象构建不同版本产品Director 指挥者定义组装顺序调用 builder 的方法不关心细节Java 极简示例java//产品 class Computer { public String cpu; public String gpu; public String memory; } //抽象建造者 interface ComputerBuilder { void buildCpu(); void buildGpu(); void buildMemory(); Computer getResult(); } //游戏电脑建造者 class GameComputerBuilder implements ComputerBuilder{ private Computer pcnew Computer(); Override public void buildCpu() { pc.cpui9; } Override public void buildGpu() { pc.gpuRTX5090; } Override public void buildMemory() { pc.memory32G; } Override public Computer getResult() { return pc; } } //指挥者规定组装步骤 class Director{ public Computer construct(ComputerBuilder builder){ builder.buildCpu(); builder.buildGpu(); builder.buildMemory(); return builder.getResult(); } } //使用 public class Test { public static void main(String[] args) { Director directornew Director(); Computer pcdirector.construct(new GameComputerBuilder()); } }开发里最常见链式调用 Builder简化版无 Director日常业务代码几乎不用上面完整四角色更多是内部静态 Builder 类链式 set比如javaUser user User.builder() .name(张三) .age(22) .phone(123) .build();lombok 的Builder注解就是建造者模式。优缺点✅优点构造复杂对象代码清晰不用超长构造函数构建逻辑和业务分离新增组合只新增 Builder分步创建对象❌缺点类变多产品结构变化时建造者都要改和工厂模式区别工厂模式直接给你完整对象不关心组装步骤建造者模式一步一步组装部件重点是构造过程三、开发建造者设计模式实例举例业务开发真实实例创建订单 Order建造者模式业务场景创建订单字段非常多有必填、大量可选字段不适合写多个重载构造方法用建造者。日常工作大多用简化 Builder没有 Director 指挥者Lombok 的Builder底层就是这套思想。1、订单实体 Orderjava/** * 订单字段多部分必填部分可选 */ public class Order { // 必填 private Long orderId; private Long userId; private String goodsName; // 可选 private Integer num; private Double price; private String address; private String remark; // 私有构造只能通过builder构建 private Order(OrderBuilder builder) { this.orderId builder.orderId; this.userId builder.userId; this.goodsName builder.goodsName; this.num builder.num; this.price builder.price; this.address builder.address; this.remark builder.remark; } // 静态入口获取建造者 public static OrderBuilder builder() { return new OrderBuilder(); } /** * 静态内部建造者类 */ public static class OrderBuilder { private Long orderId; private Long userId; private String goodsName; private Integer num; private Double price; private String address; private String remark; // 链式赋值返回this public OrderBuilder orderId(Long orderId) { this.orderId orderId; return this; } public OrderBuilder userId(Long userId) { this.userId userId; return this; } public OrderBuilder goodsName(String goodsName) { this.goodsName goodsName; return this; } public OrderBuilder num(Integer num) { this.num num; return this; } public OrderBuilder price(Double price) { this.price price; return this; } public OrderBuilder address(String address) { this.address address; return this; } public OrderBuilder remark(String remark) { this.remark remark; return this; } // 真正构建对象可以在这里做参数校验 public Order build() { // 必填参数校验建造的时候统一校验 if(orderId null || userId null || goodsName null){ throw new IllegalArgumentException(订单id、用户id、商品名称不能为空); } return new Order(this); } } }2、业务中如何使用javapublic class Test { public static void main(String[] args) { // 链式构建订单可选字段可以不写 Order order Order.builder() .orderId(10001L) .userId(888L) .goodsName(机械键盘) .num(1) .price(499.0) .address(北京通州区) .remark(尽快发货) .build(); // 只传必填可选字段全部省略也可以 Order simpleOrder Order.builder() .orderId(10002L) .userId(999L) .goodsName(鼠标) .build(); } }3、实际开发要点适用场景类属性多大量可选参数避免构造函数爆炸。优势链式调用可读性极强build()统一做参数校验。Lombok 简化业务代码不会手写上面一大段直接加注解自动生成全部 builder 代码javaBuilder public class Order { private Long orderId; private Long userId; // ...其他字段 }4. 什么时候用完整 BuilderDirector极少框架底层才用普通业务一律用上面简化版。四、建造者模式的优点和缺点建造者模式Builder优缺点优点对象创建逻辑清晰链式调用可读性高不用写一堆重载构造函数。分步构建对象可以控制构建过程build 方法集中做参数校验对象创建出来一般就是合法状态。分离构建和业务相同构建流程可以产出不同对象扩展新的对象组合方便。成员变量可以一部分必填、一部分可选非常适合字段多的实体订单、DTO。缺点代码量增加需要额外编写 Builder 内部类手写比较繁琐业务一般靠 LombokBuilder生成。如果产品类字段经常改动Builder 类也要同步修改维护成本上升。完整 Builder‑Director 模式笨重普通业务几乎不用大多只用简化链式 Builder。对象差异很大时会产生很多具体建造者类类数量膨胀。适用场景✅适合字段多、存在大量可选参数、复杂对象构建订单、DTO、配置对象❌不适合简单对象只有 2‑3 个属性用构造函数直接 new 更简单。五、单例模式的懒汉式和饿汉式有什么区别懒汉式 vs 饿汉式单例模式核心区别实例什么时候创建饿汉式饿类加载就直接 newjavapublic class HungrySingleton { // 类加载直接初始化实例 private static final HungrySingleton INSTANCE new HungrySingleton(); private HungrySingleton(){} public static HungrySingleton getInstance(){ return INSTANCE; } }创建时机类加载的时候直接创建对象线程安全天然线程安全JVM 保证不加锁优点简单、不会并发问题缺点类加载就占内存就算全程不用这个对象对象也已经创建适用对象占用内存不大确定会用到。懒汉式懒用到的时候才 new基础版线程不安全多线程会创建多个实例不能上生产javapublic class LazySingleton { private static LazySingleton instance; private LazySingleton(){} public static LazySingleton getInstance(){ if(instance null){ instance new LazySingleton(); } return instance; } }创建时机第一次调用 getInstance 才创建优点不用就不创建节省内存缺点多线程环境可能生成多个对象线程不安全生产可用双重检查锁 DCLDouble‑Checked‑Lockjavapublic class DclSingleton { private static volatile DclSingleton instance; private DclSingleton(){} public static DclSingleton getInstance(){ if(instance null){ synchronized (DclSingleton.class){ if(instance null){ instance new DclSingleton(); } } } return instance; } }volatile防止指令重排避免拿到半初始化对象。对比总结表格对比项饿汉式懒汉式 (DCL 双重检查锁)实例创建类加载直接创建调用时才创建线程安全天然安全需要 volatilesynchronized内存类加载就占用延迟加载省内存代码复杂度简单代码复杂面试一句话饿汉类加载直接实例化懒汉用到才实例化多线程要用 DCL 双重检查锁加 volatile。六、介绍枚举单列枚举单例《Effective Java》推荐实现单例的最佳方式代码javapublic enum EnumSingleton { // 唯一实例 INSTANCE; // 业务方法 public void doSomething(){ System.out.println(执行业务逻辑); } }使用javapublic class Test { public static void main(String[] args) { EnumSingleton.INSTANCE.doSomething(); } }为什么强天然线程安全枚举类在类加载阶段初始化实例JVM 保证只会实例化一次。避免反射破坏单例饿汉、懒汉 DCL 可以通过反射调用私有构造器强行 new 出新对象枚举反射无法创建实例直接抛异常。序列化安全普通单例实现 Serializable反序列化会生成新对象枚举序列化、反序列化始终返回同一个 INSTANCE。两大坑反射攻击、序列化攻击枚举全部帮你搞定。缺点饿汉加载特性类加载就创建实例属于饿汉模式不支持懒加载不用也会占内存。不能继承其他类枚举默认继承 EnumJava 不支持多继承。四种单例对比速记表格方式线程安全懒加载防反射防序列化饿汉式✅❌❌❌普通懒汉❌✅❌❌DCL 双重检查锁✅✅❌❌枚举单例✅❌✅✅面试标准答案枚举单例是 Effective Java 推荐的单例实现。由 JVM 保证实例唯一天然线程安全能够抵御反射和序列化破坏缺点是不支持延迟加载。如果对象不大优先使用枚举单例需要懒加载使用 DCL 双重检查锁。七、枚举单例的线程安全性是如何保证的枚举单例线程安全底层原理线程安全由 JVM 类加载机制保证不是加 synchronized、不是 volatile。1、枚举实例什么时候创建枚举INSTANCE在类加载的初始化阶段就完成实例化。类加载流程加载 → 验证 → 准备 →初始化执行静态变量赋值、静态代码块就在【初始化】阶段。枚举的INSTANCE本质就是public static final的静态常量。JVM 规定类的初始化阶段只会执行一次由 JVM 加锁保证多线程不会重复初始化。多个线程同时访问这个枚举类JVM 确保静态初始化逻辑只跑一遍所以INSTANCE只会 new 一次。2、关键和饿汉式原理很像javapublic enum EnumSingleton { INSTANCE; }编译后等价效果伪代码javapublic final class EnumSingleton extends Enum{ public static final EnumSingleton INSTANCE new EnumSingleton(); static { // 初始化JVM保证只执行一次 } }属于饿汉模式类加载就创建不是调用的时候才创建。3、为什么 DCL 需要 volatile枚举不需要DCL 懒汉运行期自己写代码 new 对象会发生指令重排需要 volatile 禁止。枚举实例在类加载阶段由 JVM 完成对象实例化这个阶段 JVM 内部已经处理指令重排业务代码看不到 new 过程不需要 volatile。4、面试精简回答枚举单例的线程安全由JVM 类加载机制保障。枚举实例是 static final 常量在类初始化阶段完成实例化JVM 对类初始化加了隐式锁保证多线程环境下类初始化只会执行一次因此枚举实例只会创建一份天然线程安全。补充优点同时抵御反射、序列化破坏单例局限不支持懒加载类加载就占用内存。八、dcl为什么会发生指令重排什么是指令重排指令重排 DCL 为什么必须 volatile什么是指令重排CPU、JVM 为了提高运行效率在不改变单线程执行结果的前提下打乱代码执行顺序重新编排指令。只保证单线程结果正确多线程下会出现意料之外的问题。示例代码书写顺序javaa 1; b 2;CPU 可能先执行b2再执行a1单线程结果没变化这就是指令重排。instance new DclSingleton();不是一步完成这行代码底层拆成 3 条指令分配内存空间给 instance 对象开辟堆内存初始化对象执行构造方法给成员变量赋值把 instance 引用指向这块内存地址正常顺序1 → 2 → 3指令重排后可能变成1 → 3 → 2先分配内存直接把引用指向内存对象还没初始化完成构造函数还没跑。DCL 故障场景多线程javaif(instance null){ synchronized (DclSingleton.class){ if(instance null){ instance new DclSingleton(); // 会重排 } } } return instance;线程 A 执行instance new发生重排执行 1、3还没执行 2对象没初始化完时间片用完切换线程。线程 B 进来判断instance ! null直接 return 这个半初始化、残缺的对象。线程 B 拿到的对象内部变量都是默认值直接使用就会出现诡异 bug。 这就是 DCL 不加 volatile 的致命问题。volatile 做了什么禁止指令重排序保证new的三步一定按 1‑2‑3 顺序执行。内存可见性一个线程修改 instance其他线程立刻看到最新值。javaprivate static volatile DclSingleton instance;面试简短背诵版指令重排是 JVM/CPU 为优化性能在单线程结果不变的情况下调整指令执行顺序。instance new 对象()会拆成分配内存、初始化对象、引用赋值三步可能被重排成分配内存→引用赋值→初始化对象。多线程下其他线程可能拿到未初始化完成的半实例。volatile 禁止该指令重排同时保证内存可见性修复 DCL 漏洞。九、DCL为什么要使用volatile关键字DCL 为什么要用 volatile双重检查锁 DCL如果不加volatile会因为指令重排拿到半初始化的残缺对象出现诡异 BUG。instance new DclSingleton () 底层分为 3 步分配堆内存调用构造函数初始化对象将 instance 引用指向分配好的内存JVM、CPU 为了性能可以发生指令重排执行顺序变成1 → 3 → 2也就是分配完内存先把引用赋值给 instance对象构造还没做完。故障场景线程 A 进入 synchronized执行 new发生重排做完 1、3还没做 2时间片切换。此时instance ! null但是对象内部还没初始化完毕。线程 B 执行外层if(instance null)发现不为 null直接返回这个半初始化对象。线程 B 使用这个对象成员都是默认值业务出现异常。volatile 两个核心作用禁止指令重排序保证 new 对象的三步严格按 1‑2‑3 执行不会先赋值引用再初始化。保证内存可见性一个线程修改 instance其他线程立刻读到最新的值不会读取 CPU 缓存的旧副本。synchronized 只能保证原子性、可见性不能阻止方法内部的指令重排所以 DCL 必须搭配 volatile。面试背诵版DCL 中volatile有两个作用①禁止指令重排序防止new对象时先赋值引用再执行构造初始化避免其他线程获取到半初始化对象②保证内存可见性instance 修改后对其他线程立即可见。十、介绍工厂设计模式工厂设计模式Factory Pattern属于创建型设计模式核心思想把对象创建逻辑和业务使用逻辑分离不直接 new 对象由工厂负责实例化。解决痛点代码里到处new Xxx()类名硬编码耦合严重如果构造逻辑变了所有调用处都要改。工厂模式分为三种递进版本简单工厂静态工厂—— 不属于 GoF23 种标准设计模式工厂方法模式Factory Method—— 标准 GoF抽象工厂模式Abstract Factory—— 标准 GoF一、简单工厂模式静态工厂结构产品接口 / 抽象产品统一产品规范具体产品实现抽象产品工厂类提供静态方法根据参数判断创建哪一个产品示例代码Javajava// 抽象产品 public interface Car { void run(); } // 具体产品1 class Benz implements Car { Override public void run() { System.out.println(奔驰行驶); } } // 具体产品2 class Bmw implements Car { Override public void run() { System.out.println(宝马行驶); } } // 简单工厂 public class CarFactory { public static Car getCar(String type) { if (benz.equals(type)) { return new Benz(); } else if (bmw.equals(type)) { return new Bmw(); } throw new IllegalArgumentException(未知车型); } } // 使用 public class Client { public static void main(String[] args) { Car car CarFactory.getCar(benz); car.run(); } }优缺点✅ 优点客户端不需要关心创建细节❌违反开闭原则新增车型必须修改工厂if-else工厂职责过重面试重点简单工厂不是 23 种 GoF 设计模式二、工厂方法模式 Factory Method标准工厂模式核心改进一个产品对应一个工厂消除简单工厂巨大的 if/else抽象产品Car具体产品Benz、Bmw抽象工厂CarFactory定义创建产品的方法具体工厂BenzFactory、BmwFactory各自创建对应产品UML 思想plaintext抽象产品 ← 具体产品 抽象工厂 ← 具体工厂 具体工厂 → 生产对应的具体产品Java 示例java// 抽象产品 public interface Car { void run(); } class Benz implements Car{...} class Bmw implements Car{...} // 抽象工厂 public interface CarFactory { Car createCar(); } // 奔驰工厂 public class BenzFactory implements CarFactory{ Override public Car createCar() { return new Benz(); } } // 宝马工厂 public class BmwFactory implements CarFactory{ Override public Car createCar() { return new Bmw(); } } // 客户端 public class Client { public static void main(String[] args) { CarFactory factory new BenzFactory(); Car car factory.createCar(); car.run(); } }优缺点✅符合开闭原则新增车型只新增产品类 工厂类不用修改原有代码✅ 单一职责每个工厂只生产一种产品❌ 类爆炸每增加一个产品就要新增 2 个类产品 工厂适用场景产品种类会持续扩展希望隔离对象创建逻辑。三、抽象工厂模式 Abstract Factory工厂方法只生产一类产品只造车抽象工厂生产一组相关 / 相互依赖的产品族汽车 发动机 轮胎成套生产概念区分产品等级结构Car奔驰 / 宝马产品族一套配套产品奔驰系列奔驰车身 奔驰发动机宝马系列宝马车身 宝马发动机结构抽象产品 ACar抽象产品 BEngine具体产品BenzCar、BenzEngineBmwCar、BmwEngine抽象工厂定义创建 Car、Engine 多个方法具体工厂BenzFactory造奔驰整套组件、BmwFactory造宝马整套组件伪代码示意java// 抽象工厂可以生产一整套产品 public interface AbstractFactory { Car createCar(); Engine createEngine(); } public class BenzFactory implements AbstractFactory{ Override public Car createCar() {return new BenzCar();} Override public Engine createEngine() {return new BenzEngine();} }优缺点✅ 保证同一产品族的产品互相匹配客户端一次性获取整套对象❌严重违反开闭原则如果新增一个产品比如轮胎所有工厂接口、所有实现工厂全部要修改使用场景有多套配套产品族产品族内部稳定、极少新增产品等级。三种工厂模式对比面试高频表格模式核心特点开闭原则缺点简单工厂一个工厂生产所有产品违反if-else 膨胀不属于 GoF工厂方法一个工厂只生产一种产品符合类数量急剧增多抽象工厂工厂生产一整套相关产品族新增产品等级时违反扩展新产品等级代价巨大面试高频问答整理1. 工厂方法和简单工厂最大区别简单工厂只有一个统一工厂类工厂方法每个产品一个工厂新增产品无需修改旧代码满足开闭原则。2. 工厂方法 vs 抽象工厂区别工厂方法工厂只创建单一类型产品抽象工厂工厂可以创建多个关联产品产品族。3. Spring 中哪里用到工厂模式BeanFactory典型工厂模式FactoryBean工厂方法模式JDKjava.util.Calendar#getInstance()简单工厂JDBC Driver 获取连接也使用工厂思想4. 使用工厂模式带来的好处总结客户端无需关注对象创建细节解耦对象创建逻辑集中管理统一控制缓存、初始化、权限判断方便扩展、统一管控实例。
返回列表