用一个企业级真实的工业物联网项目,带你把23种设计模式从头撸到尾
前言
实习前我觉得设计模式就是八股文。单例嘛,私有构造函数加个static变量,背就完了。策略模式嘛,一个接口几个实现类,面试能扯两句就行。
直到我在一个工业物联网项目里,亲眼看到一条设备消息从MQTT进来,经过Spring Event解耦、Pipeline九节点编排、Redis双键写入、InfluxDB时序存储、Drools规则引擎评估,最后变成一条告警推送到前端。这条链路上,十几种设计模式不是"用到了",而是"离了它系统就转不动"。
这篇博客把我在这个项目里学到的23种设计模式全部整理出来。每种模式我都会给一个生活类比、一段项目里的真实代码(已脱敏)、以及面试时怎么讲。不是教科书式的罗列,是一个后端新人真正啃完源码之后的理解。
一、创建型模式:对象怎么来
创建型模式解决一个问题:对象怎么创建。是全局只造一个?是让别人帮你造?还是一步步拼出来?
1. 单例模式(Singleton)
一句话:全局只要一个实例。
生活类比:学校只有一个教务处。不管哪个学院要盖章,都去同一个地方。
项目里的ID生成器就是单例。整个系统共享一个实例来生成唯一ID:
public class IdGenerator {
// 类加载时就创建好,饿汉式
private static IdGenerator instance = new IdGenerator(0);
// 私有构造函数,外面不能 new
private IdGenerator(long machineId) {
if (machineId > MAX_MACHINE_NUM || machineId < 0) {
throw new IllegalArgumentException("machineId 超出范围");
}
this.machineId = machineId;
}
public static IdGenerator getInstance() {
return instance;
}
public static long generateId() {
return instance.nextId();
}
}
三个关键点:构造函数private(不让别人new)、static变量(全局一份)、getInstance()(唯一入口)。
这是饿汉式,类一加载就创建。适合启动就要用的东西,比如ID生成器、配置管理器。与之对应的是懒汉式,等第一次调用才创建,适合创建成本高且不一定用的场景。懒汉式要注意线程安全,推荐双重检查锁或者静态内部类写法。
面试讲法:“项目的ID生成器用饿汉式单例,构造函数私有化,通过静态getInstance获取唯一实例。选饿汉式是因为ID生成器启动时就要用,不需要延迟加载,而且饿汉式天然线程安全。”
2. 工厂方法模式(Factory Method)
一句话:把创建对象的逻辑封装到一个专门的方法里。
生活类比:你去奶茶店说"来杯珍珠奶茶",店员帮你做好递过来。你不需要知道珍珠怎么煮、奶精从哪来。
项目里Shiro安全框架的配置类就是一个大工厂,里面十几个@Bean方法,每个方法"生产"一个组件:
@Configuration
public class ShiroConfig {
@Bean
public RedisManager redisManager() {
RedisManager redisManager = new RedisManager();
redisManager.setHost(redisHost + ":" + redisPort);
redisManager.setDatabase(database);
if (StringUtils.isNotEmpty(password)) {
redisManager.setPassword(password);
}
return redisManager;
}
@Bean
public SecurityManager securityManager(UserRealm userRealm,
SessionManager sessionManager) {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(userRealm);
securityManager.setCacheManager(redisCacheManager());
securityManager.setSessionManager(sessionManager);
return securityManager;
}
}
@Bean就是工厂方法。你定义"怎么创建对象",Spring帮你管理生命周期。使用者只需要@Autowired注入,不需要知道创建细节。配置变了(比如Redis地址换了),只改工厂方法,使用方完全不动。
工厂方法之间还能互相调用。比如customRedisSessionDAO()里直接调redisManager()拿一个配好的实例,就像奶茶店的"珍珠奶茶"会调用"煮珍珠"的工序。
面试讲法:“Shiro配置类用工厂方法模式,十几个@Bean方法分别创建RedisManager、SessionManager、SecurityManager等组件,组件间依赖通过Spring自动注入。把创建和使用分离,配置变更只改工厂方法。”
3. 抽象工厂模式(Abstract Factory)
一句话:创建一整套相关产品。
生活类比:你去苹果店,得到iPhone + AirPods + Apple Watch,一套都是苹果生态。去华为店,得到Mate + FreeBuds + Watch GT,一套都是华为生态。
工厂方法生产一个产品,抽象工厂生产一整套。
项目里的SM4国密加密工具就是抽象工厂。不同业务场景用不同密钥,每种密钥类型对应一整套加密配置:
public class SM4Utils {
// 按密钥类型缓存加密器实例
private static final Map<KeyType, SymmetricCrypto> SM4_INSTANCES
= new ConcurrentHashMap<>();
// 启动时预创建所有类型的加密器
static {
for (KeyType keyType : KeyType.values()) {
getSM4Instance(keyType);
}
}
public static SymmetricCrypto getSM4Instance(KeyType keyType) {
return SM4_INSTANCES.computeIfAbsent(keyType,
k -> SmUtil.sm4(SM4Config.getKeyBytes(k)));
}
public static String encryptHex(String content, KeyType keyType) {
return getSM4Instance(keyType).encryptHex(content);
}
}
KeyType有LICENSE、API、DEVICE等类型。每种类型对应不同的密钥字节和加密器实例。你选LICENSE,拿到的就是LICENSE那套加密配置;选API,拿到的就是API那套。ConcurrentHashMap缓存避免重复创建。
面试讲法:“SM4加密工具用抽象工厂模式,根据KeyType创建对应的加密实例。LICENSE、API、DEVICE每种密钥类型对应一整套加密配置,用ConcurrentHashMap缓存复用。”
4. 建造者模式(Builder)
一句话:分多步构建一个复杂对象。
生活类比:组装电脑。选CPU、选显卡、选内存、选硬盘,一步步装上去,最后得到一台完整电脑。
注意,建造者的核心不是"有个叫build()的方法",而是"构建过程的分步性和累积性"。方法叫build、create、assemble都行,结构才重要。
项目里AI诊断模块有个DeviceContextBuilder,负责把设备信息拼成一段结构化文本喂给大模型:
@Service
public class DeviceContextBuilder {
public String buildDeviceContext(Long nodeId) {
Instruments device = instrumentsService.getById(nodeId);
StringBuilder ctx = new StringBuilder();
// 第1步:设备基本信息
ctx.append("## 设备基础信息\n");
ctx.append("- 设备名称: ").append(nullSafe(device.getNodeName())).append("\n");
ctx.append("- 设备编码: ").append(nullSafe(device.getNodeCode())).append("\n");
// 第2步:近7天告警记录
List<InsAlarmLog> alarms = alarmLogService.list(
new LambdaQueryWrapper<InsAlarmLog>()
.eq(InsAlarmLog::getNodeId, nodeId)
.ge(InsAlarmLog::getAlarmTime, LocalDateTime.now().minusDays(7))
.last("LIMIT 20"));
ctx.append("\n## 近7天告警记录(").append(alarms.size()).append("条)\n");
for (InsAlarmLog alarm : alarms) {
ctx.append("- [").append(alarm.getAlarmTime()).append("] ")
.append(alarm.getAlarmContent()).append("\n");
}
// 第3步:近24小时解析错误数
int errorCount = jdbcTemplate.queryForObject(
"SELECT COUNT(*) FROM ins_error_info WHERE node_id = ? AND create_time > ?",
Integer.class, nodeId, LocalDateTime.now().minusHours(24));
ctx.append("\n## 近24小时解析错误: ").append(errorCount).append("次\n");
return ctx.toString();
}
private String nullSafe(Object value) {
return value == null ? "未知" : value.toString();
}
}
Controller只需要一行代码:String context = builder.buildDeviceContext(nodeId); 不需要知道背后查了几个表、怎么处理空值。构建逻辑和业务逻辑完全分离,而且别的地方要复用设备上下文,直接调这个方法就行。
面试讲法:“AI诊断模块用建造者模式构建设备上下文。DeviceContextBuilder封装了设备信息、告警历史、错误统计等多维度数据的查询和拼接逻辑,输出结构化文本供大模型分析。Controller不需要关心数据怎么查、怎么拼。”
5. 原型模式(Prototype)
一句话:通过克隆已有对象创建新对象。
生活类比:复印文件。不用重新写,直接复印一份再改。
项目里没有直接用原型模式,但批量注册设备时可以用。同一批设备配置大同小异(同协议、同端口),只有名称和MAC不同。先创建一个模板配置,clone出多份,只改不同字段:
public class DeviceConfig implements Cloneable {
private String deviceName;
private String protocol;
private int port;
private List<String> sensors;
@Override
protected DeviceConfig clone() throws CloneNotSupportedException {
DeviceConfig copy = (DeviceConfig) super.clone();
copy.sensors = new ArrayList<>(this.sensors); // 深拷贝引用类型
return copy;
}
}
// 使用
DeviceConfig template = new DeviceConfig("模板", "MQTT", 1883, sensors);
DeviceConfig config1 = template.clone();
config1.setDeviceName("温度传感器-01");
DeviceConfig config2 = template.clone();
config2.setDeviceName("温度传感器-02");
关键考点是浅拷贝和深拷贝。浅拷贝只克隆对象本身,引用类型字段还是指向原来的对象。深拷贝连引用类型也克隆一份新的。
面试讲法:“原型模式通过克隆创建新对象,适合创建成本高的场景。要注意浅拷贝和深拷贝的区别,引用类型字段需要单独克隆。Java的Object.clone()是标准实现。”
二、结构型模式:对象怎么组合
结构型模式解决一个问题:类和对象之间怎么组合、怎么包装。
6. 代理模式(Proxy)
一句话:不修改原方法,在方法前后自动加额外逻辑。
生活类比:明星的经纪人。你找明星拍戏得先找经纪人,经纪人在拍戏前后安排合同、宣传。明星本人不需要改任何代码。
项目里用Spring AOP实现了两个代理:日志切面和限流切面。
先看注解定义:
@Target({ElementType.PARAMETER, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Log {
String title() default "";
BusinessTypeEnum businessType() default BusinessTypeEnum.OTHER;
boolean isSaveRequestData() default true;
boolean isSaveResponseData() default true;
}
再看切面:
@Aspect
@Component
public class LogAspectj {
@Pointcut("@annotation(com.example.iot.common.annotaion.Log)")
public void logPointCut() {}
@AfterReturning(pointcut = "logPointCut()", returning = "jsonResult")
public void doAfterReturning(JoinPoint joinpoint, Object jsonResult) {
handleLog(joinpoint, null, jsonResult);
}
@AfterThrowing(pointcut = "logPointCut()", throwing = "e")
public void doAfterThrowing(JoinPoint joinpoint, Exception e) {
handleLog(joinpoint, e, null);
}
private void handleLog(JoinPoint joinPoint, Exception e, Object jsonResult) {
SysLog log = new SysLog();
log.setOperIp(getIp());
log.setOperUrl(getRequestURI());
log.setRequestMethod(getRequestMethod());
log.setOperParam(getArgs(joinPoint)); // 敏感字段自动脱敏
log.setCostTime(System.currentTimeMillis() - startTime);
if (e != null) {
log.setStatus("FAIL");
log.setErrorMsg(e.getMessage());
}
AsyncManager.me().execute(new LogTask(log)); // 异步写日志
}
}
业务代码只需要加一个注解:
@Log(title = "用户管理", businessType = BusinessTypeEnum.INSERT)
@PostMapping("/add")
public ResultUtil add(@RequestBody SysUser user) {
return userService.insertUser(user); // 只写业务逻辑
}
日志记录、参数脱敏、耗时统计、异步写入,全自动。限流也是同理,@RateLimit注解的方法被RateLimitAspectj代理,用Caffeine滑动窗口检查频率,超限直接抛异常。
面试讲法:“框架层用Spring AOP实现代理模式。@Log注解的方法被LogAspectj自动代理,方法执行后异步记录操作日志,敏感字段自动脱敏。@RateLimit注解的方法被RateLimitAspectj代理,用Caffeine滑动窗口限流。业务代码只需要加注解,横切关注点完全分离。”
7. 外观模式(Facade)
一句话:把一组复杂操作封装成一个简单入口。
生活类比:酒店前台。你打一个电话说"多一条毛巾、明天7点叫早、叫一辆出租车",前台在背后联系保洁部、设置闹钟、联系出租车公司。你只跟前台一个人打交道。
注意,外观模式不是"接口+实现类"。面向接口编程是一个接口多个实现可以替换,外观是一个类把一堆操作包起来给你简单入口。
项目里大数据模块的统一API网关就是外观:
@Service
public class UnifiedApiGatewayService {
public ResponseEntity<String> proxyRequest(Long apiId, String body,
HttpHeaders headers) {
// 第1步:查API定义
BdUnifiedApi api = apiMapper.selectById(apiId);
if (api == null) return ResponseEntity.notFound().build();
// 第2步:限流检查
if (api.getRateLimit() != null && api.getRateLimit() > 0) {
Bucket bucket = getOrCreateBucket(apiId, api.getRateLimit());
if (!bucket.tryConsume(1)) {
recordAlert(apiId, "RATE_LIMIT", "请求频率超过限制");
return ResponseEntity.status(429).body("请求过于频繁");
}
}
// 第3步:转发请求
long startTime = System.currentTimeMillis();
try {
ResponseEntity<String> response = restTemplate.exchange(
api.getRouteTarget(), HttpMethod.valueOf(api.getHttpMethod()),
new HttpEntity<>(body, headers), String.class);
// 第4步:记录监控指标
recordMetric(apiId, System.currentTimeMillis() - startTime, true);
return response;
} catch (Exception e) {
recordMetric(apiId, System.currentTimeMillis() - startTime, false);
recordAlert(apiId, "PROXY_ERROR", e.getMessage());
return ResponseEntity.status(502).body("目标服务不可用");
}
}
}
外部系统调进来就一行:gateway.proxyRequest(apiId, body, headers); 背后的路由查找、令牌桶限流、请求转发、指标采集、异常告警,调用者完全不知道。限流策略变了或者新增监控维度,只改这一个类。
面试讲法:“统一API网关用外观模式,把路由转发、令牌桶限流、监控采集、异常告警封装在一个proxyRequest方法里。调用者传apiId就能完成请求代理,限流策略变更只改网关一个类。”
8. 组合模式(Composite)
一句话:树形结构中,个体和容器统一对待。
生活类比:文件夹。文件夹里可以放文件,也可以放子文件夹。"删除"操作对文件夹和文件是一样的,右键删除就行。
项目里的系统菜单就是组合模式:
public class SysMenu extends BaseEntity {
private Long menuId;
private String menuName;
private Long parentId; // 父菜单ID,0表示顶级
private String type; // M=目录 C=菜单 B=按钮
@TableField(exist = false) // 不在数据库表里,运行时拼出来的
private List<SysMenu> children;
}
一个SysMenu里可以包含多个SysMenu,这就是"文件夹套文件夹"。递归构建树:
private List<SysMenu> getChild(SysMenu currentMenu, List<SysMenu> allMenus) {
return allMenus.stream()
.filter(menu -> menu.getParentId().equals(currentMenu.getMenuId()))
.peek(menu -> menu.setChildren(getChild(menu, allMenus))) // 递归
.collect(Collectors.toList());
}
前端渲染时不需要区分"这是目录还是菜单",统一递归处理。权限校验也是基于这棵树,按parentId逐级向上查找。
面试讲法:“系统菜单用组合模式,SysMenu包含children列表,通过递归构建菜单树。目录和菜单共用同一套树操作逻辑,前端渲染和后端权限校验都不需要区分节点类型。”
9. 桥接模式(Bridge)
一句话:把抽象和实现分开,让它们可以独立变化。
生活类比:遥控器和电视。遥控器有开关、换台、调音量按钮(抽象),每个品牌电视内部实现不同(实现)。你可以换遥控器不换电视,也可以换电视不换遥控器。
项目里Socket协议层体现了桥接思想。报文组装逻辑和CRC计算、序号管理通过依赖注入解耦:
@Component
public class SocketPacketBuilder {
private final SocketPacketCrcService crcService;
private final SocketPacketSequenceService seqService;
public byte[] buildPacket(int pkgType, byte[] data, ProtocolSchema schema) {
// 报文组装的抽象逻辑
int seq = seqService.nextSequence(schema.getSeqMode(), null);
int crc = crcService.calculateCrc(packetBytes, schema);
// ...
}
}
CRC算法可以换(CRC16换CRC32),Builder不用改。序号策略可以换(递增换时间戳),Builder也不用改。两者独立变化。
不过这不是标准桥接模式(抽象类+实现接口),而是Spring依赖注入实现的分离。面试时说"体现了桥接思想"就行,不要硬说"用了桥接模式"。
10. 适配器模式(Adapter)
一句话:把不兼容的接口转换成统一的接口。
生活类比:转换插头。中国充电器是两脚扁平的,欧洲插座是两脚圆形的,中间加个转换插头就能用。
项目里设备状态的历史数据格式很混乱。有数字0/1/2/3,有英文"ONLINE"/“OFFLINE”,有中文"在线"/“离线”,还有null。新系统只认统一的数值编码。InstrumentConverter的convertNodeStatus方法就是那个转换插头:
public static String convertNodeStatus(Object nodeStatus) {
if (nodeStatus == null) {
return String.valueOf(UnifiedDeviceStatusEnum.OFFLINE.getCode());
}
if (nodeStatus instanceof Number) {
int code = ((Number) nodeStatus).intValue();
UnifiedDeviceStatusEnum status = UnifiedDeviceStatusEnum.getByCode(code);
return String.valueOf(status != null ? status.getCode() : 1);
}
String raw = nodeStatus.toString().trim();
try {
int code = Integer.parseInt(raw);
UnifiedDeviceStatusEnum status = UnifiedDeviceStatusEnum.getByCode(code);
if (status != null) return String.valueOf(status.getCode());
} catch (NumberFormatException ignored) {}
UnifiedDeviceStatusEnum status = UnifiedDeviceStatusEnum.fromNodeStatus(nodeStatus);
return String.valueOf(status.getCode());
}
不管输入是Integer 0、String “ONLINE”、String “在线"还是null,输出统一是"0”(在线)、“1”(离线)、“2”(预警)、“3”(故障)、“-1”(禁用)。
严格来说这不是经典适配器(类级别,实现目标接口持有被适配者),而是数据转换器。但核心思想一样:在不改变原有数据的情况下,提供统一的访问接口。
11. 装饰器模式(Decorator)
一句话:不改变原对象,动态地给它加功能。
生活类比:手机壳。手机功能不变,套个壳多了防摔,换个壳又有不同功能。
Java IO流是最经典的装饰器:
FileInputStream fis = new FileInputStream("data.txt"); // 只能读字节
BufferedInputStream bis = new BufferedInputStream(fis); // 加了缓冲
ObjectInputStream ois = new ObjectInputStream(bis); // 又加了对象解析
每层装饰器都实现同一个接口,同时持有被装饰者的引用。功能叠加,灵活组合。
装饰器和代理的区别:代理控制访问(“你能不能用我说了算”),装饰器增强功能(“你能用,而且用得更好了”)。项目里的RateLimitAspectj是代理(超限不让你执行),BufferedInputStream是装饰器(让读操作更快)。
项目里没有直接用装饰器,但如果以后MQTT消息处理需要加压缩、加密、签名验证,可以用装饰器层层包装,每层独立,灵活组合。
12. 空对象模式(Null Object)
一句话:找不到合适的,就给一个默认的空版本,不返回null。
生活类比:你去餐厅点红烧鳄鱼,服务员说"做不了,但给您换一道招牌菜"。你至少不会饿着。如果服务员说"没有"就走了,你干等半小时才发现没人理你(NullPointerException)。
这个模式要配合策略模式一起看。项目里大数据抽取,不同数据源有不同策略。如果来了一个INFLUXDB类型但还没人写对应策略,怎么办?
@Component
public class DefaultExtractionStrategy implements ExtractionStrategy {
@Override
public String getSourceType() { return "DEFAULT"; }
@Override
public String extract(String sourceConfig, String extractConfig) {
log.warn("当前数据源类型暂未实现专用抽取策略,返回空结果");
return "{\"rows\": 0, \"message\": \"该数据源类型的抽取策略待实现\"}";
}
}
使用时:
ExtractionStrategy strategy = strategyMap.getOrDefault(
task.getSourceType(), defaultStrategy);
// strategy 永远不是 null,不需要判空
String result = strategy.extract(sourceConfig, extractConfig);
不崩溃,返回友好的空结果,同时打warn日志提醒开发者扩展实现。调用方不需要做null检查。
面试讲法:“大数据抽取用空对象模式兜底。DefaultExtractionStrategy在找不到匹配策略时返回空结果JSON而不是null,避免NPE,同时打warn日志提醒扩展。调用方不需要判空。”
三、行为型模式:对象之间怎么通信
行为型模式解决一个问题:对象之间怎么分配职责、怎么通信。
13. 观察者模式(Observer)
一句话:发布者只管发事件,监听者自己来订阅,双方互不认识。
生活类比:微信公众号。公众号发文章,所有关注者收到推送。公众号不需要知道谁关注了它,不需要遍历粉丝ID一个个发消息。
这是项目里最核心的模式之一。MQTT模块收到设备消息后,不是直接调用Pipeline和流媒体模块,而是发布一个Spring Event:
事件定义:
@Data
public class MqttMessageEvent {
private String topic;
private String payload;
private int qos;
public MqttMessageEvent(String topic, String payload, int qos) {
this.topic = topic;
this.payload = payload;
this.qos = qos;
}
}
发布者:
@Service
public class MqttMessageHandler {
private final ApplicationEventPublisher eventPublisher;
public void handleMessage(String topic, String payload, int qos) {
MqttMessageEvent event = new MqttMessageEvent(topic, payload, qos);
eventPublisher.publishEvent(event);
// 不知道谁在监听,也不关心
}
}
监听者:
@Component
public class DeviceDataEventListener {
private final DeviceReportWorkflowOrchestrator orchestrator;
@EventListener
public void handleDeviceData(MqttMessageEvent event) {
if (Objects.isNull(event) || StringUtils.isBlank(event.getPayload())) {
return;
}
orchestrator.executeFromMqtt(event.getTopic(), event.getPayload(), event.getQos());
}
}
完整链路:设备发MQTT消息 → MqttCallback收到 → MqttMessageHandler封装成Spring Event发布 → DeviceDataEventListener监听到,启动Pipeline处理。
加一个新模块有多简单?新建一个类,加@EventListener,完事。不需要改MqttMessageHandler的任何代码。这就是开闭原则。
Spring Event默认同步,publishEvent()会等所有监听者处理完才返回。加@Async可以变异步。项目里Pipeline处理是同步的(保证消息处理顺序),流媒体事件监听加了@Async(不阻塞MQTT接收)。
面试讲法:“MQTT模块用Spring Event实现观察者模式解耦。MqttCallback收到消息后通过ApplicationEventPublisher发布MqttMessageEvent,Pipeline通过@EventListener监听处理。MQTT层不知道谁在处理消息,新增处理模块只需要加一个@EventListener类,符合开闭原则。”
14. 策略模式(Strategy)
一句话:定义一组算法,封装起来,运行时动态选择。
生活类比:导航App。去同一个目的地,可以选驾车、公交、骑行,每种方式的路线规划算法完全不同。
项目里大数据模块的数据抽取是教科书级别的策略模式。
策略接口:
public interface ExtractionStrategy {
String extract(String sourceConfig, String extractConfig);
String getSourceType();
}
具体策略:
@Component
public class MysqlExtractionStrategy implements ExtractionStrategy {
@Autowired
private JdbcTemplate jdbcTemplate;
public String getSourceType() { return "MYSQL"; }
public String extract(String sourceConfig, String extractConfig) {
String sql = parseJsonValue(extractConfig, "sql");
List<Map<String, Object>> results = jdbcTemplate.queryForList(sql);
return "{\"rows\": " + results.size() + "}";
}
}
@Component
public class KafkaExtractionStrategy implements ExtractionStrategy {
public String getSourceType() { return "KAFKA"; }
public String extract(String sourceConfig, String extractConfig) {
KafkaConsumer<String, String> consumer = new KafkaConsumer<>(props);
consumer.subscribe(Collections.singletonList(topic));
ConsumerRecords<String, String> records = consumer.poll(Duration.ofSeconds(10));
return "{\"rows\": " + count + "}";
}
}
同样是extract()方法,MySQL用JdbcTemplate执行SQL,Kafka用KafkaConsumer消费消息。算法完全不同,但接口一样。
策略注册表和运行时选择:
@Service
public class ExtractionTaskServiceImpl {
private final Map<String, ExtractionStrategy> strategyMap = new HashMap<>();
private final DefaultExtractionStrategy defaultStrategy;
@Autowired
public ExtractionTaskServiceImpl(List<ExtractionStrategy> strategies,
DefaultExtractionStrategy defaultStrategy) {
this.defaultStrategy = defaultStrategy;
for (ExtractionStrategy strategy : strategies) {
if (strategy != defaultStrategy) {
strategyMap.put(strategy.getSourceType(), strategy);
}
}
}
public boolean executeOnce(Long taskId) {
BdExtractionTask task = getById(taskId);
ExtractionStrategy strategy = strategyMap.getOrDefault(
task.getSourceType(), defaultStrategy);
String result = strategy.extract(task.getSourceConfig(), task.getExtractConfig());
// ...
}
}
Spring自动注入List,构造器里按sourceType注册到Map。运行时getOrDefault一行搞定分发。加新数据源只需要新建一个实现类加@Component,不改任何老代码。
面试讲法:“数据抽取用策略模式,ExtractionStrategy定义extract和getSourceType,MySQL和Kafka各自实现。通过构造器注入所有策略构建Map注册表,运行时按sourceType动态选择。新增数据源只加实现类,符合开闭原则。”
15. 责任链模式(Chain of Responsibility)
一句话:多个处理者串成链,请求依次经过,任何一环可以中断。
生活类比:汽车工厂流水线。车身依次经过焊接→喷漆→装配→检测,每个工位只干自己的活,任何工位发现问题都可以让车身下线。
这是项目里最核心的模式。Pipeline编排引擎处理设备上报数据:
@Component
public class DeviceReportWorkflowOrchestrator {
private final DeviceReportParser parser;
private final DeviceReportValidator validator;
private final DeviceIdentityNormalizer identityNormalizer;
private final DeviceInfluxWriter influxWriter;
private final DeviceRealtimeStageProcessor realtimeStageProcessor;
private final DeviceErrorProcessor deviceErrorProcessor;
private final IWorkflowRuntimeService workflowRuntimeService;
private final IWorkflowExecutionLogService workflowExecutionLogService;
public void execute(DeviceReportContext context) {
String workflowCode = IWorkflowRuntimeService.DEFAULT_WORKFLOW_CODE;
// 三级容错第1级:全局开关
if (!workflowRuntimeService.isWorkflowRunning(workflowCode)) {
return;
}
// n1 解析(失败则中断)
if (!executeParseNode(workflowCode, context)) {
recordParseError(workflowCode, context);
return;
}
// n2 校验(失败则中断)
if (!executeValidateNode(workflowCode, context)) {
recordValidateError(workflowCode, context);
return;
}
// n3-n7 后续节点(异常不中断,记日志)
try {
executeNormalizeNode(workflowCode, context);
executeInfluxNode(workflowCode, context, deviceVo);
realtimeStageProcessor.process(deviceVo, workflowCode, context);
context.getResult().put("workflow", "success");
} catch (Exception e) {
// 三级容错第3级:try-catch兜底
context.getResult().put("workflow", "failed");
recordWorkflowError(context.getTraceId(), workflowCode, deviceVo, e);
}
}
}
每个节点的执行都有独立开关和日志:
private boolean executeParseNode(String workflowCode, DeviceReportContext context) {
// 三级容错第2级:节点开关
if (!workflowRuntimeService.isNodeEnabled(workflowCode,
WorkflowNodeCode.PARSE_PAYLOAD, true)) {
workflowExecutionLogService.nodeSkipped(context.getTraceId(),
workflowCode, WorkflowNodeCode.PARSE_PAYLOAD, "node_disabled");
return true; // 节点被禁用,跳过,不影响后续
}
long start = System.currentTimeMillis();
boolean ok = parser.parse(context);
if (ok) {
workflowExecutionLogService.nodeSuccess(context.getTraceId(),
workflowCode, WorkflowNodeCode.PARSE_PAYLOAD,
System.currentTimeMillis() - start, "success");
return true;
}
workflowExecutionLogService.nodeFailed(context.getTraceId(),
workflowCode, WorkflowNodeCode.PARSE_PAYLOAD,
System.currentTimeMillis() - start, "parse_failed");
return false;
}
为什么不用经典责任链?经典链是每个Handler持有下一个Handler的引用,自动传递。但项目需要三个能力:节点动态启停(isNodeEnabled)、每个节点独立的执行日志(traceId+耗时)、灵活控制失败行为(解析校验失败中断,标准化写入失败只记日志)。显式编排器可以精确控制每个节点。
三级容错:全局开关(整个流水线能不能开)→ 节点开关(某个工位今天开不开)→ try-catch兜底(某个工人操作失误不会炸掉工厂)。
面试讲法:“Pipeline用显式编排器模式,DeviceReportWorkflowOrchestrator按顺序调用7个处理节点。不用经典责任链是因为需要节点动态启停、独立执行日志和灵活的失败控制。三级容错保证单个节点异常不影响整个系统。”
16. 模板方法模式(Template Method)
一句话:父类定义流程骨架,子类填充具体步骤。
生活类比:做菜食谱。“热锅→放油→放食材→翻炒→出锅”,流程对所有菜一样,但放什么食材、怎么调味每道菜不同。
public abstract class DataExporter {
// 模板方法:流程固定(final防止子类改流程)
public final void export() {
connect();
List<Data> data = queryData(); // 子类实现
transform(data); // 子类实现
write(data);
disconnect();
}
private void connect() { /* 公共逻辑 */ }
private void write(List<Data> data) { /* 公共逻辑 */ }
private void disconnect() { /* 公共逻辑 */ }
protected abstract List<Data> queryData();
protected abstract void transform(List<Data> data);
}
public class MysqlExporter extends DataExporter {
protected List<Data> queryData() {
return jdbcTemplate.query("SELECT * FROM devices");
}
protected void transform(List<Data> data) { /* MySQL特有转换 */ }
}
模板方法和策略的区别:模板方法用继承,流程固定步骤可变;策略用组合,算法整体可替换。Spring的JdbcTemplate就是典型应用。
17. 命令模式(Command)
一句话:把请求封装成对象,支持排队、撤销、日志。
生活类比:餐厅点菜。服务员把你的请求写在纸条上(命令对象),厨房按纸条做菜。纸条可以排队、可以撤销、可以记录。
public interface Command {
void execute();
void undo();
}
public class DeviceRestartCommand implements Command {
private Long deviceId;
public void execute() {
mqttClient.publish("device/" + deviceId + "/cmd", "restart");
}
public void undo() {
operationLog.markCancelled(deviceId, "restart");
}
}
Java里Runnable接口就是命令模式的体现,把要执行的任务封装成对象传给线程。项目里如果做设备远程控制的撤销和批量执行,可以用命令模式。
18. 状态模式(State)
一句话:不同状态下,同一个操作的行为完全不同。
生活类比:自动售货机。待机时按按钮提示投币,已投币时按按钮出货,缺货时按按钮退币。
项目里设备状态管理用的是枚举+条件分支,不是经典状态模式:
public enum UnifiedDeviceStatusEnum {
ONLINE(0, "在线"),
OFFLINE(1, "离线"),
WARNING(2, "预警"),
FAULT(3, "故障"),
DISABLED(-1, "已禁用");
public static UnifiedDeviceStatusEnum fromNodeStatus(Object nodeStatus) {
if (nodeStatus == null) return OFFLINE;
if (nodeStatus instanceof Integer) {
UnifiedDeviceStatusEnum result = getByCode((Integer) nodeStatus);
return result != null ? result : OFFLINE;
}
String str = nodeStatus.toString().trim();
switch (str.toUpperCase()) {
case "ONLINE": case "正常": case "在线": return ONLINE;
case "WARNING": case "预警": return WARNING;
case "FAULT": case "故障": return FAULT;
case "DISABLED": case "已禁用": return DISABLED;
default: return OFFLINE;
}
}
}
状态解析器做多级判断:
@Service
public class DeviceStatusResolverImpl implements IDeviceStatusResolver {
public UnifiedDeviceStatusEnum resolveStatus(Instruments device) {
if (device.getEnable() != null && device.getEnable() == 0) {
return DISABLED;
}
String cachedStatus = cacheClient.getStatus(device.getNodeId());
if (cachedStatus != null) {
return UnifiedDeviceStatusEnum.getByCode(Integer.parseInt(cachedStatus));
}
return fromDbStatus(device.getNodeStatus());
}
}
目前用枚举+条件分支就够了,因为状态间的行为差异不大(主要是前端展示不同)。如果以后状态转换规则变复杂(比如预警持续10分钟自动升级为故障),可以重构为经典状态模式,每个状态一个类,类里定义转换规则。
19. 迭代器模式(Iterator)
一句话:提供顺序访问集合元素的方法,不暴露内部结构。
生活类比:电视遥控器换台。你按"下一个"就换到下一个台,不需要知道电视内部怎么存储频道。
Java集合框架已经帮你实现好了。项目里大量使用内置迭代器:
// Redis扫描设备状态key
Cursor<String> cursor = redisTemplate.scan(
ScanOptions.scanOptions().match("device_status:*").build());
while (cursor.hasNext()) {
String key = cursor.next();
}
// MongoDB遍历归档文档
MongoCursor<Document> cursor = collection.find().iterator();
while (cursor.hasNext()) {
Document doc = cursor.next();
}
没有自定义迭代器,但内置迭代器无处不在。
20. 备忘录模式(Memento)
一句话:保存对象的状态快照,需要时可以恢复。
生活类比:游戏存档。打Boss前存个档,打输了读档重来。
public class DeviceConfig {
private String name;
private int port;
public ConfigMemento save() {
return new ConfigMemento(name, port);
}
public void restore(ConfigMemento memento) {
this.name = memento.getName();
this.port = memento.getPort();
}
}
public class ConfigHistory {
private List<ConfigMemento> history = new ArrayList<>();
public void save(ConfigMemento memento) { history.add(memento); }
public ConfigMemento undo() {
return history.isEmpty() ? null : history.remove(history.size() - 1);
}
}
项目里如果做设备配置的历史版本回滚,可以在每次修改前保存快照,支持多步撤销。编辑器的Ctrl+Z就是典型应用。
21. 中介者模式(Mediator)
一句话:多个对象之间的通信不直接互相引用,都通过一个中介。
生活类比:房产中介。买家和卖家不直接联系,都通过中介沟通。
项目里Spring Event就有中介者的味道:
没有中介:MqttHandler ←→ PipelineService
MqttHandler ←→ StreamMediaService
MqttHandler ←→ AlarmService
→ MqttHandler认识所有人
有中介: MqttHandler ←→ Spring事件总线 ←→ PipelineService
←→ StreamMediaService
←→ AlarmService
→ 所有人都只跟事件总线打交道
各模块不直接通信,通过事件总线间接交互,降低了耦合度。
22. 访问者模式(Visitor)
一句话:在不修改类的前提下,给类添加新的操作。
生活类比:体检。你(数据结构)躺在体检台上不动,内科医生、眼科医生、牙科医生(访问者)轮流过来做不同检查。你不变,但每个医生对你做的操作完全不同。
public interface DeviceComponent {
void accept(DeviceVisitor visitor);
}
public class Sensor implements DeviceComponent {
public void accept(DeviceVisitor visitor) { visitor.visit(this); }
}
public interface DeviceVisitor {
void visit(Sensor sensor);
void visit(Gateway gateway);
}
// 性能分析
public class PerformanceVisitor implements DeviceVisitor {
public void visit(Sensor sensor) { /* 检查温度 */ }
public void visit(Gateway gateway) { /* 检查连接数 */ }
}
// 安全审计(加新操作不改已有类)
public class SecurityVisitor implements DeviceVisitor {
public void visit(Sensor sensor) { /* 检查固件版本 */ }
public void visit(Gateway gateway) { /* 检查访问控制 */ }
}
适合数据结构稳定但操作经常变化的场景。缺点是新增元素类型时需要修改所有Visitor。
23. 解释器模式(Interpreter)
一句话:定义一种语言的语法规则,写一个解释器来解释执行。
生活类比:翻译官。你给一段英文,翻译官按语法规则解释,输出中文。
项目里Drools规则引擎就是解释器。业务规则存在数据库中,运营人员可以动态配置,不需要改代码:
@Component
public class DecisionRuleEngine {
private KieContainer kieContainer;
// 从数据库加载规则,编译为DRL
public void reloadRules() {
KieServices kieServices = KieServices.Factory.get();
KieFileSystem kfs = kieServices.newKieFileSystem();
List<BdDecisionRule> dbRules = loadPublishedRules();
for (BdDecisionRule rule : dbRules) {
String drlContent = buildDrlFromRule(rule, idx++);
kfs.write("src/main/resources/db_rule_" + rule.getId() + ".drl",
drlContent);
}
KieBuilder kieBuilder = kieServices.newKieBuilder(kfs).buildAll();
KieModule kieModule = kieBuilder.getKieModule();
this.kieContainer = kieServices.newKieContainer(kieModule.getReleaseId());
}
// 解释执行规则
public List<BdDecisionAdvice> evaluate(BdRiskScore riskScore) {
KieSession session = kieContainer.newKieSession();
session.insert(riskScore);
for (BdDecisionRule rule : loadPublishedRules()) {
session.insert(rule);
}
session.fireAllRules(); // 解释执行所有匹配的规则
// 为匹配的规则生成决策建议
for (BdDecisionRule rule : rules) {
if (isRuleMatched(riskScore, rule)) {
BdDecisionAdvice advice = new BdDecisionAdvice();
advice.setAdviceContent(rule.getActionDef());
advice.setReasoning("风险评分=" + riskScore.getOverallScore()
+ ", 触发规则: " + rule.getRuleName());
decisionAdviceMapper.insert(advice);
}
}
session.dispose();
return advices;
}
private String buildDrlFromRule(BdDecisionRule rule, int index) {
StringBuilder sb = new StringBuilder();
sb.append("package com.example.iot.bigdata.rules;\n\n");
sb.append("rule \"db_rule_").append(rule.getId()).append("\"\n");
sb.append("when\n");
sb.append(" ").append(rule.getConditionExpr()).append("\n");
sb.append("then\n");
sb.append(" // Action handled by Java code\n");
sb.append("end\n");
return sb.toString();
}
}
数据库里的规则长这样:conditionExpr = “BdRiskScore(overallScore >= 0.7)”,actionDef = “立即通知运维人员”。运营人员改阈值从0.7到0.8,在数据库里改一下,点"重新加载规则",新规则立即生效。不用改代码,不用重新部署。
面试讲法:“决策规则引擎用解释器模式,基于Drools实现。业务规则存数据库,动态编译为DRL解释执行。调整风控策略不需要改代码,实现了业务规则与代码解耦。”
四、一条消息的完整旅程
把23种模式串起来看。一条设备消息从进来到落库,经过了多少种模式:
设备发送 MQTT 消息
│
▼
MqttCallback 收到消息
│
▼
MqttMessageHandler.handleMessage()
│ 【观察者】发布 MqttMessageEvent
▼
Spring 事件总线(也是【中介者】思想)
│
├──→ DeviceDataEventListener 【观察者】监听
│ │
│ ▼
│ DeviceReportWorkflowOrchestrator.execute()
│ │ 【责任链】显式编排器,7个节点依次执行
│ │ 【单例】IdGenerator 生成 traceId
│ │
│ ├── n1 Parser 解析
│ ├── n2 Validator 校验
│ ├── n3 Normalizer 标准化
│ ├── n4 InfluxDB 写入
│ ├── n5 Redis 实时态
│ ├── n6 告警处理
│ └── n7 错误处理
│
└──→ StreamMediaEventListener 【观察者】监听
Controller 层
│ 【代理】@Log 自动记日志
│ 【代理】@RateLimit 自动限流
│ 【工厂方法】@Bean 注入 Service
▼
Service 层
│ 【策略】ExtractionStrategy 按数据源选择
│ 【空对象】DefaultExtractionStrategy 兜底
│ 【外观】UnifiedApiGatewayService 统一网关
│ 【建造者】DeviceContextBuilder 构建AI上下文
│ 【抽象工厂】SM4Utils 按KeyType生产加密器
│ 【解释器】DecisionRuleEngine 动态规则
│ 【组合】SysMenu 菜单树
│ 【适配器思想】convertNodeStatus 格式转换
│ 【状态思想】UnifiedDeviceStatusEnum 状态管理
▼
数据层(InfluxDB + Redis + MongoDB + MySQL)
一条消息,十几种模式。这不是为了用而用,是业务复杂度到了那个程度,不用这些模式代码就维护不动。
五、六对最容易搞混的模式
工厂方法 vs 抽象工厂
工厂方法生产一个产品(@Bean → RedisManager),抽象工厂生产一整套(SM4Utils按KeyType生产密钥+加密器)。判断标准:生产的是一个东西还是一套东西?
策略 vs 状态
策略是外部选择用哪个算法(任务说"我是MYSQL"→选MySQL策略),状态是内部自动切换(设备连续3次超时→自动变离线)。判断标准:谁来决定?外部选=策略,内部自动切=状态。
策略 vs 责任链
策略是多选一(sourceType="MYSQL"→只执行MySQL策略),责任链是全部按顺序走一遍(解析→校验→标准化→写入→告警)。判断标准:选一个做,还是全部做?
外观 vs 适配器
外观是简化一组操作(API网关把5个操作包成1个方法),适配器是转换接口格式(把"ONLINE"/“在线”/0统一转成"0")。判断标准:简化操作还是转换格式?
观察者 vs 责任链
观察者是一对多广播(发事件,所有监听者都收到),责任链是一对一传递(每个节点处理完传给下一个)。判断标准:广播还是传递?
代理 vs 装饰器
代理控制访问(限流切面:超限不让你执行),装饰器增强功能(BufferedInputStream:让读操作更快)。判断标准:控制能不能用,还是让你用得更好?
六、面试怎么讲
面试官问"项目里用过哪些设计模式",按数据流顺序讲,3分钟:
"设备接入层,MQTT模块用观察者模式,通过Spring Event发布MqttMessageEvent,Pipeline和流媒体模块各自监听,MQTT层不知道谁在处理,加新模块不用改发布者。
数据处理层,Pipeline用显式编排器,责任链的变体,7个节点依次执行,支持节点动态启停、独立执行日志、三级容错。不用经典责任链是因为需要这些能力。
框架层,AOP代理模式,@Log和@RateLimit注解的方法被切面自动代理,横切关注点与业务分离。
大数据层,策略模式做数据抽取,Map注册表按sourceType分发,空对象模式兜底。Drools规则引擎用解释器模式,运营改规则不用改代码。
其他还有IdGenerator单例、ShiroConfig工厂方法、SM4Utils抽象工厂、DeviceContextBuilder建造者、SysMenu组合模式、API网关外观模式。"
写在最后
学设计模式之前,我觉得它就是面试八股。学完之后我发现,模式不是"背下来"的,是"认出来"的。当你读到一段代码,发现"这不就是策略模式吗"“这不就是观察者吗”,那就才是真的懂了。
最好的学习方式不是看书看视频,而是找一个真实项目,跟AI开始对话,从一条数据的完整链路开始,看它经过哪些类、哪些方法,然后问自己:这里为什么这么写?如果不这么写会怎样?
因为答案往往就是某个设计模式。
更多推荐


所有评论(0)