三层架构是企业信息管理系统中一种比较流行的架构方式,如大家所知,三层架构将信息系统分为数据访问层(DAL)、业务逻辑层(BLL)、界面表示层(UI)三部分,三层架构的好处是根据系统中代码所处的层次将系统拆开,而通过业务模型(Model)再进行连接,降低系统各层次之间的耦合度,提升程序开发和后期维护的容易度。
由于三层架构是根据由上至下的层次进行分层,而不是根据功能、应用领域进行分层,所以三层架构在每一层的关注点并不相同,数据访问层关注的是跟数据库打交道的部分,业务逻辑层关注的是业务逻辑处理部分,而界面表示层关注的是人机交互部分,所以三层架构在一定程度上也体现出了系统开发的先后顺序和分工。
本文将从我对三层架构的理解上,利用General框架从头开始打造一个信息管理系统的初步结构,以此来展示General框架在信息管理系统开发上的优势。由于不同的人对架构的理解也不一样,所以本文不强调架构的正确性,只是出于简化开发、方便编程的原则下提供一个三层架构的样本。本文中的示例工程为一个小型的商品库存管理软件,源代码请查看General框架中的Sample.Market工程。
第一步、设计业务模型
由于是示例工程,所以本文绕过需求分析过程,直接从业务模型设计开始。关于为何从业务模型开始,其实目前主要有两种开发顺序,即业务驱动设计和界面驱动设计,业务驱动设计更强调业务作为系统的核心,所有编程工作都围绕业务设计展开,而界面驱动设计认为界面是客户、项目经理、设计师、程序员最好的沟通工具,所以一切以界面为首要确定目标。而我认为这两种方式至于哪种更好,需要看实际情况,假如项目不是很大,而界面又容易确定或是客户比较注重界面设计,那界面驱动设计当然为优选方案,可以先从界面入手,做出DEMO,等客户满意后再做业务流程的开发。反之,如果业务逻辑更为重要,当然是以业务驱动设计为优选方案,而信息管理系统的开发,大部分还是以业务为中心,所以除去需求分析之后的第一步也就以业务模型设计为先。
业务模型设计可以借助Excel、PowerDesinger等工具,先用Excel整理好业务模型比较核心的数据信息,然后通过PowerDesinger做出业务模型图(即ER图),再生成物理数据库模型,再生成数据库。本文的业务模型图设计如下,并生成了对应各种数据库的物理数据库模型,再生成了数据库的建库脚本,Acess通过脚本创建数据库需要一定的小技巧,这个方法可以在百度中查到。
我通过PowerDesinger生成的建库脚本,分别创建了Access、Sqlite、SqlServer2000、SqlServer2005、Oracle、MySql的数据库,数据库名称都为Market。
第二步、生成实体模型
数据库创建完成后,先用VisualStudio创建名为Sample.Market的解决方案,并创建Sample.Market.Logic(业务逻辑层)、Sample.Market.Model(实体模型库)、Sample.Market.WinForm(WinForm界面表示层),大家发现为什么没有创建数据访问层呢,因为我是利用General框架进行开发,而General框架支持多数据库并且有ORM功能,所以数据访问层就显得不是必须的了,也可以将General.Data理解为通用的数据访问层。但是从系统解耦和更针对性的多数据库支持出发,再增加一个系统内的数据访问层也有好处,但会带来更多的编码和更多的后期维护成本,其中利弊需要自己权衡。
工程创建完毕后,利用General代码生成器这个利器,我们就可以很快的一次性把实体模型生成出来,注意这个实体模型是从数据库生成而来的,而实际上实体模型应从业务模型而来,因为数据库是业务模型生成而来,而实体模型又是从数据库生成而来,所以这三者就成了完全一致的,这样在开发角度其实更方便实用,因为只要了解其中一者就可以对三者完全了解。但问题是如果业务模型发生变化怎么办,这个问题也困扰我很久,因为虽然有先进的工具做支持,而从业务模型生成物理数据模型,再修改数据库结构,再从数据库生成实体类,依然是非常累人的一件事,直到目前我也没有特别好的办法解决这个问题,甚至曾想过制作一个从设计业务模型到生成数据库再到生成实体类的完整解决方案工具,但奈何工作量太大是我难以完成的,在此只能提出以下几点供大家参考:
1)尽量减少业务模型的修改,前期设计要尽量完善,留足冗余字段,并告知负责业务调研同事修改的成本,修改尽量要求客户签字确认;
2)将数据库和测试数据分开,比如通过SQL脚本录入测试数据,免得修改数据库结构造成测试数据丢失;
3)工具不是人,没有人的智能,如果对数据库结构或实体类的某个地方修改了,而重新建库或是重新生成实体类后又忘记复原,容易导致莫名其妙的BUG,所以尽量避免重新建库或一次重新生成所有的实体类,而是小范围修改;
4)如果实在改动太多,就抛弃业务模型,直接去数据库修改吧,这样减少了一大部分工作量,可以避免积劳成疾。
General代码生成器的使用可以参考压缩包内的说明教程,生成实体的模板已经包含在模板库中,动手能力强的可以自己修改或制作符合自己习惯的模板。
第三步、创建界面表示层
制作软件界面其实是一项非常累人的工作,大概占了整个系统开发的一半强的时间,但是在界面开发上也有很多窍门,比如通过配置动态生成菜单、通过配置动态生成表格列、表单自动生成自动收集填充、下拉框自动生成、搜索项自动生成并自动生成查询语句、呈现器自动绑定字典等等,可以说是只有想不到没有做不到,通过这些技巧可以大大提高开发的效率,减少很多重复工作,在以后我会慢慢介绍这些技巧,本文中将介绍一个表单自动收集填充的技巧。
在General.WinForm.FormHelper和General.Web.WebHelper中都有一个名为CollectAndFill的方法,分别对应窗体和网页的收集和填充,可以对 Object、DataRow、控件集这三者中的任意两者之间进行收集和填充工作,比如对表单控件收集值并赋值到实体,对实体收集值并赋值给表单,但前提是需要将表单的控件命名为与实体的属性相同的名称以进行对应。使用方法如:
创建实体并收集表单值:
2 FormHelper.CollectAndFill(this, good);
查询实体并赋值给表单
2 FormHelper.CollectAndFill(good, this);
其实控件的收集填充工作是通过各种控件对应的工具类来完成的,所以如果有收集或填充不到的控件类型,可以通过增加并注册新的工具类来实现,具体请参考源代码。
第四步、创建业务逻辑层
最重要的业务逻辑却放在最后,因为当业务模型和界面都确定之后,业务逻辑其实也跑不出圈了,只要根据需要对应完成就行了,可能有人说是不是应当先创建业务逻辑再制作界面,其实这样业务逻辑的方法反而难以确定,还是以界面制作为优先。
General代码生成器也可以生成业务逻辑层的代码,甚至说界面层的代码也完全可以生成,如果项目代码优化的比较好,可以通过先生成再修改的办法来节约很多时间,但大部分情况这个工作重复度并不高,所以不需要自动生成来完成。
创建业务逻辑类后,通过DataManager来进行数据库访问操作,比如下面这个通过ID获取商品信息的方法:
2 /// 通过ID获取商品信息
3 /// </summary>
4 /// <param name="id">商品ID</param>
5 /// <returns></returns>
6 public Goods GetGoodByID(int id)
7 {
8 return DataManager.Default.Find<Goods>(id);
9 }
通过一行代码就可以完成;
再如商品入库的方法:
2 /// 商品入库
3 /// </summary>
4 /// <param name="good">商品信息</param>
5 /// <param name="stockDate">入库日期</param>
6 /// <param name="amount">入库数量</param>
7 /// <returns></returns>
8 public ReturnCode StockInGoods(Goods good, DateTime stockDate, int amount)
9 {
10 Transaction tran = DataManager.Default.BeginTransaction();
11 try
12 {
13 // 查询是否有编号已存在的商品
14 Goods tmpGood = DataManager.Default.FindFirst<Goods>("编号 = @bh", good.编号);
15 if (tmpGood != null)
16 {
17 good.ID = tmpGood.ID;
18 good.Attach();
19 good.库存数量 = tmpGood.库存数量;
20 good.库存金额 = tmpGood.库存金额;
21 good.进货数量 = tmpGood.进货数量;
22 good.进货金额 = tmpGood.进货金额;
23 }
24 else
25 {
26 good.Detach();
27 }
28
29 if (good.库存数量 == null) good.库存数量 = 0;
30 if (good.库存金额 == null) good.库存金额 = 0;
31 if (good.进价 == null) good.进价 = 0;
32 if (good.进货数量 == null) good.进货数量 = 0;
33 if (good.进货金额 == null) good.进货金额 = 0;
34
35 // 调整库存
36 good.库存数量 += amount;
37 good.库存金额 += amount * good.进价;
38 good.进货数量 += amount;
39 good.进货金额 += amount * good.进价;
40 good.平均进价 = good.进货金额 / good.进货数量;
41 DataManager.Default.Save(good);
42
43 // 保存入库记录
44 GoodsIn goodsIn = new GoodsIn();
45 goodsIn.Goo_ID = good.ID;
46 goodsIn.日期 = stockDate;
47 goodsIn.编号 = good.编号;
48 goodsIn.进价 = good.进价;
49 goodsIn.零售价 = good.零售价;
50 goodsIn.数量 = amount;
51 goodsIn.金额 = amount * goodsIn.进价;
52 goodsIn.生产日期 = good.生产日期;
53 goodsIn.保质期 = good.保质期;
54 goodsIn.到期日期 = good.到期日期;
55 goodsIn.生产批号 = good.生产批号;
56 goodsIn.供应商 = good.供应商;
57 DataManager.Default.Save(goodsIn);
58
59 tran.Commit();
60 return ReturnCode.Successed;
61 }
62 catch
63 {
64 tran.Rollback();
65 throw;
66 }
67 }
即便是对两个表进行操作并且使用事务,代码依旧非常简短。
由于各家数据库的SQL都稍有差别,所以要做到兼容多种数据库其实是非常困难的,General框架的数据库操作是通过拼接SQL语句来完成的,没有像EntityFramework的Linq或NHibernate的HQL这样的自身查询语言,所以要兼容多种数据库是难以实现的,而这个示例程序能够兼容Access、Sqlite、SqlServer、Oracle、MySql多种数据库,主要是因为以下几点:
1)General框架对各种数据库有对应的查询生成器,同一个方法对不同数据库有不同的SQL生成实现;
2)General框架具有不同参数前缀替换能力,比如在Oracle中参数前缀是“:”而不是SqlServer的“@”,General框架会自动将作为通用前缀的“@”替换为Oracle的“:”以便兼容;
3)本示例中没有使用一些特殊的SQL语句,各家的数据库在常用的select、insert、update、delete语句上兼容的比较好。
最后,大家可以打开General框架中的示例程序查看具体的使用方法,里面还包含一个性能测试程序可以测试General框架的性能,相关的源码下载请参看上一篇文章,欢迎大家评论。