这里讲的是对MIS系统架构的一些局部位置的设计思路,也是我个人的想法,不敢以偏概全,不过包含了很多要素:权限、验证、流程、行为、结构、内容,还有表示层如何与业务层分离。
- 写在前面
这是对以前项目的总结,作为一个在别人架构的系统上写程序的开发人员,对系统架构提出新的想法我想这没什么可争议的。
我经历了两个业务需求相似的项目,第一个项目我看不到架构的影子,看到不少用工具根据数据库生成的看起来很优秀的代码,花了几个月修改bug但最后项目还是黄了,这次的失败给每个人都上了堂课。但我到公司的时候负责架构的人和多个开发人员已经不在公司了,那时写代码的工作已经接近尾声,主要就是测试、改bug和报表,我是和剩下的人一起共事的。我到公司后就做报表,用DevExpress控件做报表,以致我对DevExpress的XtraReport报表很熟悉,之后在第二个项目中负责报表并写了关于XtraReport报表的一篇随笔 。做了一个多月的报表就去改bug了,改bug很容易,在我的印象中发给我的bug我都能改好并不会产生其他的bug,而且还对原有不合理的程序进行了优化,但遗憾的是在之后的几次演示中系统都没能完全的毫无错误的正确的走完。其中客户很挑剔,关于这个我是理解的,如果我们去买房子当然希望房子好的出乎我们的想像。项目无功收场,很多同事离去了,曾经大家起吃住欢笑的日子不在有了,特别是我们的经理,他离开让我很意外,当初就是他面试我进公司的,面试时只是让我做一个类似新闻发布系统,我甚至没有做出来,他看了下代码和我平时瞎捣鼓的东西,到会议室问我期望的待遇,约好时间我就来上班了。他走时我送了本书给他,现在元旦时他还会给大家发条祝福的短信。
这次和剩下的几个人和另一组人进行了第二个项目,我从这边转到那边时架构已经做好了,当时我当心架构会跟前一个项目一样,到了之后看到比前一个大大的改进了。这个项目架构的很好,特别是界面层,界面进行了统一,或者说是标准化了下,一排按钮整齐紧挨着排列,每个按钮放在一个lebel上,设置这个lebel不可见后面的按钮会紧挨着跟上来,这点做的太棒了,它能给界面层设计带来很多启发,这点是我没想到的。这个架构将很多操作定义好,有时候像是定义死,不同的操作它会执行不同的代码并传不同的参数给一个虚方法,派生的窗体重写这写虚访法完成对业务层的访问(业务层往往是调用存储过程)。负责某个模块的开发人员要做的工作就是:写好各操作的存储过程;定义业务层对象;选择正确的窗体基类(主要有带树形的列表窗体、列表窗体和明细窗体)派生一个新的窗体,在这个窗体中使用业务层对象和一些公共对象做权限设置等初始化操作以及验证,复写虚访法,很多的地方都是用虚访法的,如验证,基类的事件中去调用这些虚访法,还有访问业务层对数据的操作也是在虚访法中去访问,基类会去调用这个虚访法。开发人员要写很多的if语句,并且大量的代码都在界面层去做,业务层显得及其单薄,当然架构人员当初肯定不是这样想的,实际工作中开发人员在没有指导和参考实例的情况下违背了架构人员的初衷。这个项目最后是成功的。
学会总结,争取每次架构比前一个架构更进一步。
- 简单而繁琐的工作——开发人员的痛
一个好的架构可以让开发人员有个赏心悦目的体验,他可以简单愉快的面对繁杂的业务问题,将精力放在困难但能给人带来挑战性和成就感的问题上,但往往不是这样。
我们经常在初始化界面时要根据权限设置可见和可用,用了一堆的if语句,在业务层也要根据权限判断是否继续执行接下来的代码,同样验证也是有一堆if语句。每当写这样的程序时脑子里要想到有那些东西要设置权限,是可见还是可用,是那些关系表达式的组合,生怕漏掉或是写错任何一个。当发现错误时我们得看着长长而且相似的代码一时找不到错误的位置。虽然这些工作很简单,但很快会有人说:“嗯,这个地方需要改进。”
给窗体设置内容时一个一个控件的选择,在属性中设置各个参数。特别是表格控件需要对每个列设置文本值各参数甚至还有事件,代码中可能还有权限。它需要一个一个的来,一会儿是复制张贴一会儿是鼠标选择焦点,切换来切换去会让思维停滞或转移,区区的零点几秒累计起来可是不小的数字。似乎我们的很多代码不是写出来的而是画出来的,除了直观没有其他的好处,这里不是否定可视化,可视化的操作也是产生代码的,只要把结构设置好,内容是可以根据业务层的数据自动完成的。
还有操作,我们要添加很多事件去调用业务层的方法,如果业务层没有你要的方法就要去业务层加个,似乎界面层上的控制是事先知道业务层的的相应方法是做什么用的,耦合度问题?
- 问题分析和解决
对于MIS系统会遇到一下几个东西:结构、内容、权限、验证、流程、操作和数据。
结构:好比框架,一块一块的区域。
内容:结构中的内容,一般是控件。
权限:我们总是用分支语句来做权限上的事,每个分支语句其实都可以看成一个关系表达式或关系表达式的组合。就像“1>2”或“(1>2||3<8)&&9==9”。经常是当前登录用户是否等于要处理的业务对象中的用户属性、当前登录用户是否有该操作权限等等。
验证:仔细分析可以看到它跟权限是一样的,只是它往往是验证要处理的业务对象中的数据与一些值的关系。
流程:不知道大家的系统中遇到的流程是什么概念,我的是业务对象的一个字段叫“状态”,它从0开始往后加,每个值就标识业务对象所处的状态,连串起来就是流程。在不同的状态下它的权限和验证都不一样,这样就可以把它合并到权限和验证中去做了,如:“状态==0&&(权限的关系表达式)”。
操作:任何位置的按键或鼠标点击都可能会有一些行为,这些行为都是根据内容来的,因为界面有内容所以在内容上的输入才有意义。这样我们可以把界面层的内容与在该内容上的操作在业务层设计成关联,就是内容包含操作。
先来看看业务层,数据访问层已经很简单并很成熟了,这里就避过了。
1. 既然有关系表达式,我们就做这样的一个类Restrict(我这里用限制一词,不用权限一词是因为验证也会用到它)来帮我们计算单个关系表达式(即:1>2这种形式)的真假值,用一个类来专门做这个是为了能够计算我们自定义的类型的关系,如图1。GetRestrict()方法计算并返回单个关系表达式的真或假。这里只写了5个静态关系操作符并且命名太过于直接了,实际开发时当然不能这样,大家能看懂就行。
图1
2. 我们要得到的是一个关系表达式组合的真假值,这样我们会有很多的Restrict和一个表示它们组合关系的表达式,看图2。这里有一个实现了.NET类库IList接口的的Restricts类,它有一个属性RestrictExpression就是表示组合关系的,举例说明这个属性,(1>2||3<8)&&9==9像这样的关系表达式的组合在这个属性里表示为”( [0] Restrict.|| [1]) Restrict.&& [2]”,相信大家已经明白它的含义了,中括号为Items数组中的一个。接下来的任务就是Parse()方法的事情了,由它去解析这样的表达式并返回布尔值,这个类中还要有验证RestrictExpression属性是否正确的代码,这些实现上的细节问题就不多说了。还要实现IList接口,这些方法在这个类图中就省去了,这样做是为了让它的调用着更方便操作它。
图2
3. 一个操作就比如一个按钮,它有在它上显示的文本、它在什么情况下可见(Visible)和什么情况下可用(Enabled)、一个要执行的方法以及方法执行前的验证,似乎操作(行为)的类就这样定义:如图3.。这个比较头痛,行且这样。
图3
4. 我们再来考察内容,就像一个控件,它有在它上面显示的文本、绑定到它的数据以及它在什么情况下可见(Visible)和什么情况下可用(Enabled),我们是不是漏了什么?对了,控件都有输入(如鼠标点击、键盘按下),这些输入会转化为事件,这就是我们的操作,因此每一个内容都有一系列的操作,我们的内容已经有个显示的文本属性操作类中的Text就省了,还有可见权限和可用权限也是。看看图4,在前面的图中已经画的类这里就不画了。其中Behavior类的属性Action是一个委托,接受来自领域对象的方法,如保存,委托DisplayRights接受来自Content类中的DisplayRights.Parse()方法,委托EnableRights同理,方法Operate()为传给界面层作为事件的方法,它先调用DisplayRights()和EnableRights()执行为真后,再调用OperateValidate.Parse()验证为真则执行Action()。可以看到事件方法的e参数的类型用了反射,根据Type的值获得类名字符串的。可以发现把所有的功能都集中到了Content类中,我想用它可以描述很多东西了,只要结构标准统一,一连串的Content对象就可以表示所要呈现的各种东西,就像Win32编程中的窗口的概念,打开的一个界面是窗口,一个控件也是窗口,完善Content类就可以等同于这样的窗口概念。
图4
5. 我们会有个业务层的基类,它是抽象类,由它来统一访问数据访问层,如图5。
图5
6. 接下来就是与我们要面对的问题有关的事情了,就是领域模型。由于我们面临的问题可能很复杂,这里无法做到概括所以的问题域,只是举个例子:单据,如图6。我们用一个抽象的基类Bill来定义共有的属性和方法,每个具体的单据继承它并加载数据、设置权限验证操作等等属性。当我们需要对数据库进行操作时(即序列化等问题)需要知道数据库或XML的设计,因此我们设计了类似ORM的框架,让它帮我们做对象关系映射和序列化的事,当修改了数据库,只要修改它就OK了,其中的接口不应仅仅这些,需要有更多的方法帮助我们知道对象关系映射的信息和对数据库的操作,同时要传IDa对象给它。注:我没有去了解过任何现有成熟的ORM框架,这里的设计思路与它们没有任何的关系,只是为了实现这样的架构而有了它,不能把它当成ORM框架来看,它应该有个基类以完成一些共有的属性和方法,这里没有仔细的去思考它。
图6
对于界面层的基类就是要设计好所需要的结构和根据业务层的对象添加相应的控件并设置属性和事件,如图7。对于结构,相应的基类画好就行。要显示什么内容就是遍历Bill类的MainContents : Content[]、DetailContents : Content[]和Behaviors : Content[]三个属性来设置相应控件,事件就是这三个中的Operations : Behavior[]属性,一个控件会有多个事件,根据Behavior类的Type : BehaviorType属性确定是那个事件,这在BehaviorType类中约定,将Behavior类的Operate方法赋给事件。
因为不同的结构设置控件的属性不同,不同的窗体又有不同的结构,因此设置控件的属性的工作就放到了具体结构的窗体构造函数中(如FormDetail、FormList和FormTree),由具体的单据窗体的构造函数去调用。
图7
这样就很好的将业务层与界面层相分离了,界面层只负责将内容一种结构去显示,至于是显示什么内容是业务层的事,当我们要显示的某列的名字有改变时只是要去修改Content对象,将问题集中在一个对象里面。
好了,整个结构的思路讲完了,大家可能已经看到大多数类的属性都是共有的,这里是为了表达自己的观点,是概念层的东西,并没有过多的去考虑实现上的细节,图8是张完整的类图。
图8
- 被解救出来的开发人员
现在开发人员的工作就更加明确和清晰了,我们以订单这个东西来简单体验一下这个架构。
首先我们定义一个实现IListable、ISavable和ISubmittable接口的类OrderPL,它要做数据库的表到DataSet数据类型(你也可以用你自定义的类型)的映射,订单拥有的操作保存和提交等等。
然后根据需求我们会定义一个订单类,名字就是订单,主要内容可能是订单号、订货日期,明细内容是所订的货物,它有货物名称、数量、价格,订单有保存和提交两个操作,根据这些我们的Order类大致这样写(注意代码中的注释)。
![ContractedBlock.gif](/Images/OutliningIndicators/ContractedBlock.gif)
![ExpandedBlockStart.gif](/Images/OutliningIndicators/ExpandedBlockStart.gif)
class Order : Bill
{
private ILoadable _loadPl;
private ISavable _savePl;
private ISubmittable _submitPl;
public Order()
{
OrderPL pl = new OrderPL();
_loadPl = pl;
_savePl = pl;
_submitPl = pl;
Data =_loadPl.Load();
Text = "订单";
MainContents = new Content[] {
new Content(
"订单号",
"DDH",
//在任何情况下都显示
null,
//流程 状态为0时可编辑
new Restricts(new Restrict[] { new Restrict (Data.Table[0].Row[0]["ZT"],0, Relative.== )} ,"[0]"),
//没有针对该控件事件的操作
null
),
new Content(
"订货日期",
"DHRQ",
null,
new Restricts(new Restrict[] { new Restrict (Data.Table[0].Row[0]["ZT"],0, Relative.== )} ,"[0]"),
null
)
};
DetailContents = new Content[] {/*货物名称、数量、价格*/};
Behaviors = new Content[] {
new Content(
"保存",
"",
//保存只能是在状态为0、订单的所有者是当前登录用户并且当前登录用户有订单的保存操作权限下可用
//这里假定系统对订单的保存操作权限用0表示(每个操作都要有一个值来表示),同时这个值应该放到一个枚举中
//如果你还有其他的复杂的权限可以添加相应的类去做
new Restricts(
new Restrict[] {
new Restrict (Data.Table[0].Row[0]["ZT"],0, Relative.==),
new Restrict (Data.Table[0].Row[0]["OWNER"],User.ID, Relative.==),
new Restrict (UserRights.GetRights(0),True, Relative.==)
},
"[0]&&[0]&&[0]"
),
//同上
new Restricts(
new Restrict[] {
new Restrict (Data.Table[0].Row[0]["ZT"],0, Relative.==),
new Restrict (Data.Table[0].Row[0]["OWNER"],User.ID, Relative.==),
new Restrict (UserRights.GetRights(0),True, Relative.==)
},
"[0]&&[0]&&[0]"
),
//保存可拥有的多个操作
new Behavior[] {
//按钮被按下的操作
new Behavior(
//表示是按钮点击操作
BehaviorType.ButtonClick,
//验证,订单号不能为空
new Restricts(new Restrict[] { new Restrict(Data.Table[0].Row[0]["DDH"],"", Relative.!=)} ,"[0]"),
//保存数据的函数
_savePl.Save,
new Restricts(
new Restrict[] {
new Restrict (Data.Table[0].Row[0]["ZT"],0, Relative.==),
new Restrict (Data.Table[0].Row[0]["OWNER"],User.ID, Relative.==),
new Restrict (UserRights.GetRights(0),True, Relative.==)
},
"[0]&&[0]&&[0]"
).Parse(),
new Restricts(
new Restrict[] {
new Restrict (Data.Table[0].Row[0]["ZT"],0, Relative.==),
new Restrict (Data.Table[0].Row[0]["OWNER"],User.ID, Relative.==),
new Restrict (UserRights.GetRights(0),True, Relative.==)
},
"[0]&&[0]&&[0]"
).Parse()
)
}
),
new Content(
"提交",
"",
new Restricts(
new Restrict[] {
new Restrict (Data.Table[0].Row[0]["ZT"],0, Relative.==),
new Restrict (Data.Table[0].Row[0]["OWNER"],User.ID, Relative.==),
new Restrict (UserRights.GetRights(0),True, Relative.==)
},
"[0]&&[0]&&[0]"
),
new Restricts(
new Restrict[] {
new Restrict (Data.Table[0].Row[0]["ZT"],0, Relative.==),
new Restrict (Data.Table[0].Row[0]["OWNER"],User.ID, Relative.==),
new Restrict (UserRights.GetRights(0),True, Relative.==)
},
"[0]&&[0]&&[0]"
),
new Behavior[] {
new Behavior(
BehaviorType.ButtonClick,
new Restricts(new Restrict[] { new Restrict(Data.Table[0].Row[0]["DDH"],"", Relative.!=)} ,"[0]"),
_submitPl.Submit,
new Restricts(
new Restrict[] {
new Restrict (Data.Table[0].Row[0]["ZT"],0, Relative.==),
new Restrict (Data.Table[0].Row[0]["OWNER"],User.ID, Relative.==),
new Restrict (UserRights.GetRights(0),True, Relative.==)
},
"[0]&&[0]&&[0]"
).Parse(),
new Restricts(
new Restrict[] {
new Restrict (Data.Table[0].Row[0]["ZT"],0, Relative.==),
new Restrict (Data.Table[0].Row[0]["OWNER"],User.ID, Relative.==),
new Restrict (UserRights.GetRights(0),True, Relative.==)
},
"[0]&&[0]&&[0]"
).Parse()
)
}
)
};
}
}
接下来就是界面层,因为大量的工作都交给业务层了,这层主要的工作就是在构造函数中调用基类的构造函数。
![ContractedBlock.gif](/Images/OutliningIndicators/ContractedBlock.gif)
![ExpandedBlockStart.gif](/Images/OutliningIndicators/ExpandedBlockStart.gif)
class FormOrder : FormDetail
{
public FormOrder(bill:Order)
{
bese(Order);
}
}
- 写在后面的废话
在08年5月份回到合肥的时候就准备写这篇东西的,当时还写了标题,之后就没有继续,现在发现写东西是要先写提纲再写正文之后才确定标题。那时的想法肯定比现在要多一些,正是想到东西在脑子里会慢慢的流失,也因为“今天的事不要留到明天去做”,所以乘着还没正式的跨入09年把东西总结下,不管是好是坏也是对自己的一个交代。
我没有去验证这里所表达的思想的可行和优劣等问题,一个纸面的东西脱离了实践就会显得惨白没有说服力。
《标准建模语言UML教程》中将静态图分为三个层次:概念层、说明层和实现层,这篇文章可能连概念层都没达到,如果要到实现层这短短的篇幅是远远不够的,更不可能把.NET那么多的特性都写进去。
最近不知道是吃了什么总是fart,如果你赞同本文的观点那自然是对我莫大的鼓舞,如果你有不同的见解或者认为有什么缺憾请指正,否则就请当我吃多了消化不良吧!