前言

实习前我觉得设计模式就是八股文。单例嘛,私有构造函数加个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开始对话,从一条数据的完整链路开始,看它经过哪些类、哪些方法,然后问自己:这里为什么这么写?如果不这么写会怎样?

因为答案往往就是某个设计模式。

更多推荐