数据引擎(Data Engine)模块属于EISaaS产品架构的框架层,它是元数据与业务数据之间的桥梁,可以将配置的元数据生成SQL语句,然后通过JDBC执行SQL语句获得报表需要的数据。
在设计上,数据引擎需要支持多种SQL语句的生成。为更灵活的支持SQL语句生成的功能,需要提供SQL语法树,以及语法树解析器。不过,在当初的详细设计方案中,只具备了计划设计的初步功能。
数据引擎模块的命名空间为:en.com.chengjun.eisaas.framework.engine.data,项目名称为eisaas-framework-engine-data。它主要由service、executor、assembler、statement、resource以及datasource等子模块构成。如前所述,我的建议是为模块定义门面类以提升API的易用性,在数据引擎模块扮演门面类的是service子模块。下图为当初对数据引擎模块的设计图:
datasource子模块
首先介绍datasource子模块,它包含的类可以认为是数据引擎的模型类,是对数据库数据的抽象,数据引擎访问数据库得到的结果就以下图所示的Table对象来呈现:
由于它是我们自行定义的模型对象,故而可以与基础设施彻底解耦,而它又具有规范的数据结构,可以将其传递给实体引擎,作为映射实体的数据值。Table相当于数据表,Record是其中的一行,Field是数据表的列:
public class Table {
private List records = new ArrayList();
public List getRecords() {
return records;
}
public void addRecord(Record record) {
records.add(record);
}
}
public class Record {
private List fields = new ArrayList();
public List getFields() {
return fields;
}
public void addField(Field field) {
fields.add(field);
}
public Object getValue(String fieldName) {
for (Field field : fields) {
if (field.getName().equalsIgnoreCase(fieldName)) {
return field.getValue();
}
}
return null;
}
}
public class Field {
private String name;
private Object value;
private String className;
public Field(String name, Object value) {
this.name = name;
this.value = value;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Object getValue() {
return value;
}
public void setValue(Object value) {
this.value = value;
}
public String getClassName() {
return className;
}
public void setClassName(String className) {
this.className = className;
}
}
resource子模块
接着介绍resource子模块,它负责完成从数据集元数据到SQL语句各个组成部分的解析,解析的结果封装为Variable(变量),例如组成SQL的数据表名是一个Variable,Where条件表达式也是一个变量,解析后的各个变量最终存储到SqlResource中,具体的解析工作则由SqlResourceParser完成。如下图所示:
SqlResourceParser的作用会在asembler模块中有所体现。
statement子模块
如果说resource模块是完成元数据到SQL语句各个组成部分的转换,则statement模块就是要把这些组成部分塞到实现准备好的模板,最终形成一个完整的SQL语句。设计类图如下所示:
模块主要接口为SqlStatment。它继承了SqlResourceProvider接口可以设置SqlResource对象。该对象存储的信息在装配SQL语句的时候需要使用。SqlResource类在datasource子模块。
SqlStatement提供了getSql()方法,用以获得数据引擎想要执行的SQL语句。在其内部,会调用SqlTemplate的evaluate()方法对模板字符串进行解析。执行不同操作的SQL语句需要与之对应的模板,故而可以将SqlStatement与SqlTemplate看作是两个结构和变化方向都一致的继承体系。
这两个继承体系都运用了模板方法模式(Template Method Pattern)。
由于SQL语句遵循通用的格式,SqlStatement就为各种CRUD操作定义通用的SQL模板。它们的差异主要体现在包含变量占位符的SQL模板值和各自对应的变量,一旦统一这些差异后,解析模板的逻辑都是一样的。与之有关的主要代码如下所示:
public interface VariableUtil {
String FUNCTION = "${function}";
String DISTINCT = "${distinct}";
String COLUMNS = "${columns}";
String TABLE_NAME = "${tableName}";
String LEFT_JOIN = "${leftJoin}";
String WHERE = "${where}";
String ORDER_BY = "${orderBy}";
String GROUP_BY = "${groupBy}";
String PARAMETER = "${parameter}";
String SET = "${set}";
}
public interface SqlTemplate {
String evaluate();
void addVariable(Variable variable);
}
public abstract class SqlTemplateBase implements SqlTemplate {
private List variables;
private String replacement;
public String evaluate() {
String template = getTemplate();
for (Variable variable : variables) {
String regex = "\\$\\{" + variable.getName() + "\\}";
template.replaceAll(regex,variable.getValue());
}
return template;
}
public void addVariable(Variable variable) {
variables.add(variable);
}
protected abstract String getTemplate();
}
public class QuerySqlTemplate extends SqlTemplateBase {
@Override
protected String getTemplate() {
return "select " + VariableUtil.COLUMNS +
" from " + VariableUtil.TABLE_NAME +
" where " + VariableUtil.WHERE;
}
}
public final class Variable {
public Variable(String name, String value) {}
public String getName() {}
public String getValue() {}
}
表面上,SqlStatement才是生成SQL语句的对象,但实际上,解析变量与模板获得SQL语句的职责都委派给了SqlTemplate,如下代码所示,getSql()方法的内部调用了SqlTemplate的evaluate()方法。SqlStatement更像是一个指导者,它允许调用者根据自己的选择添加对应的变量,同时还要确保它关联的是正确的模板类,这两个工作分别由addVariable()和createTemplate()方法完成。只有createTemplate()存在差异,因此,调用该方法的构造函数算是一个模板方法,如下示意代码所示:
public interface SqlStatement extends SqlResourceProvider {
public String getSql();
public void addVariable(Variable variable);
}
public abstract class SqlStatementBase implements SqlStatement {
private SqlResource sqlResource;
protected SqlTemplate template;
public SqlStatementBase() {
template = createTemplate();
}
public void addVariable(Variable variable) {
template.addVariable(variable);
}
public String getSql() {
return template.evaluate();
}
protected abstract SqlTemplate createTemplate();
}
public class QuerySqlStatement extends SqlStatementBase {
@Override
protected SqlTemplate createTemplate() {
return new QuerySqlTemplate();
}
}
虽说都运用了模板方法模式,我在设计时,却为模板方法的抽象同时定义了接口和抽象类,如SqlStatement与SqlStatementBase,SqlTemplate与SqlTemplateBase。现在看来,根本没有必要,完全可以将二者合并为一个抽象类,分别命名为SqlStatement与SqlTemplate。这一冗余设计应该是受到自己对“面向接口设计”之误解所限。
该设计还存在一个问题,即它具有Martin Fowler所说的“平行继承体系(Parallel Inheritance Hierarchies)”坏味道。这个坏味道的症状为:当你为某个类增加一个子类,必须也为另一个类相应增加一个子类。让我们换一个角度来看看这一设计: