一、 引言
B/S架构正越来越成为企业应用的主流模式,市场规模每年都在快速增长。然而与这种快速发展不匹配的是web开发效率仍然处于较低的水平。尽管各种企业应用的需求不尽相同,代码实现也千差万别,但毋庸置疑的是,其中存在的共性也非常多,比如,数据表的增、删、改、查询,报表,权限,流程等都能够凭直觉感觉到大量的相似性。如果我们一旦能用有效的方法,把这些共性提取出来,并能够灵活应对各种不同的需求变化,这样就会使web应用的开发效率大大提高,同时增加web应用架构的灵活性。
为了这个目标,很多公司进行了各种努力,开发出了各种web应用平台(业务中间件、业务架构平台等),期望在其平台上能够快速开发web应用。从目前的情况看,这些平台尚未得到大规模的应用,这里面有市场培育时间较短的原因,然而,更为根本的原因在于:这些平台虽然针对某种范围的需求进行了共性提取,使得开发效率大大提高,但如果需求超过了设计者的预期,其平台就不能或者很难来应对这种需求变化,简而言之,就是灵活性不够。
对这些平台进行分析后可以看出,其主要思想都是:在理解业务的前提下,对可预期的各种需求进行描述,同时有一个解析器对这些描述进行解析,生成应用。
为了尽可能地提高灵活性,我们在继承了描述思想的同时,引入了变量、内嵌对象等概念,但最重要的是引入了对象化的思想,因此,我们的方法简称为OOWD(Object Oriented Web Description)语言或OOWD平台。OOWD平台真的能够解决快速开发和灵活性的矛盾吗?让我们一起开始一段探索之旅。
二、 快速开发
下面从一个最简单的客户表的增加、修改、显示、查询开始,但为了突出重要的思想,对例子做了尽可能的简化。
1、 首先我们定义需要的所有基本信息
1) 对客户表的描述
[Table]
!Name=Custom // 唯一名称,在引用此表的时候用到
DbConn=DbConn_Common // 数据库连接信息指向描述对象 DbConn_Common
[*Field]
!Name=Id
[*Field]
!Name=Name // 数据库中的字段名
Desc= 姓名 // 字段描述,在 form 和查新列表中都会显示
NeedMark=y // 在 insert 、 update 等 sql 语句中,本字段的值需要加引号
InputType=Text // 在 form 中用 input 的 text 类型来表达
[*Field]
!Name=Age
Desc= 年龄
NeedMark=n
InputType=Text
其中数据库连接描述对象DbConn_Custom如下所示:
[Obj]
!Name=DbConn_Custom // 描述对象的唯一名称
Type=mysql // 数据库类型
Database=test // 数据库名称
Host=127.0.0.1 // 连接主机
User=root // 用户名
Password=123 // 密码
2) 对增加、修改、显示的form的描述,确定字段的显示顺序和方式
[FieldSet]
!Name=Custom_Form // 唯一名称,在指定 Form 的时候用到
TableNode=Custom // 指向表描述对象 Custom
[Row] // 表示下面的子节点显示在一行中
[*Field]
!Name=Name // 对应表中的相应字段
[*Field]
!Name=Age
3) 查询列表的描述
[FieldSet]
!Name=Custom_List // 唯一名称,在指定 List 的时候用到
TableNode=Custom // 指向表描述对象 Custom
[*Field]
!Name=Name // 对应相关的字段
[*Field]
!Name=Age
[Op] // 显示修改等链接
2、在完成了以上必须定义的基本信息以后,接着,只需要从我们已经抽象好的描述对象TableOp继承
[!Group]
!Ref=TableOp // 继承描述对象 TableOp
%Obj=Custom // 用变量 %Obj 的值替换描述对象 TableOp 中的变量 %(Obj)
运行OOWD解析器(这里我们仅以jsp为例),将生成4个jsp文件:CustomAdd.jsp、CustomEdit.jsp、CustomShow.jsp、CustomList.jsp。
可以看出,生成这4个页面的过程已经达到了最简洁的程度,这对于大多数了解或使用过快速开发平台的开发者并不陌生,不少的快速开发平台会让开发者通过界面填写一些必要的信息,点击按钮就生成一个应用。然而,在随后根据应用需求进行修改的过程可能会让开发者有不少担忧:快速开发平台对有些需求甚至无法实现,或者实现起来比较困难,以至于开发者在看到一个快速开发平台后的第一反应通常是:我的需求很特别,这个快速开发平台能应对吗?怎样才能满足这样的特殊需求?
OOWD平台能够消除开发者的这些担忧吗?让我们从剖析描述对象TableOp开始。
三、 描述对象剖析
1、 描述对象 TableOp 的定义如下:
[Group]
!Name=TableOp // 描述对象的名称,包含了下面的 4 个页面
[*Page]
!Name=%(Obj)Add // 页面名称,其中 %(Obj) 是变量,将被继承对象来指定,在本例中,被 Custom 替换
!Ref=DbAdd // 继承描述对象 DbAdd
[*Page]
!Name=%(Obj)Edit
!Ref=DbEdit
[*Page]
!Name=%(Obj)Show
!Ref=DbShow
[*Page]
!Name=%(Obj)List
!Ref=DbList
在OOWD平台中,描述对象就是一个描述树中的节点,描述对象可以包含子描述对象,可以继承其他的描述对象,上面的描述对象TableOp就是由下面的4个页面描述对象组成,而每个页面描述对象又继承其他的描述对象。所以,在OOWD平台中,描述对象是一个非常宽泛的概念,可以大到由一个应用的所有页面组成,也可以小到一个字段的描述,这给我们抽象所有范围内的共性提供了方便。
2、 再来看看 DbAdd 、 DbEdit 、 DbShow 、 DbList 等描述对象的定义:
[*Page]
!Name=DbAdd // 名称为 DbAdd 的描述对象,定义了数据库表增加页面的通用框架
[*Block]
!Ref=Frame // 从通用框架描述对象 Frame 来继承
%Content=DbAdd_Content // 指定描述对象 DbAdd_Content 替换 Base 中的 %Content 变量
%Exec=DbAdd_Exec // 指定描述对象 DbAdd_Exec 替换 Base 中的 %Exec 变量
[*Page]
!Name=DbEdit
[*Block]
!Ref=Frame
%Content=DbEdit_ Content
%Exec=DbEdit_Exec
[*Page]
!Name=DbShow
[*Block]
!Ref=Frame
%Content=DbShow_ Content
%Exec=DbShow_Exec
[*Page]
!Name=DbList
[*Block]
!Ref=Frame
%Content=DbList_ Content
%Exec=DbList_Exec
3、 上面 4 个描述对象都继承了项目所有页面的总体框架描述对象 Frame
[*Block]
!Name=Frame // 名称为 Frame 的描述对象,定义了页面的通用框架
*BlockValue=&{File:Frame} //&{File:Frame} 是 OOWD 的内建对象,表示读取文件 Frame 的内容
[*Block]
!Name=Exec // 将用本节点的内容取代 Frame 文件中的 &{ChildNode:Exec}
!Ref=%(Exec) // 继承一个可变的描述对象,其名称由继承的父对象设置变量 %( Exec) 来确定
[*Block]
!Name=Content // 将用本节点的内容取代 Frame 文件中的 &{ChildNode:Content}
!Ref=%(Content) // 继承一个可变的描述对象,其名称由继承的父对象设置变量 %(Content) 来确定
4、 为什么要用上面的这种多重继承来构造描述对象 TableOp 呢?
一个项目的所有页面都有共性,所以,我们用描述对象Frame来提取所有的共性,它并不是一个具体的页面,因为,它没有确定变量%(Exec)和%(Content)的值,而是需要它的继承者来确定。
不同表的增加页面有很多的共性,所以,我们用描述对象DbAdd来提取所有的增加页面的共性,但它也要保持项目中的共性,因此,它从描述对象Frame继承,并把其中的变量描述对象%(Exec)和%(Content)指向自己特有的描述对象DbAdd_Exec和DbAdd_Content。
表的编辑页面、显示页面、查询列表页面和表的增加页面类似。
5、 描述对象 Frame 中的文件 Frame 的内容如下:
<html>
<head>
&{ChildNode:Exec} // 被描述对象 Frame 的名称为 Exec 的子节点替换,在 DbAdd 的实例中被描述对象 DbAdd_Exec 替换
</head>
<body>
&{ChildNode:Content} // 被描述对象 Frame 的名称为 Content 的子节点替换,在 DbAdd 的实例中被描述对象 DbAdd_Content 替换
</body>
</html>
先来看看这样的架构如何应对项目内所有页面都需要变化的需求
需求1:希望在项目内所有页面增加菜单;
<html>
<head>
&{ChildNode:Exec} // 被描述对象 Frame 的名称为 Exec 的子节点替换,在 DbAdd 的实例中被描述对象 DbAdd_Exec 替换
</head>
<body>
[ 菜单的 html 代码 ] // 对于新需求,只需在这里变化一次
&{ChildNode:Content} // 被描述对象 Frame 的名称为 Content 的子节点替换,在 DbAdd 的实例中被描述对象 DbAdd_Content 替换
</body>
</html>
我们只需在frame文件中加上[ 菜单的html 代码] ,项目内所有继承了Frame的页面就都加上了菜单。这有点像模板的功能,没错,OOWD平台包含并大大扩展了模板的功能。
需求2:项目的几十个页面已经加上了菜单,现在又有了需求变化,有两个页面需要的菜单是特殊的,怎么办?
<html>
<head>
&{ChildNode:Exec} // 被描述对象 Frame 的名称为 Exec 的子节点替代
</head>
<body>
%(Menu) // 对于新需求,只需把菜单代码部分变为变量
&{ChildNode:Content} // 被描述对象 Frame 的名称为 Content 的子节点替代
</body>
</html>
我们把会变化的Menu部分用%(Menu)来表示,每个继承Frame的页面只要设置%(Menu)就可以指向不同的Menu,但这样的话,就要在几十个页面的描述对象中都指定变量%(Menu),这个变动也太大了,显然没有体现好的灵活性。幸好,在OOWD平台中,我们可以指定变量%(Menu)的默认值
在描述对象Frame中
[*Block]
!Name=Frame // 名称为 Base 的构件,定义了页面的通用框架
*BlockValue=&{File:Frame} //&{File:Frame} 是 OOWD 的内建对象,表示读取文件 Frame 的内容
%Menu=[Menu 的 html 代码 ] // 变量 %(Menu) 的默认值,在父节点上设置变量 %(Menu) 可以覆盖这里的默认值
设置了默认值后,我们只需要在那两个有特殊菜单的页面的描述对象中,设置变量%(Menu)为特殊菜单即可,这样,就用最少的变化满足了这个需求。
这个需求是很典型的共性和个性矛盾的案例,但利用对象化的继承、覆盖就很容易应对。
6、 由于篇幅所限,接下来我们仅对 DbAdd 、 DbEdit 、 DbShow 、 DbList 中 DbAdd 进行深入分析
[*Page]
!Name=DbAdd // 名称为 DbAdd 的构件,定义了 Add 的通用框架
[*Block]
!Ref=Frame // 从通用框架描述对象 Frame 来继承
%Content=DbAdd_Content // 指定构件 DbAdd 替换 Base 中的 %Content 变量
%Exec=DbAdd_Exec // 指定构件 DbAddSql 替换 Base 中的 %Exec 变量
在DbAdd中,指定了两个描述对象:DbAdd_Content负责Form的显示,DbAdd_Exec负责Sql的操作,由于这两个描述对象都体现了同一个XSLT的思想,而DbAdd_Exec中涉及的Db操作部分将在后面介绍,因此,这里我们仅对DbAdd_Content进行介绍。
7、 描述对象 DbAdd 下的 DbAdd_Content 的定义如下:
[*Block]
!Name=DbAdd_Content
*BlockValue=<<< // 多行属性值的开始
<form name=”DbAdd” method="post" action=”?action=add”>
<table>
&{Child:FormHtml} // 被下面的名称为 FormHtml 的子节点替换
</table>
<input type=submit value= 增加 >
</form>
>>> // 多行属性值的结束
[*LoopChild] // 对某一个节点的所有子节点进行遍历
!Name=FormHtml // 此名称与上面 *BlockValue 中的 &{Child:FormHtml} 对应,将用本描述对象替换 &{Child:FormHtml}
!Ref=DbAdd.FormHtml // 继承描述对象 DbAdd. FormHtml
NodeRoot=%(Obj)_Form // 变量替换后,指定要遍历的节点为 Custom_Form
8、 其中引用的描述对象 DbAdd.FormHtml 的定义如下:
[*LoopChild] // 对某一个节点的所有子节点进行遍历
!Name=DbAdd.FormHtml // 继承描述对象 DbAdd.FormHtml
RowBefore=<tr>
RowAfter=</tr>
*FieldValue=<td>{ 属性 Desc 的值 }</td><td><input type=text name=f_{ 属性 !Name 的值 }></td>
9、 被描述对象 DbAdd.FormHtml 遍历的节点
[FieldSet]
!Name=Custom_Form
TableNode=Custom // 对应的表描述对象
[Row] // 表示下面的子节点显示在一行中
[*Field]
!Name=Name // 对应表中的相应字段
[*Field]
!Name=Age
[*LoopChild]是OOWD平台对XSLT思想的一个实现,它的解析过程是:找到NodeRoot指定的节点,对此节点及其所有子节点进行遍历,根据不同的节名来查找相应的属性值进行输出。在上面的例子将产生如下的输出:
<tr>
<td>姓名</td><td><input type=text name=f_Name></td>
<td>年龄</td><td><input type=text name=f_Age></td>
</tr>
这里有一个疑问,{属性Desc的值}怎么变成了“姓名”、“年龄”?这是因为,当遍历的节点中没有此属性时,会去其对应的字段描述对象中去找此属性。这也体现了对象的继承、覆盖的思想。
XSLT对于共性提取具有非常重要的意义。通过OOWD的XSLT,把Form中的所有字段的数据和显示有效分离后,对显示进行了统一的处理,这样不仅对这个Form中的显示可以进行统一控制,对其他的所有使用这个XSLT的Form(可以跨越多个项目)都可以统一控制,这样,一个涉及多个项目的需求变化只需修改一次!
需求3:如果想确定字段描述部分和input部分的宽度比例怎么办?如果想在每个字段描述后面加个’:’怎么办?
*FieldValue=<td width=15% >{属性Desc的值}: </td><td width=35% ><input type=text name=f_{属性!Name的值}></td>
需求4:如果一个字段(比如Age)的显示很特殊,怎么办?
[FieldSet]
!Name=Custom_Form
TableNode=Custom // 对应的表描述对象
[Row] // 表示下面的子节点显示在一行中
[*Field]
!Name=Name // 对应表中的相应字段
[*Field]
!Name=Age
*FieldValue={ 特殊的显示 } // 此属性可以覆盖 LoopChild 中的 *FieldValue 属性
我们可以在接到Age中设置*FieldValue的值为“特殊显示”来覆盖掉LoopChild中的*FieldValue属性,这里再次使用了对象化的继承、覆盖的思想来应对这个需求变化。否则,很难应对这种具有杀伤力的个性化需求。
需求5:如果想对select、radio进行处理,想对日期类型加button来进行选择怎么办?
*FieldValue=<<< // 多行属性值的开始
{ 如果 InputType==Select ,根据 Select 的相关数据定义来输出 Select 的代码 }
{ 如果 InputType==Radio ,根据 Radio 的相关数据定义来输出 Radio 的代码 }
{ 如果 InputType==Date ,输出 Date 的选择 Button 和相关的代码 }
>>> 多行属性值的结束
需求6:如果想对字段值在Client端进行Js校验,Server端也进行校验,并且指定出错处理的方式。
和需求5一样,可以用OOWD平台的可编程的XSLT来满足这个需求
从上面对需求变化的应对可以看出,对于上面分析的Form、以及其他的描述对象,如:查询列表、报表、菜单等,我们都可以在最大范围内(具体的范围可由我们自己决定)进行统一的修改控制,这种共性提取大大提高了灵活性。
四、 灵活性
通过上面的实例探讨,我们来看看在灵活性方面,OOWD平台都做了哪些改进了。
在一般的描述+解析器的平台模型中,解析器往往包含了较多的逻辑,这样,一旦需求变化涉及到解析器中的逻辑,不但需要对解析器进行修改,使解析器变得复杂,而且由于解析器往往是编译的,源代码不对外开放的,使得第3方无法应对需求变化。而OOWD平台的模型是描述+OOWD语言+OOWD语言的解析器,业务逻辑部分基本包含在可随意修改的OOWD语言部分,任何人都可以方便的修改,解析器只对基本的OOWD语法进行解析,不涉及任何业务逻辑。
一般的平台其构件实例是拷贝的,以delphi或vb为例,当我们生成了5个文本框实例的时候,它们是从文本框构件复制而来,彼此是独立的。这样,当需要对5个文本框统一控制,比如更改长度,变换背景颜色时,必须要一个一个地修改。而在OOWD平台中,对所有的描述对象都是继承的,这样,无论是框架、Form、DbView、Report、甚至小到日期字段的显示,都是可以在最大范围内(比如在所有的项目中)进行统一控制,同时通过对象化的覆盖技术实现各种个性化的展现。
在上面的描述对象实例中,继承关系比较复杂,用OOWD平台都是这么复杂的结构吗?这里可以用一个类比来说明:对于对象化的思想,比如Java,我们需要定义Java语法、可以解析Java语法的解析器,还需要比较多的方便开发的类库,当开发应用时,我们也要构造自己的类。OOWD语言与此非常相似,有对象化的OOWD语言,有OOWD语言的解析器,基于OOWD语言我们可以用各种方式来构造应用!上面的比较复杂的描述对象只是我们针对实际应用,为了增加灵活性而设计的。从这一点也可以看出,为什么OOWD语言能够适应各种特殊的个性化需求,就像对象化的C++,原来用C实现的东东,在有了C++后当然还是能够实现,C++的作用只是提供了使架构更灵活的方法而已。因此,不同于很多的快速开发平台需要开发者来适应其框架,OOWD平台对开发者没有任何的限制,它只不过提供共性提取的有效工具。当然,基于OOWD平台上的描述对象库会有语言或框架等的限制,但它在OOWD平台中属于较高的层次。
在web开发领域,比较常见的提取共性的方式是模板,OOWD平台和模板有哪些不同呢?
1. 模板通常包含一些变量,需要调用者来设置。而OOWD平台中,可以把模板的变量转换为描述对象的属性,因此一个描述对象可以包含模板和他的变量。比如:一个描述长方形的模板,有三个变量:长、宽、背景色;在模板语言中,每一个使用此模板的调用者都必须指定这3个变量,而在OOWD平台,可以在描述对象中设置这3个变量的值,当我们从此描述对象进行继承时,无需再指定任何的变量值;
2. OOWD平台具有树形的层次结构,在继承的时候,变量是可以覆盖的,在上面的情况下,如果要改变继承的描述对象的背景色的值,只需在调用者指定背景色的值,就可以覆盖掉继承的描述对象中的背景色变量;
3. OOWD平台的描述对象的范畴比模板更加宽泛,一个描述对象不仅可以是一个模板,还可以是多个模板的集合,甚至是组成一个应用的所有页面;
总之,OOWD语言通过各种技术大大扩展了模板的易用性和灵活性。
所有抽取了共性的描述对象组成了OOWD描述对象库,这个库是随着项目的开发而不断积累的,这样,对每一个特定的对象,比如Form,就有不同风格的Form描述对象,可以命名为style1.Form,Style2.Form等等,每一个Style的Form都是从实际的项目中提取共性而成,具有不同的显示、不同的有效性检测、或不同的出错处理方式。当开始一个新的项目时,就可以方便的从描述对象库里选择合适的描述对象来继承。当需求变化时,也可以通过改变继承的风格或描述对象来进行整个项目范围的修改,呈现出的效果甚至是马上就生成了另一个完全不同的应用。
OOWD描述对象库的出现,使得我们能够像老中医一样不断地积累共性,每开发一个应用,都会得到新的积累,使得下一次的项目开发更加快速、灵活。无论从商业的节省成本的角度,还是从程序员的不做重复开发的角度,都是我们一直努力的目标。
五、 DB层
数据库操作在一个web应用中通常占据了大部分的比例。OOWD平台把数据库的操作抽象为DB层,将表的数据和处理的分离进行了分离。比如:在OOWD语言里,应用层要获取Custom表的Name和Age字段,只需指明要得到Custom的Name和Age字段,至于如何得到这些字段,应用层不必操心。这种分离会大大提高灵活性。
下面是表的描述对象
[Table]
!Name=Custom // 唯一名称,在引用此表的时候用到
DbConn=DbConn_Common // 数据库连接信息
[*Field]
!Name=Id // 数据库中的字段名
[*Field]
!Name=Name
Desc= 姓名 // 字段描述,在form 和查新列表中都会显示
NeedMark=y // 在insert 、update 等sql 语句中,本字段的值需要加引号
InputType=Text // 在form 中用input 的text 类型来表达
[*Field]
!Name=Age
Desc= 年龄
NeedMark=n
InputType=Text
一个常用的查询获取数据的例子是:
&{Db_Custom:SetCond_id=2}
&{Db_Custom:Select}
&{Db_Custom:Get_Name},&{Db_Custom:Get_Age}
这样的获取表字段值的方式比较简单、自然,还带来了如下的灵活性:
1)可以转换为任何开发语言的代码,和开发语言无关
2)和数据库类型无关
3)可以整体控制数据库的出错处理、日志等
4)当数据库出现性能问题时(常见于互联网应用),会对数据库进行很多的变动,比如:
把一个表中的字段拆分到不同的表中;
把一个大表拆分为很多小标;
对某些字段进行Cache处理,或者对表查询进行cache处理;
所有这些需求变化,都只需配置Db层,应用层不需做任何变动!
六、 总结
OOWD平台和各种语言是什么关系?和各种开发框架又是什么关联?
应该说,OOWD平台和这些语言和开发框架都是兼容的。本质上,OOWD平台只是提供了提取共性的方法。以DbAdd为例,我们可以用OOWD语言分别构造php、java、.Net的描述对象,也可以构造匹配Java的Hibernate的描述对象。
可以把OOWD平台看成是分层的,其高层与业务层相关,底层来适配各种语言和平台。因此,从某种程度上说,OOWD是跨语言平台的。
OOWD平台不仅仅适用于web开发领域,也适用于flash、移动开发等各种描述语言和脚本语言。
限于篇幅,上面对OOWD语言的介绍非常简略,着重说明了基于OOWD平台来架构web应用的灵活性。如果对这种架构感兴趣,可以邮件进一步交流(邮箱:weiz.bh@gmail.com)。