动态配置型软件架构
------客户信息管理系统软件架构分析设计
1. 概述
客户信息管理系统是基于客户(王总)的超前的设计思想和简化二次开发客观需求,因此客户系统必须具有高度的可扩展性和适应性,并且是具有可配置的柔性系统,包括可定制的菜单,可定制的列表,可定制的表单;同时自动生成表单JS校验,自动生成CRUD(增,删,改查)SQL语句和变更更正审核SQL语句,并自动完成数据库操作。为了方便二次开发,适应新的业务需求和校验需求,我们还提供了柔性接口供实现。
2. 客户信息软件架构设计
客户信息管理系统设计分为数据库设计、软件概要设计及详细设计,其中数据库设计主要集中在模版组、模版信息的存储设计,动态菜单、数据字典的存储设计。软件设计包括模版及模版组信息的CRUD操作,基于模版信息的动态列表,动态表单,动态JS函数,定制JS函数,动态SQL,动态缺省数据,业务校验接口设计。
2.1. 显示模版设计
2.1.1. 模版数据库设计图(pdm):
2.1.2. 模版表间关系说明
从上图可以看出,模版组(TempletGroup)和模版组用户(TempletGroupUser)理论上是一对一关系,从而每个能管理客户信息的用户就拥有一个模版组,每个模版组包含若干个模版(Templet),每个模版对应与一个客户信息的业务表(TempletTable),每个模版对应多个模版字段(TempletField),模版字段中包括表单元素的显示属性,包括是否显示,是否必填,是否跨列显示,显示表单元素的类型(输入框,下拉框等),列表显示属性(显示的列名),及业务上的修改或变更是否需要审核等。因为客户关系管理系统的业务表对应多个模版,因此肯定有共有的属性,同样字段也有共有的属性,所以设计了TempletTable和TempletTableColumn两个表,同时这个TempletTable表还是定制菜单的基础数据。在理论上,一个用户拥有一个模版组,一个模版组拥有n个用户。
2.1.3. 模版表的详细设计
2.1.3.1. 表Templet的基本信息
名称 | Templet |
注释 | 模版组模版项目明细 |
2.1.3.2. 表Templet列
名称 | 注释 | 数据类型 | 文本说明 | 强制 |
templetId | 模版项目ID | INTEGER |
| TRUE |
templetTableId | 模版表ID | INTEGER | 对应templetTable | FALSE |
templetGroupId | 模版组ID | INTEGER |
| FALSE |
templetCategoryId | 模版类型ID | INTEGER | 不使用 | FALSE |
templetDisplayName | 模版显示名称 | VARCHAR2(100) |
| FALSE |
templetListJspFile | 列表显示模版文件(暂不使用) | VARCHAR2(200) | 不使用 | FALSE |
templetJspFile | 对应的JSP文件名称和路径(暂不使用) | VARCHAR2(200) | 不使用 | FALSE |
templetPageName | 对应的页面名称(暂不使用) | VARCHAR2(200) | 不使用 | FALSE |
templetDelegateClass | 模版对应的代理类(暂不使用) | VARCHAR2(200) | 不使用 | FALSE |
templetRanking | 显示顺序 | INTEGER |
| FALSE |
templetExtend | 模版扩展信息 | VARCHAR2(200) | 未使用 | FALSE |
templetTablePeriod | 报表周期,年,季度,月,周,日 | VARCHAR2(10) | 未使用 | FALSE |
templetTablePromptPeriod | 报表提示周期,年,季度,月,周,日 | VARCHAR2(10) | 未使用 | FALSE |
templetState | 状态,1可用,0不可用 | CHAR(1) |
| FALSE |
2.1.3.3. 表TempletField的基本信息
名称 | TempletField |
注释 | 模版表字段明细 |
2.1.3.4. 表TempletField列
名称 | 注释 | 数据类型 | 文本说明 | 强制 |
fieldId | 字段id | INTEGER |
| TRUE |
templetTableColumnId | 列流水号 | INTEGER | 对应templetTableColumn表 | FALSE |
templetId | 模版项目ID | INTEGER |
| FALSE |
columnName | 数据库表列名(暂不使用) | VARCHAR2(100) | 不使用 | FALSE |
fieldName | 列名称(暂不使用) | VARCHAR2(100) | 不使用 | FALSE |
fieldDisplayName | 显示名称 | CHAR(100) |
| FALSE |
formTypyId | 表单元素类型ID | INTEGER |
| FALSE |
formSelectId | 下拉表单对应的编码表ID | INTEGER |
| FALSE |
formDefaultValue | 表单元素默认值 | VARCHAR2(200) |
| FALSE |
formVerifyRuleId | 表单元素校验规则ID | INTEGER |
| FALSE |
formMaxlength | 表单输入数据长度 | INTEGER |
| FALSE |
formWidth | 表单元素宽度 | INTEGER |
| FALSE |
formHeight | 表单元素高度 | INTEGER |
| FALSE |
formIsSpan | 表单是否跨列显示,1是,0否 | CHAR(1) |
| FALSE |
isRequired | 是否为必填项,1必填,0可不填 | CHAR(1) |
| FALSE |
isDisplay | 是否可以显示,1可以,0不可 | CHAR(1) |
| FALSE |
isModify | 是否可编辑,1 可以,0 不可以 | CHAR(1) |
| FALSE |
isAlterCheck | 是否要变更复核,1是,0否 | CHAR(1) |
| FALSE |
isModifyCheck | 是否需要修改复核 | CHAR(1) |
| FALSE |
isListDisplay | 是否在列表中显示,1是,0 否 | CHAR(1) |
| FALSE |
isUserDefined | 是否允许用户自定义编辑,1是 0否 | CHAR(1) |
| FALSE |
fieldOrder | 是否对字段排序,默认为空,填写desc 或 asc | VARCHAR2(10) |
| FALSE |
fieldExtend | 表单后缀信息 | VARCHAR2(200) |
| FALSE |
fieldRanking | 显示顺序 | INTEGER |
| FALSE |
fieldAlign | 显示对齐方式,left 居左 ,center 居中 ,right 居右 | VARCHAR2(20) |
| FALSE |
fieldDisplayStyle | 字段显示样式 | VARCHAR2(20) |
| FALSE |
2.1.3.5. 表TempletTable的基本信息
名称 | TempletTable |
注释 | 业务树菜单项目表 |
2.1.3.6. 表TempletTable列
名称 | 注释 | 数据类型 | 文本说明 | 强制 |
templetTableId | 模版表ID | INTEGER |
| TRUE |
templetTableBaseType | 类型-法人表,自然人表 | VARCHAR2(100) |
| TRUE |
templetTableType | 表类型,0主表,1普通,2财务报表,3外部表,9公共基本表。如果该属性为空,表示该项是个节点,不是具体的业务 | CHAR(1) |
| FALSE |
templetTableParentId | 父表Id,如果是跟为0 | INTEGER |
| TRUE |
templetTableName | 物理表名称,如果是一级,二级节点为空 | VARCHAR2(100) |
| FALSE |
templetTableKey | 关键字,外部系统调用时指定该Key | VARCHAR2(100) | 暂时没有使用 | FALSE |
templetTableDisplay | 表显示名称 | VARCHAR2(100) |
| FALSE |
templetTableExtend | 表扩展信息 | VARCHAR2(200) | 记录了主表的加载类的名称 | FALSE |
templetTableJspForm | 表对应的Form JSP文件 | VARCHAR2(200) | 不使用 | FALSE |
templetTableJspList | 对应列表显示Jsp文件 | VARCHAR2(200) | 不使用 | FALSE |
templetTableJspControl | 表业务对应的 控制器 | VARCHAR2(200) | 菜单树对应得JSP文件 | FALSE |
templetTableRanking | 显示顺序号 | INTEGER |
| FALSE |
templetTableState | 状态,1可用,0不可用 | CHAR(1) |
| FALSE |
2.1.3.7. 表TempletTableColumn的基本信息
名称 | TempletTableColumn |
注释 | 数据库表物理字段,对应数据字典 |
1.1 表TempletTableColumn列
名称 | 注释 | 数据类型 | 文本说明 | 强制 |
templetTableColumnId | 列流水号 | INTEGER |
| TRUE |
templetTableId | 模版表ID | INTEGER |
| FALSE |
templetTableColumnName | 列名称 | VARCHAR2(100) |
| TRUE |
templetTableColumnComment | 列说明 | VARCHAR2(500) |
| FALSE |
templetTableColumnType | 列类型 | VARCHAR2(100) |
| TRUE |
templetTableColumnSize | 列长度 | INTEGER |
| FALSE |
templetTableColumnNull | 是否可以Null,Y是,N否 | CHAR(1) |
| TRUE |
templetTableColumnRanking | 列序号 | INTEGER |
| FALSE |
2.2. 基于模版的列表设计
2.2.1. 列表设计示意图:
如图所示,从类及接口的命名方面就可以很清楚地知道设计思想,DataMap是接口,DefaultDataMap是缺省实现类,GeneralListMap是具体实现类,ColDatamgr和ExtendColDataMgr是管理DataMap接口的类。listTemplet.jsp是jsp列表模版,模板使用ColMetaInfo来展现列表,普通的Jsp只需要include列表模板即可。
根据需求分析,列表页面是基于模版设置,显示相关表头,根据来自数据库的查询数据和模板设置,按一定的格式显示表体。因此我们要构架一个数据和显示的桥梁,显示属性是基于模版设置(封装到ColMetaInfo对象),数据是取之客户信息业务表(每行数据是放到HashMap,key是列名,value值是字段的值,把HashMap加入到ArrayList中),它们之间的桥梁就是列名。
2.2.2. ColDataMgr说明
ColDataMgr的主要功能是,获取相关的数据和显示信息。
2.2.2.1. 获取列显示信息
public ArrayList getColMetaInfos()
{
。。。 。。。
}
2.2.2.2. 获取显示数据ArrayList,list列表中每个元素为HashMap
public ArrayList getData()
{
。。。 。。。
}
2.2.3. 列表模版ListTemplet.jsp实现说明
使用jsp模版可以把显示格式和显示逻辑分离,同时我们可以看出ColDataMgr其实就是处理显示的逻辑和数据,而ListTemplet.jsp处理显示的格式。推而广之,我们可以根据不同的显示需求,产生多个类似 ListTemplet.jsp, 如适应打印格式的模版,适应导出Excel格式的模版,细心的读者在看源代码的过程中会发现,DataMap接口中都有类似的方法,如getExeclColMetaInfos(),getPrintColMetaInfos(),在缺省实现类中都有缺省实现,如果需要,直接在子类中覆写(override)此类方法即可。
<%
//获取后台传来的列表显示属性和显示数据
ArrayList colMetaInfos = (ArrayList) request.getAttribute("colMetaInfos");
ArrayList datas = (ArrayList) request.getAttribute("datas");
。。。 。。。
%>
//根据colMetaInfos显示表头
。。。 。。。
<TR>
//循环显示列名称
<TD class=ItemTitle height=20 nowrap>
<%=colMeta.getDisplayName()%>
</TD>
</TR>
//通过嵌套循环,根据colMetaInfos和Datas显示表体
。。。 。。。
//没有记录显示空白行
。。。。。。
</table>
2.2.4. ExtendColDataMgr说明
列表设计的范围是对单表的显示,使用ColDataMgr就可以了,为了达到多个关联表(一对一关系)显示,使用ExtendColDataMgr,。ExtendColDataMgr代码如下:
public class ExtendColDataMgr
{
。。。 。。。
//初始化类的相关属性
public ExtendColDataMgr(ArrayList oldColMetaInfos, ArrayList oldDatas, ArrayList newColMetaInfos, HashMap newDatas, String linkKey)
{
。。。 。。。
}
public ArrayList getDatas()
{
。。。 。。。
}
}
从代码上可以明显地看出,ExtendColDataMgr是对ColDataMgr的进一步扩展,其中ArrayList
(oldDatas)中 的每个元素都是HashMap,每个HashMap中都有linkKey,该键为外键的值(如110),而在新的数据是HashMap,其中的key就是linkKey(如110), value值是HashMap。从而可以把新的值逐行加到旧的值内部。下面是数据结构示意图:
OldDatas | newDatas | |||||
name | password | adress | linkKey | linkKey | age | sex |
张三 | 122 | 长春 | 110 | 112 | 23 | 1 |
李四 | 123 | 北京 | 111 | 111 | 25 | 0 |
王五 | 124 | 广州 | 112 | 110 | 24 | 1 |
2.3. 基于模版的表单设计
2.3.1. 表单设计示意图:
如上图所示,FormDisplayMgr是管理接口FormMap的管理类,AbstractFormMap类是缺省实现类,GeneralFormDisplayMap是通用的表单影射类,ExtendFormMap类是FormMap接口的代理封装类,ExtendFormMap类实现了FormMap接口,又封装了FormMap。同样showInfoTemplet.jsp和formTemplet.jsp是jsp模版文件,普通的表单jsp只要include模版jsp就行了。showInfoTemplet.jsp和formTemplet.jsp是使用DisplayAttribute来显示页面,其中ShowInfTemplet.jsp用于显示详细信息页面,formTemplet.jsp用于新增、修改页面。
根据需求,显示页面要基于模版配置,配置的属性经过实现接口FormMap的封装,产生DisplayAtttribute的ArrayList,然后再在页面上显示出来。当然还有自动校验Js语句,
2.3.2. ExtendFormMap说明
ExtendFormMap同样是对GeneralDisplayMap的再次封装,主要适用于多个表数据放入一个表单中:
public ArrayList getDisplayAttributes(){
ArrayList list = this.formMap.getDisplayAttributes();
list.addAll(new GeneralFormDisplayMap(colDisplayMetaDatas, selectValuesMap, colDefaultData).getDisplayAttributes());
return list;
}
2.3.3. FormSelectAttribute说明
在最初的设计中,并没有FormSelectAttribute类,是后来提出下拉菜单要求有级联关系,如省,市, 县之间有级联关系,原来的设计是在DisplayAttribute类中有二维数组,存储下拉菜单的value,text。为了满足新的需求,加上了FormSelectAttribute类,加上了子下拉菜单的名称,代码如下:
public class FormSelectAttribute {
private String selectName;
private String subSelectName;
private String[][] allOptions;
。。。。。。
}
2.3.4. 级联下拉菜单JS实现说明
2.3.4.1. 级联菜单联动
下拉菜单的级联是通过父菜单发生改变,子菜单也发生变化,即JS的onChange来触发:
οnchange="chageDiv_<%=formElementsKey%>(this, '<%=display.getSelectValues().getSubSelectName()%>')"
2.3.4.2. JS实现下拉菜单重绘
下拉菜单封装在DIV中,因此改变DIV中的内容即可实现,使用JS的innnerH
TML即可实现:
document.getElementById("div_" + targetId).innerHTML = str;
2.3.4.3. 多级(n级)菜单的联动
为了实现多级(n级)菜单的联动,使用了递归算法:
function nestFlushSubSelect(targetId)
{
。。。。。。
<%
if(selectMap.get(display.getSelectValues().getSubSelectName()) != null)
{
%>
nestFlushSubSelect(<%=display.getSelectValues().getSubSelectName()%>);
<%}%>
。。。。。。
}
2.4. 基于模版的JS校验设计
2.4.1. JsFunctionGenerator接口:
public interface JsFunctionGenerator {
public String getFunctionText();
}
2.4.2. ValidateFunctionGenerator的构造方法介绍
2.4.2.1. 使用模版定义的单纯的JS校验
public ValidateFunctionGenerator(long templetId)
{
。。。 。。。
}
2.4.2.2. 除了模版定义的单纯JS校验,另外定制的js校验 accessFunctions
public ValidateFunctionGenerator(long templetId, String accessFunctions)
{
。。。 。。。
}
2.4.2.3. 适用于多个表的同一表单操作,根据functionNo可以区别不同的校验
public ValidateFunctionGenerator(long templetId, String accessFunctions, String functionNo)
{
。。。 。。。
}
2.4.3. 表单元素间的互相校验
表单元素间的互相校验关系可以写在表单提交函数的后面。
2.5. 基于模版的SQL语句生成器设计
2.5.1. SQL语句生成器设计示意图:
2.5.2. BizSqlGenerator接口说明
其实从接口上考虑,并没什么稀奇之处,代码如下:
public interface BizSqlGenerator {
public String getSql();
public String[] getSqls();
}
即要么得到一个sql语句,如Select语句,要么得到Insert语句,要么得到一组语句,如审核Sql语句,还有是既要一条语句,又要一组语句,如撤销。
2.5.3. DefaultBizSqlGenerator缺省实现类说明
为此,我们设计了缺省实现类,如下:
public abstract class DefaultBizSqlGenerator implements BizSqlGenerator {
public String getSql()
{
return null;
}
public String[] getSqls()
{
return null;
}
}
2.5.4. SqlGenerator子类实现说明
其下的子类,都各有各的构造方法,从而初始化类的相关属性。从这个方面来说,面向对象编程其实是面向接口编程,接口内只定义外界调用的方法,至于怎么实现,实现的条件等问题由实际实现类来考虑,下面以审核SQL语句生成器例子说明,其他几个子类都类似。
2.5.4.1. 审核SQL语句生成器BizAuditSqlGenerator实现说明
//构造方法要完成初始化modifyInfos,修改的内容
private String[] ids;
private ArrayList modifyInfos = new ArrayList();
private String userId;
public BizAuditSqlGenerator(String[] ids, String userId)
{
this.ids = ids;
this.userId = userId;
init();
}
public String[] getSqls()
{
//使用modifyInfos生成审核SQL语句数组,这样就达到了既实现了接口的方法,又可以不增加新的接口方法的目的。
。。。 。。。
}
//初始化modifyInfos
private void init(){
。。。 。。。
}
2.6. 动态缺省数据设计
缺省数据是由模版定义的,在新增页面上,显示的就是模版定义的缺省数据,但有些缺省数据则直接和运行时间有关,而不是死的缺省数据。
2.6.1. DefaultValueMgr 实现说明:
相当于一个工厂方法,根据defaultKey来创建类并调用方法获取缺省值。从而为下一步二次开发提供了基础。
public class DefaultValueMgr {
public static String getValue(String defaultKey)
{
String ret = "";
if(defaultKey.equals(ClientConstant.DefaultValue.CURRENT_YEAR))
{
ret = (new Integer(DefaultDate.getCurrentYear())).toString();
}
。。。 。。。
return ret;
}
}
2.6.2. DefaultDate 实现说明:
public class DefaultDate {
public static int getCurrentYear()
{
return Calendar.getInstance().get(Calendar.YEAR);
}
public static int getCurrentMonth()
。。。 。。。
public static int getCurrentDay()
。。。 。。。
}
在ClientConstant的内部类ClientConstant,在模版中也是存储这样的缺省值,当程序获知这样的缺省值时,就会产生相关的缺省值,同样如果以后有新的需求也可以修改这样三个类的代码。
2.6.3. ClientConstant中的缺省值
该值同样保存在模版的缺省值字段中,当程序读到这样的值,就会自动调用相关的方法来获取缺省值
public static class ClientConstant {
public static final String CURRENT_YEAR = "$default_current_year";
public static final String CURRENT_MONTH = "$default_current_month";
public static final String CURRENT_DAY = "$default_current_day";
}
2.7. 业务校验接口设计
通过上图可知,FiltetFactory工厂类根据模版Id(templetId),调用静态常量类ClientConstant,获取相应的实现FilterOperator接口的实现类的全名(含包名FullName)字符串,FiltetFactory工厂类使用Class.forName(类的全名),创建FilterOperator接口的实现类,调用FilterOperator接口的实现类的接口方法即可。
2.7.1. 业务过滤工厂类FilterFactory
public class FilterFactory {
/**
* 根据templetTableId,从ClientConstant中得到过滤类的全名(含包名),
* 根据类名称创建过滤类
* @param templetTableId
* @return
*/
public static FilterOperator getFilterOperator(String templetTableId)
{
。。。 。。。
filterOperator = (FilterOperator)(Class.forName(filterOperatorClassName).newInstance();
。。。 。。。
}
工厂类依赖静态常量类的配置,同样你可以自己写配置文件,或properties,或xml,当使用bean.xml来配置,恭喜你,你在使用Spring,事实上Spring的底层也没什么高招,也就是用java的反射而已。
2.7.2. ClientConstant的静态内部类 FilterProperty
public static class FilterProperty
{
private static HashMap filterClassProperties = new HashMap();
//初始化filterClassProperties, templetId,和类FullName初始化
private static void initFilter()
{
。。。 。。。
filterClassProperties.put("100681", "com.iss.itreasury.clientmanage.customer.bizlogic.valid.CorpComeMarkInfo");
。。。 。。。
}
//根据模版Id(templetId),获取相应的实现FilterOperator接口的实现类的全名(含包名FullName)字符串
public static String getFilterClassName(String key)
{
initFilter();
return (String)filterClassProperties.get(key);
}
}
2.7.3. AbstractFilterOperator 运用了命令模式
public abstract class AbstractFilterOperator implements FilterOperator {
protected String msg;
public boolean isPass(HashMap hm) {
String act = (String) hm.get("operation");
boolean bool = false;
if (ClientConstant.BizWebOperation.BIZ_INSERT.equalsIgnoreCase(act)) {
bool = validInsert(hm);
} else if (ClientConstant.BizWebOperation.BIZ_MODIFY
.equalsIgnoreCase(act)
|| ClientConstant.BizWebOperation.BIZ_CORRECT
.equalsIgnoreCase(act)) {
bool = validModify(hm);
} else if (ClientConstant.BizWebOperation.BIZ_DELETE
.equalsIgnoreCase(act)) {
bool = validDelete(hm);
} else {
bool = false;
this.msg = "无法识别的操作类型";
}
return bool;
}
public String getAlterString() {
return this.msg;
}
。。。 。。。
}
2.8. 非模版的特殊需求设计
有些不适用于模版处理的情况,例如模版的定制、报表等模块开发,可以直接使用类似于其他模块那样,自行实现,与模版无关。
3. 设计总结
客户信息管理系统是基于模版定制的软件架构,是在数据库中定义的模版的基础之上,展示动态列表、动态表单、动态JS校验、动态SQL、自动执行的功能,完全实现了配置化管理。并且留有大量的可扩展的接口,简化和优化了二次开发,是非常有益的设计尝试和超前的设计理念。