如何快速、灵活地架构web应用

一、 引言
    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)。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值