ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

设计模式新解:从Java经典实现到多Agent主从模式的AI落地

设计模式新解:从Java经典实现到多Agent主从模式的AI落地 聊到设计模式很多人的第一反应是“这老古董到底还有没有用”。尤其这两年AI辅助编程工具越来越多简单业务场景下让工具直接生成代码确实省事设计模式这种需要动脑子思考的东西好像越来越没存在感。但我最近在做多Agent系统开发时反而发现那些被不少人嫌弃的“经典设计模式”正以各种变形重新回到核心位置——比如热搜里提到的“主从模式”本质上就是把子Agent当作一种特殊的Tool来调用背后其实就是组合、委派、门面这些老模式在现代场景下的新应用。这篇文章我会从设计模式的本源讲起结合真实的Java实现代码再聊到智能体设计、多Agent协作这些新场景中模式怎么“变形”落地。不管你是准备设计模式期末考试、做设计模式大作业还是正在研究智能体设计模式PDF资料、想搞懂Java/C里的设计模式这篇都值得你花十分钟静下心看完。1. 设计模式到底是什么——先解决“为什么学它”的问题1.1 设计模式不是代码是“经验的抽象”我见过太多人一上来就背模式的名字和类图结果真到项目里完全想不起来用或者反过来乱用一通。说白了设计模式不是某段具体代码它是一类问题的“标准解法模板”。就像厨师做菜菜谱千变万化但“先热油、再下葱姜蒜爆香”这个思路是通用的设计模式就是软件世界里的“烹饪套路”。打个比方你第一次写支付对接可能直接在一个Service里堆if-else今天加支付宝明天加微信后天加银联每次上线都提心吊胆。等你写多了你会发现这类“多种实现可替换”的问题有一个固定的拆解思路——把变化的部分抽出来定义一个统一接口然后让不同实现去对接调用方只认接口。这个思路被总结成文档、画成类图、起了名字就是“策略模式”。所以学设计模式学的不是那张类图而是“识别问题本质”的能力。当你看到“多种算法/行为可替换”时能条件反射地想到策略模式当你看到“全局只能有一个实例”时能瞬间想起单例模式——这才是学习设计模式真正要达成的东西。1.2 三大分类创建型、结构型、行为型GoF那本经典书把23种模式分成三类这个分类本身就是一种学习地图创建型模式Creational解决“对象怎么创建”的问题。核心思想是“别直接用new把创建逻辑封装起来”。典型代表单例、工厂方法、抽象工厂、建造者、原型。结构型模式Structural解决“类和对象怎么组合”的问题。核心思想是“通过组合/继承把小的结构拼成大的结构”。典型代表适配器、装饰器、代理、外观、组合、桥接。行为型模式Behavioral解决“对象之间怎么协作”的问题。核心思想是“把对象间的通信、职责分配、算法封装起来”。典型代表策略、观察者、模板方法、迭代器、责任链、状态。我个人的学习建议是不要按书的顺序从头看到尾那会非常枯燥。按“使用频率”来学优先学单例、工厂、策略、观察者、装饰器这五个它们覆盖了绝大多数日常开发场景。把这三个分类理解成三个维度创建是“出生”的问题结构是“长相”的问题行为是“相处”的问题理解起来会轻松很多。1.3 学习设计模式的最佳姿势带着问题学而不是带着模式学很多初学者是反着来的——先学会了某个模式然后满世界找地方套。比如学了装饰器模式恨不得每个类都包一层学了观察者模式所有模块之间都用事件通信。这种“手里拿着锤子看什么都像钉子”的做法比不学模式还可怕。正确姿势是先遇到问题再去找模式。具体分三步写代码时遇到“坏味道”比如一个方法巨长、if-else堆成山、类与类之间耦合到改一处崩三处停下来问自己这个问题的本质是什么是“创建逻辑太复杂”还是“行为可替换”还是“对象间通信混乱”带着问题去翻模式分类找到对应解法理解它的思路后再去重构。所以这篇文章后面每一章我都会坚持“问题 → 思路 → 代码 → 适用场景”的写法而不是直接甩一堆类图。这样你学完才能真正用得上。2. 五个高频设计模式全解析含Java代码示例2.1 单例模式Singleton全局唯一的“办事窗口”问题场景配置文件读取类、数据库连接池、线程池、日志管理器。这些对象如果每次用都new一个会造成资源浪费甚至数据不一致。比如读取配置文件的类如果实例化了多个每个实例各自缓存一份配置其中一个实例被修改了配置另一个还蒙在鼓里查问题的时候能把人逼疯。核心思路类的构造方法私有化外部不能直接new类自己维护唯一实例提供全局访问点所有调用方拿到的都是同一个对象。public class ConfigManager { // volatile 防止指令重排双重检查锁定的关键 private static volatile ConfigManager instance; private MapString, String configMap; // 构造方法私有外部无法 new private ConfigManager() { configMap new HashMap(); // 模拟从配置文件加载 configMap.put(app.name, MyApp); configMap.put(app.version, 1.0.0); } public static ConfigManager getInstance() { if (instance null) { synchronized (ConfigManager.class) { if (instance null) { instance new ConfigManager(); } } } return instance; } public String getConfig(String key) { return configMap.get(key); } public void setConfig(String key, String value) { configMap.put(key, value); } }这段代码里有个经典考点“双重检查锁定”Double-Checked Locking。先看实例为不为空不为空直接返回避免每次都要抢锁为空的线程进入同步块里面再查一次防止多个线程同时通过第一层检查后各自new一个实例。volatile关键字是为了防止instance new ConfigManager()这一步发生指令重排在极端情况下其他线程拿到一个“半初始化”的对象。使用注意单例模式虽然简单但滥用会让单元测试变得很痛苦。因为单例的全局状态没法轻松mock测试之间还会互相污染。后来流行的依赖注入IoC容器本质上就是在“帮你管理单例的生命周期”而不是让你到处写getInstance()。2.2 工厂模式Factory把“new”这件事集中管理问题场景业务代码里到处散落着new一旦实现类变了所有地方都要改。比如支付场景你对接了支付宝、微信、银联如果每次都在业务代码里写if (type.equals(ali)) return new AliPay();那每加一个支付方式所有涉及支付的地方都要动一遍。核心思路把“创建哪个对象”的决策逻辑收敛到一个独立的工厂类/方法里。调用方只告诉工厂“我要什么类型”工厂负责创建并返回具体对象。public interface PaymentChannel { void pay(double amount); } public class AliPay implements PaymentChannel { Override public void pay(double amount) { System.out.println(通过支付宝支付 amount 元); } } public class WeChatPay implements PaymentChannel { Override public void pay(double amount) { System.out.println(通过微信支付 amount 元); } } public class PaymentFactory { public static PaymentChannel create(String channelType) { if (ali.equalsIgnoreCase(channelType)) { return new AliPay(); } if (wechat.equalsIgnoreCase(channelType)) { return new WeChatPay(); } throw new IllegalArgumentException(不支持的支付渠道 channelType); } } // 调用方 PaymentChannel channel PaymentFactory.create(ali); channel.pay(99.0);这一段代码写出来很多人会质疑这不就是把if-else从业务代码挪到工厂里了吗好处在哪好处在于“变动的收敛”。新增一种支付方式时你只需要改工厂这一个地方调用方的代码一行都不用动。而且调用方从“依赖具体类”变成了“依赖接口”这是依赖倒置原则的体现。更进一步的实践中工厂方法往往会配合配置文件或者注册表把类型和实现类的映射关系配置化连工厂代码都不用改这就是Spring的BeanFactory干的事情。2.3 策略模式Strategy消灭“神仙打架”的if-else问题场景同一个行为有多种不同的算法/规则而且运行时要动态切换。比如电商订单的优惠计算新用户首单减20满300减50VIP打8折不同活动之间还能叠加。如果你用if-else写每加一个活动就要往方法里塞一个分支方法越来越长改一个分支还容易影响别的分支。核心思路定义一组算法把它们各自封装成独立的类并且可以互相替换。调用方持有一个策略接口引用运行时传入具体策略。public interface DiscountStrategy { double calculate(double originalPrice); } // 新用户立减 public class NewUserDiscount implements DiscountStrategy { private static final double REDUCTION 20.0; Override public double calculate(double originalPrice) { return Math.max(0, originalPrice - REDUCTION); } } // 满减 public class FullReductionDiscount implements DiscountStrategy { private final double threshold; private final double reduction; public FullReductionDiscount(double threshold, double reduction) { this.threshold threshold; this.reduction reduction; } Override public double calculate(double originalPrice) { if (originalPrice threshold) { return originalPrice - reduction; } return originalPrice; } } // VIP折扣 public class VipDiscount implements DiscountStrategy { private final double rate; public VipDiscount(double rate) { this.rate rate; } Override public double calculate(double originalPrice) { return originalPrice * rate; } }在订单服务里你想用哪个策略就传哪个public class OrderService { public double calculateFinalPrice(double originalPrice, DiscountStrategy strategy) { return strategy.calculate(originalPrice); } }策略模式最大的价值不是“少写几个if-else”而是把每个算法隔离成了独立单元互不干扰。每个策略类都可以单独测试可以单独复用。想加一个新活动新增一个策略类就行完全不需要动现有代码完美符合开闭原则。实际心得策略模式配合枚举用起来更顺手。把枚举作为策略的“注册表”每个枚举项关联一个策略实现调用时直接用枚举映射既避免了if-else又方便维护。2.4 观察者模式Observer让“事件”驱动系统解耦问题场景某个对象状态变化时需要通知一堆其他对象做出响应。典型场景下单成功后要发短信、发邮件、推送App通知、更新库存、记日志。如果硬编码在订单Service里每加一个“下单后要做的事”都要改动订单Service的代码今天加短信明天加积分后天加优惠券订单类被越改越庞大。核心思路定义一对多的依赖关系一个被观察者Subject维护多个观察者Observer当被观察者状态变化时自动通知所有观察者。public interface OrderEventListener { void onOrderPaid(Order order); } public class SmsNotifier implements OrderEventListener { Override public void onOrderPaid(Order order) { System.out.println(发送短信通知用户 order.getUserPhone()); } } public class StockUpdater implements OrderEventListener { Override public void onOrderPaid(Order order) { System.out.println(扣减商品库存 order.getProductId()); } } public class OrderService { private ListOrderEventListener listeners new ArrayList(); public void registerListener(OrderEventListener listener) { listeners.add(listener); } public void unregisterListener(OrderEventListener listener) { listeners.remove(listener); } public void payOrder(Order order) { // 核心支付逻辑... System.out.println(订单支付成功 order.getOrderId()); // 通知所有观察者 for (OrderEventListener listener : listeners) { listener.onOrderPaid(order); } } }这样一来下单后要发什么通知、做什么后续动作完全是“插拔式”的。新业务方只需要实现OrderEventListener接口然后在初始化时注册进来即可订单核心逻辑一行都不用改。容易踩的坑观察者模式最大的隐患是“回调顺序”和“异常隔离”。如果某个观察者的逻辑崩溃了会不会影响后面的观察者所以在真实项目里通知代码通常会加try-catch或者使用异步事件总线不让一个观察者的失败拖垮整个主流程。后面我会在问题排查章节详细说这个。2.5 装饰器模式Decorator给对象“叠加BUFF”问题场景你想给一个对象增加功能但又不想修改它原来的代码也不想通过继承去造一堆子类。最经典的例子是Java I/O流BufferedReader包装了FileReader给文件读取加上了缓冲功能LineNumberReader再包装BufferedReader又加上了行号功能。如果每种组合都用继承那类数量会爆炸。核心思路装饰器和被装饰对象实现同一个接口装饰器内部持有一个被装饰对象在调用被装饰对象方法的前后添加额外的行为。public interface DataSource { void write(String data); String read(); } public class FileDataSource implements DataSource { private String filename; public FileDataSource(String filename) { this.filename filename; } Override public void write(String data) { System.out.println(写入文件 filename data); } Override public String read() { return 文件内容; } } // 加密装饰器 public class EncryptedDataSource implements DataSource { private final DataSource wrapper; public EncryptedDataSource(DataSource wrapper) { this.wrapper wrapper; } Override public void write(String data) { String encrypted encrypted( data ); wrapper.write(encrypted); } Override public String read() { String data wrapper.read(); return decrypted( data ); } } // 压缩装饰器 public class CompressedDataSource implements DataSource { private final DataSource wrapper; public CompressedDataSource(DataSource wrapper) { this.wrapper wrapper; } Override public void write(String data) { wrapper.write(compressed( data )); } Override public String read() { return decompressed( wrapper.read() ); } } // 使用 DataSource source new FileDataSource(test.txt); // 先压缩再加密 DataSource compressed new CompressedDataSource(source); DataSource encrypted new EncryptedDataSource(compressed); encrypted.write(Hello World);注意装饰器的嵌套顺序很关键上面这段代码从外到内是加密 → 压缩 → 文件。所以写入时先加密处理数据然后把加密结果交给压缩装饰器压缩后再写入文件。读取时流程正好反过来。和继承的对比继承是静态的编译期就确定了增强逻辑装饰器是动态的运行时可以自由组合。但装饰器有个缺点——会引入大量小类调试时整个调用链很长一层套一层排查问题会比较费劲。当装饰层级超过三层我建议考虑是否该重新设计了。3. 设计模式在智能体开发中的新应用——老树开新花3.1 主从模式底层就是组合模式加委派模式最近“主从模式”这个热词在Agent开发圈子里反复出现。简单说就是一个主AgentMaster负责拆解任务、调度多个子AgentSubAgent各司其职执行具体子任务。这本质上就是“组合模式”Composite加上“委派模式”Delegate在AI领域的一次华丽转身。组合模式的核心思想是“部分-整体”的树形结构客户端可以像处理单个对象一样处理组合对象。主Agent就是树干子Agent就是树枝树叶。对外暴露的时候你只需要跟主Agent对话它内部维护着一个子Agent列表根据任务类型动态选择让哪个子Agent干活。再看“子Agent作为Tool调用”这个观点很多人觉得这是新鲜概念其实这就是“门面模式”Facade的AI变体。门面模式要求为子系统提供统一入口让客户端不用关心子系统内部有多少模块。在主从Agent设计中主Agent就是一个门面你把子Agent包装成Tool的接口形式调用方根本不用关心“这到底是一个模型函数还是一个拥有独立思考能力的子Agent”反正都是输入参数、返回结果。3.2 把SubAgent当作Tool调用——适配器模式的新战场主流Agent框架里Tool的抽象通常是一个函数签名接受一个JSON字符串参数返回一个JSON字符串结果。如果你想把一个子Agent暴露成Tool给别人调用就需要写一个适配器Adapter把Agent的对话接口转换成Tool的标准接口。这个适配器做的事情包括把Tool接收的入参转换成子Agent能理解的System Prompt或者初始消息把子Agent的多轮对话输出收敛成一个最终结果字符串处理超时、重试、异常情况必要时记录调用链方便追踪。class SubAgentToolAdapter: 把子Agent包装成标准Tool的适配器。 对上层来说这只是一个可调用的函数 对子Agent来说它只是收到了一个任务请求。 def __init__(self, sub_agent, max_retries3): self.sub_agent sub_agent self.max_retries max_retries def execute(self, params_json: str) - str: # 转换入参为Agent可理解的格式 task_prompt self._convert_to_prompt(params_json) for attempt in range(self.max_retries): try: # 调用子Agent获取最终结果 result self.sub_agent.run(task_prompt) return self._convert_to_result(result) except TimeoutError: continue return json.dumps({status: failed, error: sub_agent timeout}) def _convert_to_prompt(self, params_json): return f你是一个专门处理子任务的Agent请根据以下参数完成任务{params_json} def _convert_to_result(self, agent_output): return json.dumps({status: success, result: agent_output})这段代码我建议所有做Agent开发的都好好体会一下。适配器模式在这里干的活和当年做Android适配、做第三方SDK接入时干的活没有任何本质区别——把不兼容的接口转换成目标系统期望的接口。模式还是那个模式只是换了身赛博皮肤。3.3 多Agent协作里的观察者模式与事件驱动多Agent系统里Agent之间不是所有时候都要同步调用。比如一个Agent在等待另一个Agent的结果时可以先去干别的活等结果出来了再回来处理。这种“异步协作”用到的就是观察者模式的升级版——事件总线。每个Agent可以订阅它感兴趣的事件类型比如“任务完成”“检索到新资料”“用户中断”。当某个Agent完成了自己的工作它往事件总线里发布一条事件其他订阅了该事件的Agent就会被异步唤醒。这和前面订单服务的观察者模式逻辑一模一样只是传播介质从进程内的方法调用变成了消息队列。我当时做多Agent编排的时候就把消息总线设计成了组合模式一个事件可以拆成多个子事件也可以合并多个子事件的结果。单一事件订阅是叶子节点组合事件订阅是树枝节点。这种设计让整个编排系统非常灵活新加一种Agent协作方式基本就是加几个订阅关系的事。所以你看设计模式的价值不在于“背下来”而在于你能不能在遇到新问题时认出它“旧相识”的本质。主从、Tool化、事件驱动这些听起来高大上的Agent概念骨子里还是老朋友。4. 设计模式怎么选——避免从“不用模式”走到“乱用模式”4.1 判断标准每次纠结时问自己三个问题我在设计模式大作业评审和技术评审中经常看到一种情况代码里堆了一堆模式但没人说得清为什么用。这里分享一个我自己用了很多年的判断框架遇到“要不要上某个模式”的纠结时先问三个问题变化的频率有多高如果这个维度几乎不变比如系统里就一种支付方式那工厂模式纯属多余直接new就完事了。模式是为“变化”服务的没有变化就没有模式的用武之地。调用方是否需要感知具体实现如果所有调用方都只关心接口行为不关心具体类型那策略模式、工厂模式就值得考虑。反之如果调用方本来就依赖具体类硬抽接口反而是画蛇添足。对象间的协作是同步还是异步如果A发生变化后B必须立刻同步响应那观察者模式要考虑好时序和异常隔离。如果允许异步那事件总线更合适。这三个问题问完90%的“要不要用模式”之争都能落地。4.2 常见误用把简单问题复杂化比如网上特别火的“用策略模式消灭if-else”这类文章我持保留态度。如果你的分支只有两三个而且一两年都不见得新增一个那if-else完全没问题可读性甚至更好。为了消灭if-else而搞出五六个类、一个工厂、一堆测试这叫“过度设计”。另一种典型误用是“观察者模式遍地开花”。我曾经维护过一个系统团队把所有的模块间通信都改成了事件驱动结果全局搜索某个对象在哪些地方被修改根本搜不到因为都通过事件广播出去了调用链完全不可见。排查一个问题要在十几个事件的订阅关系里跳来跳去。观察者模式适合“一对多”“多对一”的解耦但不是所有依赖都应该解耦有些直接调用的代码清晰程度远高于事件广播。还有一类误用是“适配器模式乱wrap”。框架返回的对象本来直接能用有人非要包一层自己的适配器美其名曰“隔离第三方依赖”。结果是每次框架升级适配器代码要跟着改平白无故多了一层维护成本。适配器模式用在不兼容的接口对接上不是为了“以防以后要换框架”这种虚构的假设服务的。4.3 什么时候真的不用模式这里给大家吃个定心丸很多项目根本不需要刻意使用设计模式。单体小系统、内部工具、脚本、原型验证直接用最简单直白的写法反而最高效。模式是给“复杂度”准备的武器不是给“面子”准备的装饰。我个人的经验线是代码不超过几千行、改动频率极低 → 不用模式怎么简单怎么来某个维度确认会持续变化比如支付渠道会持续增加→ 针对这个维度上模式其余保持简单团队协作人数多、模块边界清晰 → 在模块边界上使用模式模块内部保持简单。模式的选择和架构分层一样都是在“管理复杂度”而不是“展示技术”。5. 真实项目中的避坑经验与问题排查实录5.1 单例模式的线程安全问题前面代码里用了volatile加双重检查但在实际项目中我最推荐的方式不是自己写单例而是用枚举或者静态内部类。枚举单例是《Effective Java》作者推荐的写法代码简洁而且天然防止反序列化破坏单例。public enum ConfigManager { INSTANCE; private MapString, String configMap new HashMap(); public String getConfig(String key) { return configMap.get(key); } }这段代码相比手写双检锁不仅短而且线程安全机制由JVM保证。除非你需要继承某个类枚举不能继承否则优先用枚举单例。这是很多设计模式教程里不会强调的实战细节。5.2 观察者模式的异常隔离与内存泄漏我在订单系统里用观察者模式时遇到过一个问题某个观察者抛了异常导致整个payOrder事务回滚用户钱扣了但订单状态没更新客诉电话直接爆掉。后来所有通知逻辑都加了try-catch并且把短信、邮件这些非核心通知改成了异步执行。还有内存泄漏问题。如果你把一个观察者注册到全局的事件管理中心但忘记在对象销毁时调用unregisterListener那这个对象永远被事件中心持有垃圾回收器无法回收它。这就是“隐性内存泄漏”。在Web应用里Spring生命周期管理的Bean通常没问题但如果你手动new了一个观察者并注册进去用完后一定要记得反注册。5.3 策略模式与if-else的边界策略模式的注册表方式也踩过坑。我见过一种写法把策略实例放进Mapkey是字符串类型用Spring的Component自动注入。听着很优雅但一旦策略多了定位“哪个策略生效”就变成了查Map的Key在代码里全局搜字符串调试体验很差。我的建议是策略的编号都用枚举表示不要用裸字符串。这样编译器能帮你检查IDE能帮你跳转重构的时候也安全得多。枚举加上对应的策略字段一个枚举项对应一个策略实现查找和匹配一目了然。public enum DiscountType { NEW_USER(new NewUserDiscount()), FULL_REDUCTION(new FullReductionDiscount(300, 50)), VIP(new VipDiscount(0.8)); private final DiscountStrategy strategy; DiscountType(DiscountStrategy strategy) { this.strategy strategy; } public DiscountStrategy getStrategy() { return strategy; } }5.4 快速排查清单最后整理一个我在设计模式评审、代码复查时常用的自查表分享出来给大家参考检查项可能的问题自查方法单例是否线程安全懒加载未加锁或未用volatile双检锁是否用了volatile是否可用枚举替代观察者是否异常隔离一个观察者崩溃影响主流程通知逻辑是否有try-catch是否异步化观察者是否反注册长期运行导致内存泄漏对象销毁路径是否调用unregister工厂是否收敛调用方仍散落new具体类全局搜索关键实现类的new关键字策略是否可扩展新策略需要改动旧代码新增策略是否只需新增类不改动调用方装饰器层级是否合理嵌套过深调用链难以追踪装饰层数是否超过3层能否用AOP替代把这些检查项当成代码评审的“体检表”每次提交前过一遍能省下不少线上事故的排查时间。聊到这儿设计模式的“老”与“新”其实已经打通了。我做多Agent系统时最大的体会是技术框架、热词会一直变但“如何管理复杂度”这件事几十年来内核一直没有变。你去看那些被称赞“代码写得好”的项目大概率不是因为它用了多少花哨模式而是它在正确的地方用了恰到好处的模式把复杂问题拆成了人能理解的结构。如果你正在学设计模式我的建议很简单把这篇文章里的五个模式先跑通再去找个真实项目里“代码坏味道”明显的地方尝试用对应模式重构一遍。踩过坑、动过手这些模式才真正算是你的。
返回列表