为什么需要三层结构?
对于开发系统来说,我们要考虑系统性能如何?是否具有足够的容错能力?能否从容应对客户不断变化的需求?面向对象思想的确很酷,可是在工程实践中的实际应用状况又是怎样的?怎样才能使我们的代码具有最大的“可重用性”和“可扩展性”?传说中的三层结构和设计模式又是什么东西?层次结构在现实社会里随处可见。记得有个笑话讲有个村长得意地向他老婆吹牛:“全中国只有四个人比我官大,乡长、县长、省长和国务院总理”。这个笑话也体现了真实社会中分层的现象。社会人群会分层,公司人员结构也会分层,楼房是分层的,甚至做包子的笼屉都是分层的。虽然分层的目的各有不同,但都是为解决某一问题而产生的。所以,分层架构其实是为了解决某一问题而产生的一种解决方案。
示例:
1 using System; 2 using System.Collections.Generic; 3 using System.ComponentModel; 4 using System.Data; 5 using System.Drawing; 6 using System.Text; 7 using System.Windows.Forms; 8 using System.Data.SqlClient; 9 10 namespace MySchool 11 { 12 ///13 /// 登录窗体 14 /// 15 public partial class LoginForm : Form 16 { 17 public LoginForm() 18 { 19 InitializeComponent(); 20 } 21 22 // 点击取消按钮,关闭应用程序 23 private void btnCancel_Click(object sender, EventArgs e) 24 { 25 Application.Exit(); 26 } 27 28 // 点击登录按钮时,设置用户名和登录类型 29 private void btnLogIn_Click(object sender, EventArgs e) 30 { 31 bool isValidUser = false; // 标识是否为合法用户 32 string message = ""; // 如果登录失败,显示的消息提示 33 34 // 如果验证通过,就显示相应的用户窗体,并将当前窗体设为不可见 35 if (ValidateInput()) 36 { 37 // 调用用户验证方法 38 isValidUser = ValidateUser( 39 cboLogInType.Text, 40 txtLogInId.Text, 41 txtLogInPwd.Text, 42 ref message); 43 44 // 如果是合法用户,显示相应的窗体 45 if (isValidUser) 46 { 47 // 将输入的用户名保存到静态变量中 48 UserHelper.loginId = txtLogInId.Text; 49 // 将选择的登录类型保存到静态变量中 50 UserHelper.loginType = cboLogInType.Text; 51 52 ShowUserForm(); 53 54 this.Visible = false; 55 } 56 // 如果登录失败,显示相应的消息 57 else 58 { 59 MessageBox.Show(message, "登录失败", 60 MessageBoxButtons.OK, MessageBoxIcon.Error); 61 } 62 } 63 } 64 65 // 验证用户是否进行了输入和选择 66 private bool ValidateInput() 67 { 68 if (txtLogInId.Text.Trim() == "") 69 { 70 MessageBox.Show("请输入用户名", "输入提示", MessageBoxButtons.OK, MessageBoxIcon.Information); 71 txtLogInId.Focus(); 72 return false; 73 } 74 else if (txtLogInPwd.Text.Trim() == "") 75 { 76 MessageBox.Show("请输入密码", "输入提示", MessageBoxButtons.OK, MessageBoxIcon.Information); 77 txtLogInPwd.Focus(); 78 return false; 79 } 80 else if (cboLogInType.Text.Trim() == "") 81 { 82 MessageBox.Show("请选择登录类型", "输入提示", MessageBoxButtons.OK, MessageBoxIcon.Information); 83 cboLogInType.Focus(); 84 return false; 85 } 86 else 87 { 88 return true; 89 } 90 } 91 92 // 验证用户输入的用户名和密码是否正确 93 // 验证的结果有两种情况:通过和不通过,返回值为布尔型 94 // 不通过的原因可能有多种,在方法的参数中增加消息字符串,用以标识不通过的情况 95 public bool ValidateUser(string loginType, string loginId, string loginPwd, ref string message) 96 { 97 int count = 0; // 数据库查询的结果 98 bool result = false; // 返回值,是否找到该用户 99 100 // 查询是否存在匹配的用户名和密码 101 if (loginType == "管理员") // 判断管理员用户 102 { 103 // 查询用sql语句 104 string sql = string.Format( 105 "SELECT COUNT(*) FROM Admin WHERE LogInId='{0}' AND LogInPwd='{1}'", loginId, loginPwd 106 ); 107 108 // 创建Command命令 109 SqlCommand command = new SqlCommand(sql, DBHelper.connection); 110 111 try 112 { 113 DBHelper.connection.Open(); // 打开连接 114 115 count = (int)command.ExecuteScalar(); // 执行查询语句 116 117 // 如果找到1个,验证通过,否则是非法用户 118 if (count == 1) 119 { 120 result = true; 121 } 122 else 123 { 124 message = "用户名或密码不存在!"; 125 result = false; 126 } 127 } 128 catch (Exception exp) 129 { 130 message = exp.Message; 131 Console.WriteLine(exp.Message); // 出现异常,打印异常消息 132 } 133 finally 134 { 135 DBHelper.connection.Close(); // 关闭数据库连接 136 } 137 } 138 else if (loginType == "学员") 139 { 140 // 查询用sql语句 141 string sql = string.Format( 142 "SELECT COUNT(*) FROM Student WHERE LogInId='{0}' AND LogInPwd='{1}'",txtLogInId, txtLogInPwd 143 ); 144 145 SqlCommand command = new SqlCommand(sql, DBHelper.connection); // 查询命令 146 147 try 148 { 149 DBHelper.connection.Open(); // 打开连接 150 151 count = (int)command.ExecuteScalar(); // 执行查询语句 152 153 // 如果没有找到,则是非法用户 154 if (count == 1) 155 { 156 result = true; 157 } 158 else 159 { 160 message = "用户名或密码不存在!"; 161 result = false; 162 } 163 } 164 catch (Exception exp) 165 { 166 Console.WriteLine(exp.Message); ; 167 } 168 finally 169 { 170 DBHelper.connection.Close(); // 关闭数据库连接 171 } 172 } 173 174 return result; 175 } 176 177 // 根据登录类型,显示相应的窗体 178 public void ShowUserForm() 179 { 180 switch (cboLogInType.Text) 181 { 182 //// 如果是学员,显示学员窗体 183 case "学员": 184 StudentForm studentForm = new StudentForm(); 185 studentForm.Show(); 186 break; 187 // 如果是管理员,显示管理员窗体 188 case "管理员": 189 AdminForm adminForm = new AdminForm(); 190 adminForm.Show(); 191 break; 192 default: 193 MessageBox.Show("抱歉,您请求的功能尚未完成!"); 194 break; 195 } 196 } 197 } 198 }
通过上面的示例我们不难发现:
● 操作数据库的代码与界面代码混合在一起,一旦数据库发生哪怕是一点细微变化(例如:字段名称改变),代码的改动量都是相当巨大的。
● 当客户要求更换用户界面时(如要求改用IE浏览器方式访问系统),因为代码的混杂,改动工作也是非常巨大的。
● 不利于协作开发,例如负责用户界面设计的工程师必须对美工,业务逻辑,数据库各方面知识都非常了解。
常用的三层架构设计
软件系统最常用的一般会讲到三层架构,其实就是将整个业务应用划分为表示层、业务逻辑层、数据访问层等,有的还要细一些,通过分解业务细节,将不同的功能代码分散开来,更利于系统的设计和开发,同时为可能的变更提供了更小的单元,十分有利于系统的维护和扩展。
常见的三层架构基本包括如下几个部分,如图1所示。
图1 常见的三层架构
数据访问层DAL:用于实现与数据库的交互和访问,从数据库获取数据或保存数据到数据库的部分。
很多人最闹不清的就是数据访问层,到底那部分才算数据访问层呢?有些认为数据库就是数据访问层,这是对定义没有搞清楚,DAL是数据访问层而不是数据存储层,因此数据库不可能是这一层的。也有的把SQLHelper(或其同类作用的组件)作为数据访问层,它又是一个可有可无的东西,SQLHelper的作用是减少重复性编码,提高编码效率,因此如果我习惯在乎效率或使用一个非数据库的数据源时,可以丢弃SQLHelper,一个可以随意弃置的部分,又怎么能成为三层架构中的一层呢? 可以这样定义:与数据源操作有关的代码,就应该放在数据访问层中,属于数据访问层。
业务逻辑层BLL:业务逻辑层承上启下,用于对上下交互的数据进行逻辑处理,实现业务目标。
表示层Web:主要实现和用户的交互,接收用户请求或返回用户请求的数据结果的展现,而具体的数据处理则交给业务逻辑层和数据访问层去处理。
日常开发的很多情况下为了复用一些共同的东西,会把一些各层都用的东西抽象出来。如我们将数据对象实体和方法分离,以便在多个层中传递,例如称为Model。一些共性的通用辅助类和工具方法,如数据校验、缓存处理、加解密处理等,为了让各个层之间复用,也单独分离出来,作为独立的模块使用,例如称为Common。
此时,三层架构会演变为如图2所示的情况。
图2 三层架构演变结果
业务实体Model:用于封装实体类数据结构,一般用于映射数据库的数据表或视图,用以描述业务中客观存在的对象。Model分离出来是为了更好地解耦,为了更好地发挥分层的作用,更好地进行复用和扩展,增强灵活性。
Model是什么?它什么也不是!它在三层架构中是可有可无的。它其实就是面向对象编程中最基本的东西:类。一个桌子是一个类,一条新闻也是一个类,int、string、doublie等也是类,它仅仅是一个类而已。
这样,Model在三层架构中的位置,和int,string等变量的地位就一样了,没有其它的目的,仅用于数据的存储而已,只不过它存储的是复杂的数据。所以如果你的项目中对象都非常简单,那么不用Model而直接传递多个参数也能做成三层架构。 那为什么还要有Model呢,它的好处是什么呢。下面是思考一个问题时想到的,插在这里: Model在各层参数传递时到底能起到做大的作用? 在各层间传递参数时,可以这样: AddUser(userId,userName,userPassword,…,) 也可以这样: AddUser(userInfo) 这两种方法那个好呢。一目了然,肯定是第二种要好很多。 什么时候用普通变量类型(int,string,guid,double)在各层之间传递参数,什么使用Model传递?下面几个方法: SelectUser(int UserId) SelectUserByName(string username) SelectUserByName(string username,string password) SelectUserByEmail(string email) SelectUserByEmail(string email,string password) 可以概括为: SelectUser(userId) SelectUser(user) 这里用user这个Model对象囊括了username,password,email这三个参数的四种组合模式。UserId其实也可以合并到user中,但项目中其它BLL都实现了带有id参数的接口,所以这里也保留这一项。 传入了userInfo,那如何处理呢,这个就需要按照先后的顺序了,有具体代码决定。 这里按这个顺序处理 首先看是否同时具有username和password,然后看是否同时具有email和password,然后看是否有username,然后看是否有email。依次处理。 这样,如果以后增加一个新内容,会员卡(number),则无需更改接口,只要在DAL的代码中增加对number的支持就行,然后前台增加会员卡一项内容的表现与处理即可。
通用类库Common:通用的辅助工具类。
对数据库的共性操作抽象封装成数据操作类(例如DbHelperSQL),以便更好地复用和使代码简洁。数据层底层使用通用数据库操作类来访问数据库,最后完整的三层架构如图3所示。
图3 最后完整的三层架构
数据库访问类是对ADO.NET的封装,封装了一些常用的重复的数据库操作。如微软的企业库SQLHelper.cs,动软的DBUtility/DbHelperSQL等,为DAL提供访问数据库的辅助工具类。
通过以上分析,我们知道如今常用的三层架构是个什么样子,同时,我们也知道了三层架构在使用过程中的一些演化过程。那么,为什么要这样分层,每层结构到底又起什么作用呢?我们继续往下看。
14.1.2 趣味理解:三层架构与养猪
为了更好地理解三层架构,就拿养猪来做个例子吧。俗话说:“没吃过猪肉,还没见过猪跑啊!”。
图4是三层架构化的养猪产业流水线趣味对此图。
图4 三层结构与养猪
对比图3与图4,我们可以看出:
数据库好比猪圈,所有的猪有序地按区域或编号,存放在不同的猪栏里。
DAL好比是屠宰场,把猪从猪圈取出来进行(处理)屠杀,按要求取出相应的部位(字段),或者进行归类整理(统计),形成整箱的猪肉(数据集),传送给食品加工厂(BLL)。本来这里都是同一伙人既管抓猪,又管杀猪的,后来觉得效率太低了,就让一部分人出来专管抓猪了(DBUtility),根据要求来抓取指定的猪。
BLL好比食品加工厂,将猪肉深加工成各种可以食用的食品(业务处理)。
Web好比商场,将食品包装成漂亮的可以销售的产品,展现给顾客(UI表现层)。
猪肉好比Model,无论是哪个厂(层),各个环节传递的本质都是猪肉,猪肉贯穿整个过程。
通用类库Common相当于工人使用的各种工具,为各个厂(层)提供诸如杀猪刀、绳子、剪刀、包装箱、工具车等共用的常用工具(类)。其实,每个部门本来是可以自己制作自己的工具的,但是那样会使效率比较低,而且也不专业,并且很多工作都会是重复的。因此,就专门有人开了这样的工厂来制作这些工具,提供给各个工厂,有了这样的分工,工厂就可以专心做自己的事情了。
当然,这里只是形象的比喻,目的是为了让大家更好地理解,实际的情况在细节上会有所不同。这个例子也只是说明了从猪圈到商场的单向过程,而实际三层开发中的数据交互是双向的,可取可存。不过,据说有一种机器,把猪从这头赶进去,另一头就噗噗噜噜地出火腿肠了。如果火腿肠卖不了了,从那头再放进去,这头猪又原原本本出来了,科幻的机器吧,没想到也可以和三层结构联系上。以上只是笑谈,不过也使三层架构的基本概念更容易理解了。
上面谈了那么多,有人会问,我直接从数据库取出内容直接操作不可以吗?为什么要这么麻烦地用三层架构呢?三层架构到底有什么好处呢?
不分层,当然可以,就好比整个过程不分屠宰场、加工场之类的,都在同一个场所(工厂)完成所有的活(屠杀、加工、销售)。但为什么要加工厂和商场呢?因为当规模比较大的时候,管理起来就会变得非常复杂,这样的养殖方式已经无法满足规模化的需要了。并且,从社会的发展来看,社会分工是人类进步的表现。社会分工的优势就是让适合的人做自己擅长的事情,使平均社会劳动时间大大缩短,生产效率显著提高。能够提供优质高效劳动产品的人才能在市场竞争中获得高利润和高价值。人尽其才,物尽其用最深刻的含义就是由社会分工得出的。软件开发也一样,做小项目的时候,分不分层确实看不出什么差别,并且显得更麻烦啰嗦了。但当项目变大和变复杂时,分层就显示出它的优势来了。所以分不分层要根据项目的实际情况而定,不能一概而论。