设计模式与报表系统:模板方法定义报表格式、策略模式选择图表、建造者构建报表

报表系统,说白了就是数据展示的最后一公里。我这些年做过不少报表相关的项目,从简单的Excel导出到复杂的BI看板,发现一个规律:报表的骨架是固定的,但血肉是千变万化的

今天咱们就聊聊怎么用三种设计模式,把报表系统拆得明明白白。模板方法管格式、策略模式管图表、建造者管构建——各司其职,互不干扰。

核心思路: 报表 = 固定流程(模板方法) + 可变图表(策略) + 灵活构建(建造者)

1. 模板方法:把报表的骨架焊死

你想想看,不管什么报表,流程是不是都差不多?先取数据、再处理、然后渲染、最后输出。这个流程是固定的,但每一步的具体实现可以不同。

模板方法模式就是干这个的。我在项目中遇到过好几次,业务方说「我要加一种新报表」,结果发现80%的代码都是重复的。后来我用模板方法把公共流程抽出来,新报表只需要重写几个关键步骤就行。

// Java 示例:报表模板
public abstract class ReportTemplate {
    // 模板方法,定义报表生成流程
    public final Report generate() {
        // 1. 获取数据
        Data data = fetchData();
        // 2. 处理数据
        Data processed = processData(data);
        // 3. 渲染报表
        String content = render(processed);
        // 4. 输出报表
        return output(content);
    }

    // 子类必须实现的方法
    protected abstract Data fetchData();
    protected abstract String render(Data data);

    // 可选覆盖的方法(钩子方法)
    protected Data processData(Data data) {
        return data; // 默认不做处理
    }

    protected Report output(String content) {
        return new Report(content); // 默认输出
    }
}

// 具体报表:销售报表
public class SalesReport extends ReportTemplate {
    @Override
    protected Data fetchData() {
        return database.query("SELECT * FROM sales");
    }

    @Override
    protected String render(Data data) {
        return "

销售报表

" + data.toHtml(); } @Override protected Data processData(Data data) { // 销售数据需要汇总 return data.summarize(); } }
我的习惯: 模板方法里尽量多用钩子方法(hook),给子类留点「偷懒」的空间。比如加个 shouldAddFooter(),默认返回 true,子类想不要页脚就重写一下。

2. 策略模式:图表想怎么画就怎么画

报表里最头疼的是什么?图表。柱状图、折线图、饼图、散点图……用户今天喜欢这个,明天又想要那个。如果写一堆 if-else,代码会变得又臭又长。

策略模式就是解决这个问题的。把每种图表封装成一个策略,报表系统只负责「选哪个策略」,不关心「策略怎么画」。

// C++ 示例:图表策略
class ChartStrategy {
public:
    virtual ~ChartStrategy() = default;
    virtual std::string draw(const Data& data) = 0;
};

class BarChart : public ChartStrategy {
public:
    std::string draw(const Data& data) override {
        return "绘制柱状图: " + data.toJson();
    }
};

class LineChart : public ChartStrategy {
public:
    std::string draw(const Data& data) override {
        return "绘制折线图: " + data.toJson();
    }
};

// 报表上下文,持有策略
class ReportContext {
private:
    std::unique_ptr<ChartStrategy> chartStrategy;
public:
    void setChartStrategy(std::unique_ptr<ChartStrategy> strategy) {
        chartStrategy = std::move(strategy);
    }

    std::string generateChart(const Data& data) {
        if (!chartStrategy) {
            throw std::runtime_error("请先设置图表策略");
        }
        return chartStrategy->draw(data);
    }
};

嗯,这里要注意:策略模式不是让你把所有图表类都塞到一个工厂里。我见过有人把策略模式和工厂模式混着用,结果策略类越来越多,反而不好维护。我的建议是:策略类只负责「怎么画」,不负责「选哪个」。选哪个是调用方的事。

3. 建造者模式:报表对象太复杂?一步步来

一个完整的报表对象,包含标题、表头、数据行、图表、页脚、样式……如果直接用构造函数传参,那参数列表能写满一屏幕。而且有些参数是可选的,有些是必填的,很容易搞错。

建造者模式就是为这种场景设计的。它把对象的构建过程拆成多个步骤,每一步都可以独立配置,最后一步才生成最终对象。

// Java 示例:报表建造者
public class Report {
    private String title;
    private List<String> headers;
    private List<List<String>> rows;
    private String chart;
    private String footer;

    private Report() {} // 私有构造,只能通过建造者创建

    public static class Builder {
        private Report report = new Report();

        public Builder setTitle(String title) {
            report.title = title;
            return this;
        }

        public Builder setHeaders(List<String> headers) {
            report.headers = headers;
            return this;
        }

        public Builder addRow(List<String> row) {
            if (report.rows == null) {
                report.rows = new ArrayList<>();
            }
            report.rows.add(row);
            return this;
        }

        public Builder setChart(String chart) {
            report.chart = chart;
            return this;
        }

        public Builder setFooter(String footer) {
            report.footer = footer;
            return this;
        }

        public Report build() {
            // 校验必填字段
            if (report.title == null || report.title.isEmpty()) {
                throw new IllegalStateException("标题不能为空");
            }
            return report;
        }
    }
}

// 使用示例
Report report = new Report.Builder()
    .setTitle("月度销售报表")
    .setHeaders(Arrays.asList("月份", "销售额", "利润"))
    .addRow(Arrays.asList("1月", "100万", "20万"))
    .addRow(Arrays.asList("2月", "120万", "25万"))
    .setChart("柱状图")
    .setFooter("数据来源:ERP系统")
    .build();
我曾经踩过的坑: 建造者模式里,build() 方法一定要做校验。有一次我忘了校验标题,结果生成了一堆「无标题报表」,用户投诉说看不懂。从那以后,我每个建造者的 build() 方法里都会加至少三个校验。

4. 三种模式如何协同工作?

这三种模式不是孤立的,它们可以完美配合。我画了一张图来说明它们的关系:

报表系统三种设计模式协作图 模板方法:定义报表生成流程 获取数据 处理数据 渲染图表 输出报表 策略模式:选择图表类型 柱状图 | 折线图 | 饼图 | 散点图 建造者模式:构建报表对象 标题 → 表头 → 数据行 → 图表 → 页脚 模板方法控制流程,策略模式提供图表,建造者组装报表 三者各司其职,互不耦合,方便扩展和维护

从图上可以看得很清楚:模板方法像导演,控制着整个报表生成的「剧本」;策略模式像演员,根据剧本要求表演不同的「图表戏份」;建造者模式像制片人,把所有的「素材」组装成最终的「影片」。

5. 实战中的避坑指南

这三种模式组合起来用,确实很强大。但我在实际项目中遇到过几个坑,分享给大家:

  • 模板方法不要滥用继承:如果报表种类太多,继承层次会变得很深。我建议用组合代替继承,把公共逻辑放到工具类里。
  • 策略模式不要和工厂模式混淆:策略模式是「怎么画」,工厂模式是「创建谁」。我曾经在一个项目里把策略工厂和策略类混在一起,结果代码变得很难调试。
  • 建造者模式要支持链式调用:这是建造者的精髓。如果不支持链式调用,那还不如直接用构造函数。
  • 别忘了单元测试:每种模式单独测试,组合起来再测一次。我吃过亏,以为单独测没问题,组合起来就出 bug 了。
总结一下: 模板方法定流程、策略模式选图表、建造者组报表——这三种模式配合起来,能让你的报表系统既灵活又稳定。下次遇到报表需求,不妨试试这个组合拳。

公众号:蓝海资料掘金营,微信deep3321