当一个 Java 方法被多个布尔参数控制时,分支数量会快速增长,调用方也难以读懂。本文以订单导出为例,演示如何用命令对象、处理器注册表和不可变请求模型拆分流程,并讨论模式边界与常见误区。

问题背景:参数不多,流程却越来越难读

业务系统里经常有这样的导出方法:既支持 CSV,也支持 Excel;既可以直接下载,也可以发送邮件;有时还要包含敏感字段。最初只有一个参数,继续加 if 似乎没什么问题,直到方法变成这样:

public ExportResult exportOrders(
        List<Order> orders,
        boolean excel,
        boolean sendEmail,
        boolean includeSensitiveData) {

    if (!includeSensitiveData) {
        orders.forEach(order -> order.setCustomerPhone("***"));
    }

    byte[] content;
    String fileName;
    if (excel) {
        content = buildExcel(orders);
        fileName = "orders.xlsx";
    } else {
        content = buildCsv(orders);
        fileName = "orders.csv";
    }

    if (sendEmail) {
        emailService.send(fileName, content);
        return ExportResult.emailed(fileName);
    }

    return ExportResult.download(fileName, content);
}

它的问题不只是代码长。调用处的语义也很模糊:

exportOrders(orders, true, false, true);

三个布尔值分别代表什么,必须回到方法定义才能确认。更麻烦的是,布尔参数会形成组合:三个开关理论上有八种情况。以后再增加“是否压缩”,组合会变成十六种,而测试通常无法覆盖所有路径。

此外,示例代码还直接修改了传入的订单对象。调用方如果在导出后继续使用这些对象,可能拿到已经脱敏的数据。这类副作用隐藏在流程内部,很难从方法签名看出来。

第一步:先用明确类型替代布尔参数

重构不必一开始就套用设计模式。先让输入表达业务含义:

public enum ExportFormat {
    CSV, EXCEL
}

public enum DeliveryMethod {
    DOWNLOAD, EMAIL
}

public record ExportRequest(
        ExportFormat format,
        DeliveryMethod deliveryMethod,
        boolean includeSensitiveData,
        String email) {

    public ExportRequest {
        Objects.requireNonNull(format, "format");
        Objects.requireNonNull(deliveryMethod, "deliveryMethod");

        if (deliveryMethod == DeliveryMethod.EMAIL
                && (email == null || email.isBlank())) {
            throw new IllegalArgumentException("邮件交付必须提供邮箱地址");
        }
    }
}

调用代码随即清楚很多:

ExportRequest request = new ExportRequest(
        ExportFormat.EXCEL,
        DeliveryMethod.EMAIL,
        false,
        "ops@example.com"
);

这里使用 record 表达不可变请求,适用于 Java 16 及以上版本。较低版本可以改用普通的不可变类。构造阶段完成基本校验,也能避免无效参数一直流入核心流程。

不过,明确参数只能解决可读性问题。格式生成和交付方式仍是两个独立变化维度,如果继续写在一个方法里,分支依旧会增长。

用命令接口隔离格式生成

先把“如何生成文件”抽象为一个独立命令:

public interface ExportCommand {
    ExportFormat supports();

    ExportFile execute(List<OrderExportRow> rows);
}

public record ExportFile(String fileName, byte[] content) {
    public ExportFile {
        Objects.requireNonNull(fileName, "fileName");
        content = Arrays.copyOf(
                Objects.requireNonNull(content, "content"),
                content.length
        );
    }

    @Override
    public byte[] content() {
        return Arrays.copyOf(content, content.length);
    }
}

每种格式只负责自己的生成逻辑:

public final class CsvExportCommand implements ExportCommand {
    @Override
    public ExportFormat supports() {
        return ExportFormat.CSV;
    }

    @Override
    public ExportFile execute(List<OrderExportRow> rows) {
        StringBuilder csv = new StringBuilder("id,customer,phone,amount\n");
        for (OrderExportRow row : rows) {
            csv.append(row.id()).append(',')
               .append(escape(row.customer())).append(',')
               .append(escape(row.phone())).append(',')
               .append(row.amount()).append('\n');
        }
        return new ExportFile(
                "orders.csv",
                csv.toString().getBytes(StandardCharsets.UTF_8)
        );
    }

    private String escape(String value) {
        String safe = value == null ? "" : value.replace("\"", "\"\"");
        return "\"" + safe + "\"";
    }
}

ExcelExportCommand 可以使用项目已经选定的 Excel 库实现,但它仍然只暴露相同的 execute 接口。这样,主流程不需要了解工作簿、单元格或 CSV 转义规则。

用交付处理器拆开下载与邮件发送

文件生成后,还需要决定如何交付:

public interface DeliveryHandler {
    DeliveryMethod supports();

    ExportResult deliver(ExportFile file, ExportRequest request);
}

public final class EmailDeliveryHandler implements DeliveryHandler {
    private final EmailService emailService;

    public EmailDeliveryHandler(EmailService emailService) {
        this.emailService = emailService;
    }

    @Override
    public DeliveryMethod supports() {
        return DeliveryMethod.EMAIL;
    }

    @Override
    public ExportResult deliver(ExportFile file, ExportRequest request) {
        emailService.send(request.email(), file.fileName(), file.content());
        return ExportResult.emailed(file.fileName());
    }
}

public final class DownloadDeliveryHandler implements DeliveryHandler {
    @Override
    public DeliveryMethod supports() {
        return DeliveryMethod.DOWNLOAD;
    }

    @Override
    public ExportResult deliver(ExportFile file, ExportRequest request) {
        return ExportResult.download(file.fileName(), file.content());
    }
}

最终的编排服务只负责选择组件和组织步骤:

public final class OrderExportService {
    private final Map<ExportFormat, ExportCommand> commands;
    private final Map<DeliveryMethod, DeliveryHandler> handlers;

    public OrderExportService(
            List<ExportCommand> commandList,
            List<DeliveryHandler> handlerList) {
        this.commands = indexCommands(commandList);
        this.handlers = indexHandlers(handlerList);
    }

    public ExportResult export(List<Order> orders, ExportRequest request) {
        List<OrderExportRow> rows = orders.stream()
                .map(order -> toRow(order, request.includeSensitiveData()))
                .toList();

        ExportCommand command = requireCommand(request.format());
        ExportFile file = command.execute(rows);

        DeliveryHandler handler = requireHandler(request.deliveryMethod());
        return handler.deliver(file, request);
    }

    private OrderExportRow toRow(Order order, boolean includeSensitiveData) {
        String phone = includeSensitiveData ? order.getCustomerPhone() : "***";
        return new OrderExportRow(
                order.getId(),
                order.getCustomerName(),
                phone,
                order.getAmount()
        );
    }

    private ExportCommand requireCommand(ExportFormat format) {
        ExportCommand command = commands.get(format);
        if (command == null) {
            throw new IllegalArgumentException("不支持的导出格式:" + format);
        }
        return command;
    }

    private DeliveryHandler requireHandler(DeliveryMethod method) {
        DeliveryHandler handler = handlers.get(method);
        if (handler == null) {
            throw new IllegalArgumentException("不支持的交付方式:" + method);
        }
        return handler;
    }

    private Map<ExportFormat, ExportCommand> indexCommands(
            List<ExportCommand> commandList) {
        return commandList.stream().collect(Collectors.toUnmodifiableMap(
                ExportCommand::supports,
                Function.identity(),
                (left, right) -> {
                    throw new IllegalStateException("导出格式处理器重复:" + left.supports());
                }
        ));
    }

    private Map<DeliveryMethod, DeliveryHandler> indexHandlers(
            List<DeliveryHandler> handlerList) {
        return handlerList.stream().collect(Collectors.toUnmodifiableMap(
                DeliveryHandler::supports,
                Function.identity(),
                (left, right) -> {
                    throw new IllegalStateException("交付处理器重复:" + left.supports());
                }
        ));
    }
}

这里的命令模式不是为了消灭所有 if,而是把可独立变化、可单独测试的行为封装起来。新增 PDF 格式时,只需增加一个 PdfExportCommand 并完成注册,原有 CSV、Excel 和交付逻辑都不需要修改。

测试也从组合测试变成组件测试

重构后,可以分别验证脱敏、格式选择和交付行为:

@Test
void shouldMaskPhoneWithoutChangingOriginalOrder() {
    Order order = new Order(1L, "张三", "13800000000", new BigDecimal("99.00"));
    ExportRequest request = new ExportRequest(
            ExportFormat.CSV,
            DeliveryMethod.DOWNLOAD,
            false,
            null
    );

    ExportResult result = service.export(List.of(order), request);

    assertThat(new String(result.content(), StandardCharsets.UTF_8))
            .contains("***")
            .doesNotContain("13800000000");
    assertThat(order.getCustomerPhone()).isEqualTo("13800000000");
}

这个测试特意检查原始对象没有被修改。相比只断言文件生成成功,它守住了更重要的副作用边界。

常见坑

第一,不要把每个三五行的方法都做成命令类。只有当行为存在多个实现、需要独立测试,或者未来确实会独立变化时,抽象才有价值。固定且简单的逻辑留在服务中往往更容易维护。

第二,注册表必须检查重复项。直接使用 Map.put 会让后注册的实现悄悄覆盖前一个实现,最终表现取决于装配顺序。启动阶段明确失败,比运行时偶尔选择错误实现安全得多。

第三,不要让命令重新依赖整个编排服务,否则会形成循环职责。导出命令负责生成文件,交付处理器负责发送或返回,编排服务负责组织流程,边界应当保持单向。

第四,大文件导出不适合始终使用 byte[]。当数据规模可能明显增长时,应评估临时文件、输出流或对象存储,避免文件内容和中间对象同时占用堆内存。是否改为流式接口,应依据真实文件规模决定,而不是为了“通用”提前复杂化。

实践建议

面对带有多个布尔参数的方法,可以按三个问题逐步判断:参数能否替换成有业务含义的枚举或请求对象;不同分支是否代表独立变化的行为;拆分后能否减少共享状态和副作用。先改善模型,再引入模式,通常比直接创建大量接口更稳妥。

设计模式的价值不在于代码看起来更高级,而在于让新增变化发生在明确的位置。一次有效的重构,应让调用处更容易读懂,让各组件能够独立测试,也让错误配置尽可能早地暴露。如果拆分之后仍要同时修改许多实现,说明真正的变化边界可能还没有找对。

最后修改:2026 年 09 月 28 日
如果觉得我的文章对你有用,请随意赞赏