下载工作台
Java 编程实战

设计模式:可维护代码的套路

试读上半部分 · 解锁后可读全文

第 14 章 · 设计模式:可维护代码的套路

本章目标:理解设计模式解决的是变化中的复用问题;掌握创建型(单例、工厂方法、建造者)、结构型(装饰器、适配器)、行为型(策略、模板方法、观察者、责任链)共 8 个企业高频模式;能把 ch06 的定价策略识别为策略 + 模板方法组合;为 toolkit-demo 实现报表导出管线综合运用多模式;对照 python-dev 中的鸭子类型实现。

学时建议:5~6 小时(含 2 小时跟练)

前置:完成 ch05 OOPch06 接口与抽象类ch13 泛型


14.1 场景说明:代码为什么会"腐化"?

toolkit-demo 的报表功能一开始只有一个 if (type == "csv") ... else if (type == "json") ...;三个月后加了 Excel、PDF,方法膨胀到 200 行,每改一处都可能碰坏其他格式。设计模式的价值不在"炫技",而在把会变的东西隔离到可以替换的单元里

需求变化(加一种导出格式)
  无模式:改大方法 → 风险扩散
  有模式:新增一个类 → 旧代码不动(开闭原则)
原则一句话
开闭原则对扩展开放,对修改关闭
面向接口编程依赖抽象而非具体类
组合优于继承用"装配"替代"层级"
说明:本章所有代码为教学虚构示例,练习目录仍为 ~/learn-java/toolkit-demo

14.2 三大分类速览

分类关注点本章覆盖
创建型对象怎么 new单例、工厂方法、建造者
结构型类与对象怎么组合装饰器、适配器
行为型对象间怎么协作策略、模板方法、观察者、责任链

GoF 共 23 种,本模块只精讲企业代码与 Spring 源码里真正高频的 8 种;其余(门面、代理、享元等)在 spring-boot-web / spring-cloud-web 结合框架再认。


14.3 单例:全局唯一实例

适用:配置中心、连接池、注册表。推荐枚举写法——天然防反射、防序列化破坏:

package demo.pattern;

/** 枚举单例 — Effective Java 推荐 */
public enum AppConfig {

    INSTANCE;

    private String env = "dev";

    public String env() { return env; }

    public void setEnv(String env) { this.env = env; }
}
AppConfig.INSTANCE.setEnv("staging");
System.out.println(AppConfig.INSTANCE.env());   // 任何地方拿到的都是同一实例
写法问题
饿汉式(static final 字段)类加载即创建,浪费但安全
双重检查锁(DCL)必须配 volatile,写错就翻车
枚举最简、最安全,推荐
Spring 的 Bean 默认就是单例作用域——框架帮你管,业务代码里很少需要手写单例。本节重在理解原理与坑。

14.4 工厂方法:把 new 的决定权交出去

导出器创建逻辑集中管理,调用方只面对接口:

package demo.pattern;

public interface Exporter {
    String format();
    byte[] export(String content);
}

class CsvExporter implements Exporter {
    public String format() { return "csv"; }
    public byte[] export(String content) {
        return ("col_a,col_b\n" + content).getBytes(java.nio.charset.StandardCharsets.UTF_8);
    }
}

class JsonExporter implements Exporter {
    public String format() { return "json"; }
    public byte[] export(String content) {
        return ("{\"rows\":\"" + content + "\"}").getBytes(java.nio.charset.StandardCharsets.UTF_8);
    }
}

/** 工厂:按格式名创建对应导出器 */
public final class ExporterFactory {

    private ExporterFactory() {}

    public static Exporter create(String format) {
        return switch (format.toLowerCase()) {
            case "csv" -> new CsvExporter();
            case "json" -> new JsonExporter();
            default -> throw new IllegalArgumentException("未知格式: " + format);
        };
    }
}

新增 Excel 导出?只加一个类 + 工厂里加一个分支,调用方零改动。Spring 的 BeanFactory、日志的 LoggerFactory 都是此模式。


14.5 建造者:参数很多时的优雅构造

构造器参数超过 4 个就该考虑 Builder(ch11 的连接配置就是典型):

package demo.pattern;

public class ReportJob {

    private final String name;
    private final String cron;
    private final int retry;
    private final boolean async;

    private ReportJob(Builder b) {
        this.name = b.name;
        this.cron = b.cron;
        this.retry = b.retry;
        this.async = b.async;
    }

    public static class Builder {
        private String name = "unnamed";
        private String cron = "0 0 * * *";
        private int retry = 0;
        private boolean async = false;

        public Builder name(String name) { this.name = name; return this; }
        public Builder cron(String cron) { this.cron = cron; return this; }
        public Builder retry(int retry) { this.retry = retry; return this; }
        public Builder async(boolean async) { this.async = async; return this; }

        public ReportJob build() {
            if (retry < 0) {
                throw new IllegalArgumentException("retry 不能为负");
            }
            return new ReportJob(this);
        }
    }

    @Override
    public String toString() {
        return "ReportJob{name='%s', cron='%s', retry=%d, async=%b}"
            .formatted(name, cron, retry, async);
    }
}
ReportJob job = new ReportJob.Builder()
    .name("daily-report")
    .cron("0 30 6 * * *")
    .retry(3)
    .async(true)
    .build();
若只是数据传输,ch06 的 record + 紧凑构造器往往更轻;Builder 适合可选参数多 + 构造需校验的场景(StringBuilder、Lombok @Builder、Spring 的 RestTemplateBuilder 皆如此)。

14.6 策略模式:算法族可互换

ch06 的 PricingStrategy 就是策略模式——运行时按条件选择计价算法:

        ┌─ StandardPricing(原价)
Context ├─ MemberPricing(会员价)
        └─ BulkPricing(批量价)
// ch06 已实现的接口,此处看"如何选择策略"
PricingStrategy strategy = switch (customerLevel) {
    case "member" -> new MemberPricing(0.95);
    case "bulk"   -> new BulkPricing(10, 0.9);
    default       -> new StandardPricing();
};
double total = strategy.lineTotal(item, quantity);

识别特征:一组可互换的算法 + 同一个接口 + 运行时选择。JDK 里的 Comparator、Spring 的 ResourceLoader 都是策略。


14.7 模板方法:固定流程,开放步骤

ch06 的 AbstractBulkPricing 同样是模板方法——父类定流程骨架,子类填差异步骤:

priceFor()(父类 final)
   ├─ 取基价(固定)
   └─ applyRate()(abstract,子类实现)← 变化的只有这一步
策略 vs 模板方法区别
策略整个算法可换(组合,接口)
模板方法流程固定,只换步骤(继承,抽象类)

经验法则:整个替换用策略;骨架不变用模板方法。Spring 的 JdbcTemplate(固定获取连接/释放连接,SQL 由你填)是模板方法的经典。


14.8 观察者:一对多的事件通知

toolkit-demo 报表生成后通知多个订阅方(日志、邮件、Webhook):

package demo.pattern;

import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;

/** 事件监听器 */
public interface ReportListener {
    void onReportReady(String reportName);
}

/** 事件源 */
public class ReportPublisher {

    private final List<ReportListener> listeners = new CopyOnWriteArrayList<>();

以下内容需解锁后阅读

试读已结束。解锁本章 ¥5.00,或开通年度会员畅读全部教程。
年度会员 ¥199.00/年; 小紫 AI 工作台有效会员 ¥99.00/年

正文仅在服务端鉴权后下发,未付费无法获取下半部分内容。