设计模式与报表系统:模板方法定义报表格式、策略模式选择图表、建造者构建报表
报表系统,说白了就是数据展示的最后一公里。我这些年做过不少报表相关的项目,从简单的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();
}
}
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();
4. 三种模式如何协同工作?
这三种模式不是孤立的,它们可以完美配合。我画了一张图来说明它们的关系:
从图上可以看得很清楚:模板方法像导演,控制着整个报表生成的「剧本」;策略模式像演员,根据剧本要求表演不同的「图表戏份」;建造者模式像制片人,把所有的「素材」组装成最终的「影片」。
5. 实战中的避坑指南
这三种模式组合起来用,确实很强大。但我在实际项目中遇到过几个坑,分享给大家:
- 模板方法不要滥用继承:如果报表种类太多,继承层次会变得很深。我建议用组合代替继承,把公共逻辑放到工具类里。
- 策略模式不要和工厂模式混淆:策略模式是「怎么画」,工厂模式是「创建谁」。我曾经在一个项目里把策略工厂和策略类混在一起,结果代码变得很难调试。
- 建造者模式要支持链式调用:这是建造者的精髓。如果不支持链式调用,那还不如直接用构造函数。
- 别忘了单元测试:每种模式单独测试,组合起来再测一次。我吃过亏,以为单独测没问题,组合起来就出 bug 了。
公众号:蓝海资料掘金营,微信deep3321