news 2026/9/24 22:35:25

从继承到装饰器:Java通知模块重构实战,告别组合爆炸

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
从继承到装饰器:Java通知模块重构实战,告别组合爆炸

大概三年前的某个深夜,我盯着项目里那十七个以Notify开头的类,第一次认真琢磨装饰器模式(Decorator Pattern)到底能救多少代码。当时那是一个消息通知模块,需求方从“先发个短信就行”一路加码到“短信邮件站内信都要、要记录日志、要失败重试、要延迟到上班时间再发”,短短三周,类越来越多,改一个逻辑要牵连七八个文件。后来我用装饰器模式把整块重写,类数量直接砍掉一半,后面再加新增强,一行旧代码都不用动。这篇文章就把我从踩坑到用顺手的全过程记录下来,适合那些正在被“组合爆炸”折磨、或者刚接触装饰器模式想搞懂它到底怎么用的同学。

1. 先还原那个让我决定重写的需求现场

1.1 三周内需求变了四次的“简单通知功能”

故事起点特别朴素:运营那边说“用户下单后发一条短信通知就行”。我当时的实现也特别朴素——一个SmsNotify类,一个send()方法,里面拼好文案调短信服务商的接口,前后十分钟搞定。

结果第二周需求就来了:“短信之外,还要发邮件和站内信。” OK,这也不算难,我抽出接口MessageSender,让短信、邮件、站内信三个实现类各自干活,调用方根据场景选择渠道。到这里为止,代码还挺干净。

第三周开始不对劲了。先是“每次发送都要把时间、渠道、结果记录下来,方便排查”;过了两天又说“部分用户收到的通知内容要脱敏,不能明文存日志”;再然后“晚上下单的邮件,别半夜打扰用户,延迟到第二天上午9点再发”;最后是“发送失败要自动重试三次”。每一个需求单独拿出来都不难,但合在一起,事情就变味了。

1.2 继承方案在第几次改动后彻底失控

我当时的第一反应是写个子类。SmsNotify要加日志?写一个SmsNotifyWithLog。邮件要加日志?再写一个EmailNotifyWithLog。短信要日志又要重试?那就SmsNotifyWithLogAndRetry。第三周结束时,项目里躺着下面这种表:

基础渠道增强能力组合继承方案需要的类
短信 / 邮件 / 站内信日志3 个
短信 / 邮件 / 站内信日志 + 重试3 个
短信 / 邮件 / 站内信日志 + 加密3 个
短信 / 邮件 / 站内信日志 + 重试 + 加密 + 延迟3 个

表面看只有十几个类,但继续往下想就害怕了:如果基础渠道数量是 m,可选增强能力数量是 n,用继承实现所有组合,最坏情况要写m × 2ⁿ个类。3 个渠道、4 种增强,就是 3 × 16 = 48 个类,而且假设你后面又加了第 5 种增强“限流”,类数量直接翻倍到 96 个。

更难受的不是数量,是改代码。日志逻辑要从短信类复制到邮件类再复制到站内信类,每次改日志格式要全局搜索替换,漏一个就是线上事故。继承在这个场景下的问题不是“不够用”,而是“结构性不可维护”——增强能力和具体实体强耦合,加一种新增强,等于把所有相关渠道类都动一遍,这恰恰违反了开闭原则。

1.3 第一版重写:把“增强”从“实体”里拆出来

熬夜重构那晚,我想明白了一件事:短信、邮件、站内信这些“实体”其实很稳定,会变的是“增强行为”——日志、加密、延迟、重试。那为什么不反过来设计?

实体保持单纯,增强行为独立成类,然后用“一层包一层”的方式叠加。这就是装饰器模式最朴素的思想:我不修改短信发送这个动作本身,而是给它外面套一个“日志外壳”,再套一个“重试外壳”,套完之后对外仍然是一个MessageSender,调用方完全感知不到里面的结构变化。

当时我还没意识到,这个“套壳后类型不变”的特性,是整个模式最精妙的地方,也是后面所有坑的根源。带着这个思路,我正式梳理了装饰器模式的标准结构。

2. 装饰器模式的四梁八柱:角色、结构与代码骨架

2.1 四个角色的职责分配

装饰器模式一共四个角色,少一个都不完整:

  • 抽象构件(Component):定义业务接口,是所有参与者的共同契约。在通知例子里就是MessageSender
  • 具体构件(ConcreteComponent):真正干活的类,也就是被装饰的原始对象,如SmsSender
  • 抽象装饰器(Decorator):持有一个 Component 类型的引用,并实现和 Component 相同的接口。它存在的意义是让“装饰器”这个类型能和“构件”互相替换。
  • 具体装饰器(ConcreteDecorator):在转发调用给内部 target 的前后,插入自己的增强逻辑,如LoggingDecoratorRetryDecorator

这四个角色最关键的是第三和第四个:抽象装饰器保证了“接口不变”,具体装饰器保证了“行为增强”。两者缺一不可,少了抽象装饰器,调用方就得区分“外面这层是装饰器还是本体”,类型透明就没了。

2.2 先跑通最小骨架(附完整 Java 代码)

用通知场景写一个最小可运行的骨架,代码量不大,但它把四个角色的关系展示得很清楚。

// 抽象构件:定义稳定接口 public interface MessageSender { void send(String message); } // 具体构件:真正发短信的类 public class SmsSender implements MessageSender { @Override public void send(String message) { System.out.println("[短信渠道] 发送: " + message); } } // 抽象装饰器:持有一个 MessageSender 引用,接口与构件一致 public abstract class MessageSenderDecorator implements MessageSender { protected MessageSender target; public MessageSenderDecorator(MessageSender target) { this.target = target; } @Override public void send(String message) { target.send(message); } } // 具体装饰器 A:日志 public class LoggingDecorator extends MessageSenderDecorator { public LoggingDecorator(MessageSender target) { super(target); } @Override public void send(String message) { System.out.println("[日志] " + LocalDateTime.now() + " 开始发送"); target.send(message); System.out.println("[日志] 发送结束"); } } // 具体装饰器 B:加密 public class EncryptDecorator extends MessageSenderDecorator { public EncryptDecorator(MessageSender target) { super(target); } @Override public void send(String message) { String encrypted = "【密文】" + message + "【END】"; target.send(encrypted); } }

使用的时候就是套娃式构造:

MessageSender sender = new LoggingDecorator(new EncryptDecorator(new SmsSender())); sender.send("您好,您的验证码是 123456");

输出顺序应该是先打日志“开始发送”,然后内部把消息加密后交给短信渠道发送,再打日志“发送结束”。这里有个值得注意的细节:LoggingDecorator在最外层,所以“开始发送”最先打印;EncryptDecorator在中间,所以传给短信渠道的内容已经变成了密文。这个顺序问题后面我会专门讲坑。

2.3 递归组合的本质:为什么装饰器能无限套娃

很多初学者问过我同一个问题:LoggingDecorator明明也是MessageSender,它内部持有的target也是MessageSender,那它调target.send()的时候,不还是调了个MessageSender的方法吗?到底怎么做到“层层增强”的?

答案是:装饰器组合本质就是一个递归调用链。每个装饰器的send()方法里,target.send(message)这一行调用,会在运行期被动态分发到它下一个内层对象的方法上。你从最外层调用send(),它会先执行自己的前置逻辑,然后调用内一层的send(),内一层再执行自己的前置逻辑……直到最里面真正的SmsSender.send()执行完,控制权再一层层往回返,执行各层的前置后置逻辑。

这个结构就像快递包裹:最外层纸箱拆开,里面还有个盒子,盒子里还有气泡膜,气泡膜里才是真东西。你拿到的始终是个“包裹”,要经过一层层拆解才触达核心。装饰器模式允许你在外面无限套壳,只要每一层都遵循同一个接口,套多少层都不影响调用方。

3. 实战:把通知功能升级成“全能外挂”

3.1 需求清单:日志、加密、延迟、重试

光看骨架不过瘾,我把当时真实落地的四个装饰器逐一写出来。先列需求:

  1. 日志:记录每次发送的时间、内容摘要、发送结果。
  2. 加密:对敏感通知内容做加密处理,防止日志泄露明文。
  3. 延迟:支持延迟到指定时间再发送,比如晚上下单的邮件推到次日 9 点。
  4. 重试:发送失败时自动重试,最多 3 次,间隔 2 秒。

这四个增强之间彼此独立,顺序不同效果不同,正好适合用装饰器来组织。

3.2 四个具体装饰器的实现要点

日志装饰器比较简单,重点说一下加密、延迟和重试这三个有“状态”的装饰器。

public class RetryDecorator extends MessageSenderDecorator { private final int maxAttempts; private final long intervalMillis; public RetryDecorator(MessageSender target, int maxAttempts, long intervalMillis) { super(target); this.maxAttempts = maxAttempts; this.intervalMillis = intervalMillis; } @Override public void send(String message) { for (int attempt = 1; attempt <= maxAttempts; attempt++) { try { target.send(message); return; } catch (Exception e) { if (attempt == maxAttempts) { throw e; // 最后一次失败直接抛出 } System.out.println("[重试] 第 " + attempt + " 次失败," + intervalMillis + "ms 后重试"); try { Thread.sleep(intervalMillis); } catch (InterruptedException ie) { Thread.currentThread().interrupt(); throw new RuntimeException("重试被中断", ie); } } } } }

这里有个容易忽略的细节:重试装饰器只在target.send()抛出异常时才重试。如果底层渠道发送失败但不抛异常、只返回 false,那return直接执行了,重试逻辑根本不会触发。所以使用重试装饰器时,一定要确认底层构件的失败语义是“异常抛出”还是“返回值标记”,否则装饰器形同虚设。

加密装饰器也一样,必须在构造时传入密钥,而且加密逻辑要尽量薄,不要把加解密算法写死在装饰器里:

public class EncryptDecorator extends MessageSenderDecorator { private final String secretKey; public EncryptDecorator(MessageSender target, String secretKey) { super(target); this.secretKey = secretKey; } @Override public void send(String message) { String encrypted = encrypt(message, secretKey); target.send(encrypted); } private String encrypt(String plaintext, String key) { // 实际项目中这里调用 AES 等加密工具类 // 为了演示,只做简单 Base64 编码 return Base64.getEncoder().encodeToString(plaintext.getBytes()); } }

延迟装饰器是最容易写错的一个。如果直接用Thread.sleep()阻塞当前线程,会把调用方线程也卡住。我当时在项目里用的是ScheduledExecutorService,让消息调度到延迟时间之后再真正发送:

public class DelayDecorator extends MessageSenderDecorator { private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(); private final Duration delay; public DelayDecorator(MessageSender target, Duration delay) { super(target); this.delay = delay; } @Override public void send(String message) { scheduler.schedule(() -> target.send(message), delay.toMillis(), TimeUnit.MILLISECONDS); } }

延迟装饰器改变了方法的同步语义——调用send()会立即返回,实际发送发生在调度线程里。这意味着外层如果有日志装饰器,日志里“发送结束”会先于真实发送发生,日志顺序就和实际执行顺序不一致了。解决办法是把延迟装饰器放在最内层,让它只延迟“真实发送”,不延迟增强逻辑的编排。

3.3 用静态工厂把装饰链收口

四个装饰器都写好了,如果你让业务代码自己new装饰链,用不了几天就会乱套——有人从头套到尾,有人从尾套到头,结果千奇百怪。我当时的做法是提供一个通知工厂,把常用的装饰链组合沉淀成方法:

public class NotificationFactory { public static MessageSender createSmsWithFullFeatures(String secretKey) { MessageSender sender = new SmsSender(); sender = new LoggingDecorator(sender); sender = new EncryptDecorator(sender, secretKey); sender = new DelayDecorator(sender, Duration.ofHours(8)); sender = new RetryDecorator(sender, 3, 2000); return sender; } public static MessageSender createEmailWithLoggingAndRetry() { MessageSender sender = new EmailSender(); sender = new LoggingDecorator(sender); sender = new RetryDecorator(sender, 3, 2000); return sender; } }

工厂方法把“装饰链如何组装”这件事集中管理起来,业务方只需要调用NotificationFactory.createSmsWithFullFeatures(key)拿到一个MessageSender,不知道也不关心里面套了几层。后续新增增强能力,也不改工厂方法的签名,只改内部组装逻辑。

3.4 验证:一次调用,整条链路如何协同

组装好的链子是:RetryDecorator -> DelayDecorator -> EncryptDecorator -> LoggingDecorator -> SmsSender,从外到里一层层包。

调用send("验证码 123456")后,执行流是这样的:

  1. RetryDecorator进入循环,尝试调下一层。
  2. DelayDecorator把任务提交给调度线程,主线程立即返回。
  3. 调度线程到点后调用EncryptDecorator,把“验证码 123456”加密成密文。
  4. LoggingDecorator打印“开始发送”,记录的是密文,不是明文,符合脱敏需求。
  5. SmsSender真正把密文发出去。
  6. 如果SmsSender抛异常,异常沿调用链冒泡回RetryDecorator,触发重试,整个过程再来一遍。

这个例子也说明了一个重要原则:装饰器链的顺序就是业务的执行顺序。你把加密放在日志外面,日志记的就是密文;把加密放在日志里面,日志记的就是明文。需求是“日志不许出现明文”,那就必须让EncryptDecorator包在LoggingDecorator外面。这也是为什么我强烈建议用工厂方法收口装饰链——一旦某个环境搞错了顺序,排查起来非常痛苦。

4. 你其实早就在用:框架与源码里的装饰器身影

4.1 Java I/O 流:最经典的教科书案例

很多设计模式的文章讲装饰器都以 Java I/O 流为例,因为这确实是标准库里最大规模的装饰器应用。看这行代码:

BufferedReader reader = new BufferedReader( new InputStreamReader( new FileInputStream("test.txt"), StandardCharsets.UTF_8));

FileInputStream是具体构件,BufferedReader是典型的具体装饰器——它给字符流增加了“缓冲”能力,但对外仍然是Reader类型。你可以继续往上套LineNumberReader,或者换一个构件,套在StringReader上,BufferedReader 内部不用改一行代码。

I/O 流家族里FilterInputStream的所有子类都是装饰器:BufferedInputStream加缓冲,DataInputStream加基本类型读取能力,PushbackInputStream加回退读取能力。你可以任意组合它们,这正是装饰器模式在真实项目里存活了几十年的证明。

4.2 Python 的 @ 语法背后发生了什么

Python 的装饰器语法是装饰器模式的一种语言级实现,但它加了语法糖之后,很多人反而没意识到这玩意儿和 Java 手写装饰器是同一个思想。

import functools import time def timing(func): @functools.wraps(func) def wrapper(*args, **kwargs): start = time.perf_counter() result = func(*args, **kwargs) print(f"{func.__name__} 耗时 {time.perf_counter() - start:.4f}s") return result return wrapper @timing def send_message(text): time.sleep(1) print(f"发送: {text}")

@timing等同于send_message = timing(send_message),本质就是把原函数包进一个外壳函数,外壳保留了原函数的签名和行为,再附加计时逻辑。这里有个新手必踩的坑:如果不加@functools.wraps(func),装饰后的函数__name__会变成wrapper,依赖函数名做日志或路由判断的地方会出诡异问题。这和 Java 装饰器导致getClass()返回装饰器类名是同一个坑——装饰会掩盖身份信息,需要显式保留。

4.3 Web 中间件与 Spring AOP:改名换姓的装饰器

如果你写过 Koa 或 Express,应该对下面这种中间件链不陌生:

app.use(loggingMiddleware); app.use(authMiddleware); app.use(rateLimitMiddleware); app.use(handler);

每个中间件接收(req, res, next),在调用next()前后执行自己的逻辑,请求一层层穿透中间件,到达真正的业务处理函数。这就是装饰器模式在 Web 框架里的变体——通道换成了next回调,增强方式依然是“前置逻辑 + 转发 + 后置逻辑”。

Spring AOP 里的@Transactional@Cacheable本质上也是装饰器思想的实现,只不过它用动态代理在运行时生成代理对象,不需要手动 new 每个装饰器。但底层逻辑一样:目标方法被包装,事务、缓存这些横切逻辑在目标方法前后执行。理解了装饰器模式,再看 Spring AOP 的拦截器设计会顺畅很多。

5. 选型对比:装饰器、继承、代理、适配器到底怎么挑

5.1 装饰器 vs 继承:类爆炸问题的数学解释

前面提到过,继承实现组合需求,最坏需要m × 2ⁿ个类,m 是基础构件数,n 是增强能力数。用装饰器模式,需要的类数量是m + n + 1(m 个具体构件、n 个具体装饰器、1 个抽象装饰器),因为组合是在运行期动态完成的,不需要为每一种组合创建类。

两者还有一个本质差异:继承是静态编译期绑定,装饰器是动态运行期组合。继承的关系在编译完成时就锁死了,一个类只能是某一个组合的结果;装饰器可以在运行时自由选择套哪几层、以什么顺序套,甚至可以给同一个对象套不同的壳。

所以我的选型建议是:增强能力少且固定,用继承无所谓;增强能力多且自由组合,优先装饰器;增强能力会在运行期动态变化,只能装饰器。

5.2 装饰器 vs 代理:看起来像,本质不同

这两个是最容易被混为一谈的,因为代码结构几乎一样——都是持有一个被包装对象的引用,都对方法调用做了拦截。我区分它们的标准是看“意图”:

维度装饰器代理
核心意图给对象增加新行为控制对对象的访问
对象来源由外部传入,装饰器不负责创建代理通常自己创建或查找被代理对象
接口保持必须保持接口不变,类型透明可以改变接口,也可以做访问控制
典型例子Java IO 装饰流Spring AOP、RPC 远程代理、懒加载代理

举一个最能说明问题的例子:权限代理可以在调用真实方法前检查当前用户有没有权限,没有就直接拒绝,甚至不调用真实方法;装饰器不会拒绝调用,它只会给调用“加料”。如果某个类描述的是“增强”,那大概率是装饰器;如果描述的是“控制”,那就是代理。

5.3 装饰器 vs 适配器:一个加法、一个翻译

适配器模式核心是“转换接口”,把 A 类型的接口转换成 B 类型,让原本不兼容的类能协作。装饰器是“保持接口不变”,在同一个接口之下增加功能。

生活中的例子更好理解:适配器是电源转换头,你把欧标插头插进国标插座,需要转换头,插头型号变了;装饰器是手机壳,你给手机套个防摔壳,充电口还是那个充电口,接口没变,只是抗摔能力增强了。在代码里,如果一个类实现了某个新接口、把旧接口的方法“翻译”成新接口的调用,那是适配器;如果一个类实现的是和原对象完全相同的接口,那才是装饰器。

6. 踩坑实录:装饰器模式最容易翻车的六个场景

6.1 装饰顺序不对,结果完全相反

我在实战里被坑得最狠的一次就是顺序问题。加密装饰器和日志装饰器的顺序,直接决定了日志里记录的是明文还是密文;重试装饰器和延迟装饰器的顺序,直接决定了“失败后是立刻重试还是再延迟一次”。一旦线上环境把顺序搞反,轻则日志泄露敏感信息,重则重试风暴打垮下游服务。

我总结了一个判断方法:从外到内看,谁先拿到消息,谁就最先处理消息。最外层的装饰器对消息有第一处置权,最内层的构件是最后处理消息的。你希望“先加密再记日志”,就把加密放在日志外层;你希望“日志记录原始内容,加密只影响实际发送”,就把日志放在加密外层。这种决策必须在设计阶段就确定下来,并且用工厂方法固定,禁止业务方自行拼装。

6.2 依赖具体类型的调用方会失效

装饰器模式要成立,前提是调用方只面向抽象构件编程。但现实里总有代码会写出这种行为:

// 这是反面教材 public void sendWithFallback(MessageSender sender) { if (sender instanceof SmsSender) { // 单独处理短信渠道的特殊逻辑 } sender.send("test"); }

一旦senderLoggingDecoratorRetryDecorator包过,instanceof SmsSender就变成 false,这段特殊逻辑会直接失效。同理,equals()hashCode()List.contains()这些基于对象身份的判断都会出问题。

解决办法有两个层面。设计层面,尽量保证业务逻辑不依赖具体类型;架构层面,如果确实需要访问“被装饰对象的特定能力”,可以考虑在抽象构件接口里暴露这些能力,或者用一个unwrap方法手动解套。但解套本身就是危险的信号,说明你的抽象可能设计得不够完整。

6.3 深链路的调试与可读性之痛

装饰器套到四五层之后,日志和堆栈会变得很难看。异常堆栈里是一长串at xxxDecorator.send(xxxDecorator.java:12),从下往上一层接一层,没有经验的同事看着就头大。我项目里甚至出现过套了七层的“装饰塔”,排查一个发送问题花了两个小时。

我的应对策略有两个。一是给每个装饰器写清晰的 toString,打印出它的类型和内部 target 的类型,这样日志里一条 toString 就能看出整条装饰链的结构。二是在工厂方法里限制装饰链的最大层数,超过五层就停下来问自己:是不是该重新设计了?装饰器模式的威力在于灵活组合,但灵活不代表无限制堆叠。

6.4 过度装饰:什么时候不要用装饰器

装饰器模式很优雅,但它不是银弹。我见过有人给一个只有两种渠道、一种增强的项目硬套装饰器,类没少写,代码反而难读。判断标准很简单:

  • 增强能力少于两个,且基本不变 → 直接在实现类里加逻辑,别用装饰器。
  • 增强能力之间强依赖,比如“加密必须在日志之前且两者永远一起出现” → 与其用两个装饰器,不如合并成一个,或者直接在业务里写清楚。
  • 增强逻辑和业务逻辑强耦合,拆不掉 → 装饰器反而制造了人为的割裂感。

记住 YAGNI 原则:不要为还不需要的灵活性付代价。一个项目的代码量和它的灵活性需求应该是匹配的,过度设计比不够设计更恶心,因为不够设计至少好懂。

6.5 并发与状态:装饰器不是无状态的

初学者最容易忽略的是装饰器的状态问题。比如你在日志装饰器里加了一个AtomicInteger counter统计发送次数,这个装饰器对象会被多个线程共享,counter 是线程安全的;但如果换成普通的int计数,并发场景下数据就乱了。还有延迟装饰器里的ScheduledExecutorService,它内部有线程池资源,不能每次 new 一个,否则线程资源会泄漏。

我在项目里约定:装饰器尽量保持无状态,或只持有不可变配置和线程安全的组件。凡是需要可变状态的,要么抽出去,要么用并发安全的数据结构。这个约定让装饰器在并发环境下的行为可预测,排查问题时不用先怀疑装饰器内部状态错乱。

6.6 更优雅的替代方案:什么时候换别的路

最后说两句实在话。装饰器模式解决的是“动态叠加增强”的问题,但如果你发现自己在不断写装饰器,可能说明你的问题本质上不是“增强”,而是“策略选择”——这时候策略模式加工厂可能更清晰;如果你发现不同增强之间有复杂的依赖和交互,可能pipeline模式更适合你;如果你需要的是在运行期频繁修改组合策略,那不妨考虑规则引擎或配置化方案。

我个人的经验是:装饰器模式最适合的依然是横切关注点——日志、监控、重试、限流、缓存这一类与业务逻辑正交的能力。一旦装饰器开始掺和业务判断,就该停下来重新思考了。


最后分享一个实战小技巧:我后来在项目里给装饰器统一加了一个getInnerTarget()方法,用来返回内部持有的target对象,专门用于单元测试里断言装饰链的结构。测试某个工厂方法是否按预期顺序组装装饰器时,不需要真的发一次消息,只需要解套验证每一层是不是期望的类型。这个小工具帮我省下了无数和“顺序配错”搏斗的时间。如果你也正在用装饰器模式重构一个越改越乱的模块,多用几层装饰不可怕,可怕的是顺序乱、类型依赖、状态失控——这三关过了,装饰器就是一把趁手的好刀。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/24 22:35:24

EMC电波暗室日常维护指南:从吸波材料到屏蔽壳体的关键细节

先讲个很多人容易忽略的事实&#xff1a;EMC电波暗室虽然看起来是一间“贴着海绵的房间”&#xff0c;本质上却是一台精密的电磁测量设备。它的价值既体现在屏蔽壳体的结构上&#xff0c;更体现在内部吸波材料、转台、天线塔和接口面板这些“细枝末节”的状态里。我见过不少实验…

作者头像 李华
网站建设 2026/9/24 22:34:55

Nav2自定义Behavior插件从零实现与避坑指南

最近在搞导航任务时&#xff0c;总需要在行为树里塞一些自定义逻辑&#xff0c;比如到达目标点后要查询外部服务、绕障完成后要上报状态、或者根据业务侧下发的一个字符串去切换不同的导航模式。Navigation2 本身就提供了大量 Behavior 节点&#xff0c;但业务逻辑千奇百怪&…

作者头像 李华
网站建设 2026/9/24 22:34:52

GPU超节点Scale-Up域扩容实战:从72卡到144卡的拓扑规划与故障恢复

这两年做大模型训练集群的人&#xff0c;肯定绕不开一个词&#xff1a;超节点。而我最近几个月的大半精力&#xff0c;都耗在把超节点的Scale-Up域从72卡扩到144卡这件事上。听起来只是多了72块卡&#xff0c;对吧&#xff1f;真动手才知道&#xff0c;这基本等同于把一栋楼的电…

作者头像 李华
网站建设 2026/9/24 22:33:59

BLE数传全链路实战:从串口配置到手机App对接的避坑指南

1. 为什么BLE数传值得单独拿出来讲BLE数传这件事&#xff0c;看起来简单——不就是手机连个蓝牙模块&#xff0c;然后收发数据吗&#xff1f;但真正做过完整链路的人都知道&#xff0c;从串口到手机App这条路上&#xff0c;坑多到能写一本小册子。我前后做过好几个基于BLE的数传…

作者头像 李华
网站建设 2026/9/24 22:33:58

从“567890”看懂编号识别、校验位与数据清洗实战

“567890”这个标题乍一看就是六个数字&#xff0c;没有任何上下文&#xff0c;连个分隔符都没有。但恰恰是这种“信息缺失”的状态&#xff0c;才是我们日常工作中最常遇到的情况&#xff1a;一个编号、一串流水号、一列看起来毫无规律的字符&#xff0c;背后往往藏着一条完整…

作者头像 李华
网站建设 2026/9/24 22:33:54

LeetCode 128题最长连续序列:哈希表解法与O(n)复杂度剖析

刷 LeetCode 的人基本都有一个感受&#xff1a;有些题是“看着简单&#xff0c;做了才知道水多深”&#xff0c;128 题《最长连续序列》就是典型。你拿到题的第一反应大概率是“排序然后数一遍”&#xff0c;但题目末尾一句“要求时间复杂度 O(n)”直接堵死了这条最顺的路。这道…

作者头像 李华