资讯详情

资讯详情

Java设计模式实战:高频模式与订单系统组合应用

提到“Java设计模式”很多同学第一反应是GoF那本经典书里的23种模式再加上各种衍生变体一下子就能把人劝退。这个数字本身其实很有误导性。日常工作和项目复盘里真正高频出现的模式也就是七八种创建型里的单例、工厂、建造者结构型里的代理、适配器、装饰器行为型里的策略、观察者、模板方法。把这十来种吃透再去碰复杂框架和中间件源码你会舒服很多面试被问“你用过哪些设计模式”时也不至于只能背定义。这篇文章更适合两类人看一类是写了一两年业务代码、想把代码往“可维护、可扩展”方向掰一掰的同学另一类是背过几个模式名词但真正换一个场景就不知道怎么下手的初学者。我不会把23种模式挨个列一遍而是挑最常用的几种按“解决什么问题、怎么设计、什么场景用、容易踩什么坑”来展开。最后用一套订单系统的例子把主要的几个模式串起来让你看到模式不是孤立的而是可以组合起来解决真实问题的。1. 设计模式到底解决什么问题1.1 从三个设计原则说起很多人学设计模式容易陷入一个误区上来就记UML图记类与类之间是继承还是组合结果一到自己写代码还是全堆在Service里。设计模式的本质不是画出漂亮的类图而是解决一个问题当需求变化时怎么让代码的改动面最小。这里必须提三个原则几乎所有常用模式都在围着它们转开闭原则对扩展开放对修改关闭。翻译成人话就是加一个新功能时最好别去改动已经跑得好好的老代码而是靠新增类、新增配置把它接进来。策略模式、工厂模式就是典型。单一职责一个类只有一个让它变化的原因。如果某个类里面有“发送HTTP请求”“解析JSON”“写数据库”“发短信”四件事任何一个需求变动都会改到这个类那这个类迟早被改得千疮百孔。依赖倒置调用方依赖抽象接口而不是依赖具体实现类。这样换实现的时候调用方不用跟着动。这三个原则不是玄学。设想一个没有这些原则的Service方法里面先创建数据库连接再拼SQL再判断订单类型再算价格再发通知。一次需求变动这些代码全要动测试全要回归。设计模式就是把这些容易变化的部分隔离出来让每个变化只影响一个小模块。1.2 三类模式的划分逻辑23种经典模式按目的分成三类这个分类本身也暗示了代码里“变化”最容易发生的位置创建型模式管对象怎么来。核心是把“创建对象”这件事从业务代码里剥离。单例管对象的唯一性工厂管对象的创建逻辑建造者管复杂对象的组装过程。结构型模式管类和对象怎么组合。核心是让对象之间的关系灵活且清晰。代理管访问控制适配器管接口转换装饰器管能力增强。行为型模式管对象之间怎么协作、怎么分配算法。核心是减少调用方和具体行为之间的耦合。策略管算法族的切换观察者管事件通知模板方法管流程骨架的复用。打个不严谨但好记的比方如果把写代码比作搭一个团队创建型模式是“怎么招人、怎么组队”结构型模式是“部门怎么划分、谁向谁汇报”行为型模式是“需求怎么流转、任务怎么协作”。三种变化相互独立所以模式也被拆成了三个维度。2. 创建型模式对象怎么生成才合理2.1 单例模式全局唯一的正确写法单例模式是面试出现频率最高的模式没有之一。它解决的场景很清晰有些对象一个进程里只需要一个实例比如配置管理器、线程池、数据库连接池、缓存管理器。如果反复new要么浪费资源要么状态不一致。最容易被追问的是写法。饿汉式、懒汉式、双重检查锁、静态内部类、枚举每种都有讲究。我给你看两种实际项目里最值得用的// 双重检查锁定注意volatile public class ConfigManager { private static volatile ConfigManager instance; private ConfigManager() {} public static ConfigManager getInstance() { if (instance null) { synchronized (ConfigManager.class) { if (instance null) { instance new ConfigManager(); } } } return instance; } }为什么外面判断了一次instance null进入同步块后还要再判断一次因为如果两个线程同时通过了第一次判断其中A线程拿到锁创建了实例并释放锁B线程再进入同步块时如果不重新检查就会再创建一个实例单例就失效了。这是“Double Checked Locking”里最关键的一层。为什么加了volatile因为new ConfigManager()这行代码在字节码层面不是一步完成的大致要经历分配内存、初始化对象、把引用指向内存地址这三步。如果CPU指令重排序可能出现引用已经指向了一块尚未初始化完的内存另一个线程拿到的就是半成品对象。volatile禁止这种重排序。这是很多面试官最喜欢深挖的点。静态内部类写法更推荐日常使用因为代码更短也天然线程安全public class ConfigManager { private ConfigManager() {} private static class Holder { private static final ConfigManager INSTANCE new ConfigManager(); } public static ConfigManager getInstance() { return Holder.INSTANCE; } }它的原理是 JVM 在加载Holder类时才创建实例而类加载过程是加锁且线程安全的所以既实现了懒加载又不需要自己写同步逻辑。单例模式最大的坑有两个。第一如果单例持有可变状态那它就不是“配置快照”而是“全局变量”多个调用方互相改状态问题很难排查。第二如果用反射或者反序列化可以破坏单例。用枚举实现单例是防御这两种破坏最彻底的方式不过大部分项目里为了贴合Spring体系更常用的是把对象交给Spring容器管理由容器保证单例类本身不需要写getInstance()。这点后面避坑章节再细说。2.2 工厂模式把“new”收拢到一处工厂模式解决的是“创建谁”的问题。场景很常见业务里有好几种订单类型普通订单、团购订单、秒杀订单每种订单的处理逻辑完全不一样。如果直接在业务代码里一堆if (type 1) ... else if (type 2) ...那么每新增一种类型都要改动核心方法违反开闭原则。工厂模式的思路是把“根据条件创建哪一个实现类”这个判断独立出去。以订单处理为例public interface OrderProcessor { void process(Order order); } public class NormalOrderProcessor implements OrderProcessor { public void process(Order order) { // 普通订单处理逻辑 } } public class GroupOrderProcessor implements OrderProcessor { public void process(Order order) { // 团购订单处理逻辑 } } public class OrderProcessorFactory { public static OrderProcessor create(String type) { if (normal.equals(type)) { return new NormalOrderProcessor(); } if (group.equals(type)) { return new GroupOrderProcessor(); } throw new IllegalArgumentException(不支持的订单类型: type); } }业务调用方拿到的是OrderProcessor接口具体是哪个实现由工厂决定。之后新增秒杀订单只需要加一个SeckillOrderProcessor再在工厂里加一个分支调用方的核心代码一个字都不用改。很多人看到这里会问这不就是“把if从Service挪到了Factory”吗成立但意义不一样。OrderProcessorFactory是一个纯创建逻辑的类它和业务逻辑隔离测试时可以直接构造真实工厂来验证“输入什么类型返回什么处理器”。而如果把创建逻辑和业务逻辑混在同一个方法里测试就需要模拟整个业务上下文。把不同变化点隔开本身就是开闭原则落地的方式。工厂模式还有变种比如工厂方法模式、抽象工厂模式。工厂方法适合“创建逻辑在不同子类里有不同实现”的场景抽象工厂适合“创建一族相互关联的对象”的场景比如数据库访问层里同时创建Connection、Statement、ResultSet。对大多数业务系统来说一个简单工厂加一个统一的接口基本上就够用了。2.3 建造者模式复杂对象的组装建造者模式解决的是“构造参数太多”的问题。假设你要构造一个订单请求对象里面有商品列表、收货地址、优惠券、发票信息、备注、配送时间等十几二十个字段。写构造函数的话十几个参数排在一起调用方要么记不住顺序要么传错类型要么遇到不需要的字段还得传null。JavaBean的setter方案可以解决一部分问题但对象会被暴露在“构造到一半”的状态而且如果某些字段要求不可变setter就不好使了。建造者模式的写法如下public class OrderCreateRequest { private final String productId; private final String address; private final String couponId; private final boolean needInvoice; private OrderCreateRequest(Builder builder) { this.productId builder.productId; this.address builder.address; this.couponId builder.couponId; this.needInvoice builder.needInvoice; } public static class Builder { private String productId; private String address; private String couponId; private boolean needInvoice; public Builder productId(String productId) { this.productId productId; return this; } public Builder address(String address) { this.address address; return this; } public Builder couponId(String couponId) { this.couponId couponId; return this; } public Builder needInvoice(boolean needInvoice) { this.needInvoice needInvoice; return this; } public OrderCreateRequest build() { // 在build里可以做必填校验 if (productId null || address null) { throw new IllegalArgumentException(商品ID和地址不能为空); } return new OrderCreateRequest(this); } } }调用方的代码变成这样OrderCreateRequest request new OrderCreateRequest.Builder() .productId(P-1001) .address(某小区某栋) .couponId(COUPON-2024) .needInvoice(false) .build();相比十几个参数的构造函数这种写法有两个额外好处。第一调用方可以只设置自己关心的字段不需要为其他字段传占位空值。第二校验逻辑可以集中在build()里做而不是散落在业务代码各处的if判断。现在很多项目直接用 Lombok 的Builder注解生成这套代码效果一样。理解建造者模式的价值以后再看Lombok生成的代码就不会觉得是黑魔法。3. 结构型模式类与对象如何协作3.1 代理模式不改变源码的增强代理模式的场景是你想在某个对象的现有方法前后加入一些逻辑比如日志、权限校验、事务控制、性能统计但又不想改这个类本身的代码。代理类包装了真实对象调用方和代理类打交道代理类在调用真实对象方法前后插入增强逻辑。看一下JDK动态代理的写法public class LogProxy implements InvocationHandler { private final Object target; public LogProxy(Object target) { this.target target; } Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { long start System.currentTimeMillis(); Object result method.invoke(target, args); System.out.println(方法执行耗时: (System.currentTimeMillis() - start)); return result; } public static Object wrap(Object target) { return Proxy.newProxyInstance( target.getClass().getClassLoader(), target.getClass().getInterfaces(), new LogProxy(target) ); } }调用的时候OrderService service (OrderService) LogProxy.wrap(new OrderServiceImpl()); service.createOrder(request);代理模式在框架层面最常见的落地就是Spring AOP。切入点、通知、切面这套东西本质就是给目标对象生成一个代理在方法调用前后执行日志、权限、事务等逻辑。理解了动态代理的原理看Spring的AOP源码会轻松很多。有个高频面试题JDK动态代理为什么要求目标对象实现接口因为代理类是运行时动态生成的一个新类它通过反射实现目标接口来伪装成目标类型如果没有接口就没法生成这样的类。如果目标对象没有接口Spring会退回到CGLIB直接生成目标类的子类来重写方法。所以“JDK动态代理针对接口CGLIB针对类”这句话就是从这里来的。代理模式和装饰器模式外观很像经常被拿来对比。放一个简单区分表维度代理模式装饰器模式目的控制访问、加入横切逻辑增强原有功能对象创建代理类通常自行创建或由外部工厂传入目标对象装饰器由调用方把对象传进来使用方感知调用方不知道真实对象的存在调用方知道自己包了一层一句话体会代理关心“能不能让你调”装饰器关心“调之前之后还能加点什么”。3.2 适配器模式和装饰器模式的区分适配器模式解决的场景是接口不兼容。典型的例子是旧系统里有一个订单查询接口OldOrderQuery返回的是某种结构新系统里另一个服务需要NewOrderQuery接口返回的是另一种结构。直接改旧接口会影响所有老调用方让旧接口的实现去适配新接口也不行因为可能不止一个老实现。常规做法是写一个适配器类public class OrderQueryAdapter implements NewOrderQuery { private final OldOrderQuery oldOrderQuery; public OrderQueryAdapter(OldOrderQuery oldOrderQuery) { this.oldOrderQuery oldOrderQuery; } Override public NewOrderResult query(NewOrderRequest request) { OldOrderResult oldResult oldOrderQuery.query(convert(request)); return convert(oldResult); } }适配器的意义在于它把“老接口的数据如何映射成新接口”这个差异集中到了一个类里两边各自保持稳定。日常开发中遇到最多的适配器场景包括对接第三方服务商时为了保护业务层不依赖外部SDK、老系统接口和新系统接口过渡期共存、多种文件格式转换。和装饰器模式的区分点很直接适配器是为了让原本不兼容的两个接口能够对接接口和数据的形态都会变装饰器则保持原接口形态不变只增强方法行为。类名上有“Adapter”就是兼容层有“Decorator”或者“Wrapper”就是增强层。4. 行为型模式算法与交互如何解耦4.1 策略模式替换if-else的利器策略模式解决的是“同一件事有多种算法且算法之间可以相互替换”。典型场景是订单金额计算新用户有首单折扣会员有会员价大促有满减。如果把这些算法全堆在一个方法里方法里会充满if (isNewUser) ... else if (isMember) ...。每加一种营销活动这个方法的圈复杂度就上升一截测试用例也越写越痛苦。用策略模式重构之后变成这样public interface DiscountStrategy { BigDecimal calculate(BigDecimal amount, UserContext user); } public class NewUserDiscountStrategy implements DiscountStrategy { Override public BigDecimal calculate(BigDecimal amount, UserContext user) { return amount.multiply(new BigDecimal(0.9)); } } public class MemberDiscountStrategy implements DiscountStrategy { Override public BigDecimal calculate(BigDecimal amount, UserContext user) { return amount.multiply(new BigDecimal(0.85)); } }调用方持有策略的接口通过某种方式选定具体策略public class OrderPriceCalculator { private final DiscountStrategy strategy; public OrderPriceCalculator(DiscountStrategy strategy) { this.strategy strategy; } public BigDecimal calculate(BigDecimal amount, UserContext user) { return strategy.calculate(amount, user); } }业务代码变成了两行选策略执行策略。选策略的过程通常交给工厂模式这就出现了“工厂策略”的经典组合。策略模式最核心的价值不是消灭if-else消灭if-else只是表面现象真正的价值在于把“每一条业务规则”隔离成独立的类让它可单独测试、可单独维护。但是要注意如果一个场景只有两种互斥状态比如“有效订单”和“无效订单”用if-else往往比硬套策略模式更直观。策略模式不是万能药。4.2 观察者模式事件驱动的基本形态观察者模式解决的场景是“一个状态变化之后多个地方需要跟随动作”。比如订单创建成功之后要扣库存、发通知、加积分、更新统计。如果在核心的创建订单方法里挨个调用这些逻辑那么每一次新增动作都要改动创建订单的代码而且如果其中一个动作失败了还会影响主流程。观察者模式的标准写法是主题维护一个观察者列表状态变化时调用所有观察者的通知方法。public interface OrderEventListener { void onOrderCreated(OrderCreateEvent event); } public class OrderEventPublisher { private final ListOrderEventListener listeners new ArrayList(); public void registerListener(OrderEventListener listener) { listeners.add(listener); } public void publishOrderCreated(OrderCreateEvent event) { for (OrderEventListener listener : listeners) { listener.onOrderCreated(event); } } }库存服务、短信服务、积分服务各自实现OrderEventListener并注册进来。订单主流程里只需要eventPublisher.publishOrderCreated(event)这一行调用。以后新增“订单创建后发送优惠券”只需要再写一个监听器注册进来订单核心代码不需要改。这个模式是消息队列的“本地版”。本地的事件发布订阅适合微服务内部解耦等系统演进到多个服务都需要这个事件时再把OrderEventPublisher换成消息队列的Producer就好。Spring里用EventListener注解可以省掉手动注册监听器的代码它就是观察者模式在框架里的标准实现。观察者模式最容易忽略的问题是同步调用带来的性能瓶颈。如果注册了一堆慢监听器发布事件时主线程会被阻塞。常见解决方案是把事件发布放进线程池异步执行或者直接用消息队列。但异步会引入“发出事件后立刻看效果可能还没处理完”的一致性问题需要结合业务接受程度权衡。4.3 模板方法模式固定流程中的扩展点模板方法模式解决的是“多个流程的骨架相同只有某几个步骤不同”的场景。典型场景是订单处理流程校验参数、计算金额、保存订单、发送通知。大部分订单类型走同样的骨架但计算金额这一步各有各的算法。与其把整个流程复制粘贴到每个实现里不如把骨架放在抽象类中变化的部分留成抽象方法让子类实现。public abstract class OrderProcessorTemplate { public final void process(OrderContext context) { validate(context); BigDecimal amount calculateAmount(context); saveOrder(context, amount); notifyUser(context); } protected abstract BigDecimal calculateAmount(OrderContext context); protected abstract void notifyUser(OrderContext context); private void validate(OrderContext context) { // 通用校验逻辑 } private void saveOrder(OrderContext context, BigDecimal amount) { // 通用保存逻辑 } }子类只需要实现自己不同的那两步。模板方法模式特别适合那些“流程稳定但步骤细节经常变”的业务把变化收敛到扩展点里骨架代码稳定子类职责清晰。和策略模式的区别在于模板方法控制着完整的流程骨架包含多个步骤策略模式只解决“一个步骤里的算法选择”。模板方法更宏观策略更局部两者经常可以嵌套使用。5. 用一个订单系统把常用模式串起来5.1 场景设计与模式选型前面每种模式都是单独讲的真实项目里它们从来不是孤立出现。我做一个简洁的订单创建场景把上面这些模式串起来需求是这样的。电商平台收到创建订单的请求请求参数包括商品ID、数量、收货地址、优惠券ID。创建订单时系统需要判断订单类型普通订单走普通处理秒杀订单要额外做库存锁定还要根据用户身份计算折扣新用户9折、会员85折订单创建成功之后要扣库存、发通知、加积分。另外要求给核心服务加上方法执行日志。这个场景对应六个模式选型订单请求对象字段多选建造者模式构建OrderCreateRequest。订单类型的分发逻辑选工厂模式创建不同的OrderProcessor实现。金额折扣计算选策略模式不同策略类对应不同用户类型。订单创建成功后下游的多个动作选观察者模式发布事件。日志增强选代理模式包装核心服务。配置管理器保持全局唯一选单例模式。5.2 关键代码骨架先定义一个订单请求对象用建造者模式构建OrderCreateRequest request new OrderCreateRequest.Builder() .productId(P-1001) .quantity(2) .address(某小区某栋) .couponId(COUPON-2024) .build();工厂模式构建对应订单处理器public class OrderProcessorFactory { public static OrderProcessor create(String orderType) { if (seckill.equals(orderType)) { return new SeckillOrderProcessor(); } return new NormalOrderProcessor(); } }策略模式封装折扣计算DiscountStrategy strategy DiscountStrategyFactory.create(user); BigDecimal finalAmount strategy.calculate(originalAmount, user);观察者模式处理订单创建事件OrderEventPublisher publisher OrderEventPublisher.getInstance(); publisher.registerListener(new StockListener()); publisher.registerListener(new NotifyListener()); publisher.registerListener(new PointsListener()); OrderCreateEvent event new OrderCreateEvent(orderId, userId, finalAmount); publisher.publishOrderCreated(event);代理模式给订单服务加日志OrderService orderService (OrderService) LogProxy.wrap(new OrderServiceImpl()); orderService.createOrder(request);整个创建订单的主流程放在OrderServiceImpl里它不关心订单是哪种类型、折扣怎么算、下游要发什么通知只负责把前面几个组件的流程串起来。新增订单类型时加实现加工厂分支新增折扣策略时加策略类加策略工厂分支新增下游动作时加监听器。核心代码都不需要动。这就是组合使用模式带来的效果。5.3 如果不用这些模式会怎样为了直观感受模式的价值假设不用任何模式一个创建订单方法大概会长这样public void createOrder(MapString, Object params) { // 1. 从map里手动取出所有参数类型转换 String productId (String) params.get(productId); int quantity (int) params.get(quantity); // 2. 判断订单类型 if (normal.equals(orderType)) { // 扣库存、算价、保存 } else if (seckill.equals(orderType)) { // 扣库存、锁定库存、算价、保存 } // 3. 判断用户类型算折扣 if (isNewUser) { // 9折 } else if (isMember) { // 85折 } // 4. 保存、发短信、加积分... }这个方法在第一次写的时候很省事但接下来每个需求都会往方法里加一段代码。过半年这个方法可能有几百行新同学接手时根本不敢动。所谓技术债通常就是这么一点点累积起来的。使用模式组合之后新增需求这件事的改动路径变得清晰且有边界每个新需求都能在不影响旧功能的情况下接入。这就是“向代码腐化说不”的底气。6. 常见问题与排查心得6.1 面试高频问题速查设计模式是面试常客把下面这些问题理一遍基本能覆盖大多数面试场景问题回答要点单例模式的线程安全怎么保证静态内部类、枚举、双重检查锁双重检查锁为什么加volatile工厂模式解决了什么问题创建逻辑集中新增产品类时尽量不修改业务调用方策略模式和状态模式什么区别策略是外部主动选择算法状态是状态驱动行为切换代理模式和装饰器模式的区别代理强调控制访问装饰器强调增强功能JDK动态代理为什么只能代理接口动态代理类通过反射实现接口来伪装没接口就无从实现观察者模式同步通知阻塞怎么办异步化、消息队列、批量合并事件6.2 实际项目中容易踩的坑第一个坑过度设计。很多文章和课程为了讲透设计模式会把一个本来很简单的需求强行抽象出接口、工厂、策略、模板四层。我自己见过一个商城项目总共就三种商品状态硬是写了六个策略类加一个策略工厂。策略模式确实消灭了if-else但带来了类爆炸和阅读困难。判断标准可以很简单这个分支未来是否真的会频繁扩展如果一年可能才加一种状态if-else显然更实用。第二个坑单例模式里的可变状态。单例对象如果存了可变成员变量就相当于全局变量。A线程改了状态B线程读到的值就变了。之前排查过一个诡异问题某个运维系统多线程并发时配置时好时坏最后定位到就是一个单例对象里存了用户上下文。正确做法是单例只放不变的无状态依赖需要传上下文时通过方法参数传。第三个坑工厂模式一层套一层。工厂本来是为了收拢创建逻辑但有些项目里先有工厂接口再有两个抽象工厂再是具体工厂最后调用方根本不知道到底该注入哪个工厂。对一个业务复杂度不高的系统来说一个静态工厂方法直接干活往往就够不需要先定义工厂接口再去实现。第四个坑观察者事件的异常处理。监听器里任何一个抛出异常如果发布器没有捕获后面的监听器可能都不会执行。库存扣减成功了但通知服务抛异常导致积分没加这种问题特别隐蔽。发布事件时一定要考虑异常隔离单个监听器失败不能影响其他监听器。第五个坑策略类和Spring联用时误存状态。如果用Spring管理策略Bean默认是单例的那策略类里就不能放“当前用户”之类的成员变量否则并发用户之间会互相串数据。策略类应当设计成无状态的用户信息通过方法参数传进来。6.3 避坑清单先从重构已有代码开始练习不要在新项目刚起步时就铺设计模式。需求还没稳定之前抽象往往是空中楼阁。一个类职责不清楚时优先拆类别急着上策略和代理。设计模式是结构上的调整不是职责混乱的遮羞布。遇到“要不要用模式”的犹豫时默认先别上等第二次面对类似变动时再抽。这叫作“发现重复之后再抽象”比“提前猜测需求”要稳。看框架源码时不要只盯着某个类用了什么模式要看它为什么要在这个地方用模式。这个“为什么”比模式本身的UML图重要得多。我个人在实际项目里的体会是设计模式最终拼的是代码品味而写漂亮的代码最好的方式就是自己写一遍。那个订单系统例子就是一个很好的起点。如果把文中的代码自己敲一遍然后试着加一种“预售订单”类型再加一种“VIP用户折扣策略”再注册一个“订单创建后给推荐人发佣金”的监听器你会真实感觉到新旧代码的区别。设计模式不是背会的是踩坑之后学会的。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →