面向对象其实是一个非常宽泛的概念,宽泛到不足以用一个章节甚至是一本书来说明面向对象,但MOL将尽量在书中的例子中浸透面向对象的思维。
前面已经讲述了通常的三层代码结构,本节将在三层代码中加入面向对象的元素。这种面向对象的思想在本节中将体现在两个地方:
通常意义上,从数据库中获取数据,一般都需要按照ADO.NET的步骤来写,ADO.NET的代码如下:
/// <summary> /// 获取所有的订单信息 /// </summary> /// <returns></returns> public DataSet GetOrder() { string connectionString = @"Data Source=.;Initial Catalog= ReportServer;User ID=sa;Password=000"; DataSet re = new DataSet(); using (SqlConnection con = new SqlConnection(connectionString)) { string sql = @"select userid,username from users"; SqlDataAdapter ada = new SqlDataAdapter(sql, con); ada.Fill(re); } return re; }
获取到数据源以后,需要在BLL层进行数据装配。BLL层代码比较简单,只是简单调用DAL层返回数据,BLL层代码如下:
public class OrderBll { /// <summary> /// 调用DAL获取数据 /// </summary> /// <returns></returns> public DataSet GetOrder() { OrderDal dal = new OrderDal(); return dal.GetOrder(); } }
然后再以DataSet的形式返回到前台,前台在展示的时候,就需要按照DataSet的格式来展示。比如,展示userid字段,那么就需要这样写:
<ul> <li><span>userid:</span>@ViewBag.userid</li> <li><span>username:</span>@ViewBag.username</li> </ul>
展示效果如图2-16所示。
图2-16 获取数据展示效果
通过上面的代码可以发现,DataSet这个类将贯穿于DAL-BLL-UI这三层之间。而DataSet类是一个与数据库打交道的类,这样就导致我们不管在哪一层(Layer)都需要知道这个数据集(DataSet)里的结构,包括这个数据集中有几个表,每个表中包含哪些字段等。
当操作的表达到几十个的时候,用DataSet操作数据已经变得非常痛苦,所以我们更希望DataSet只停留在DAL层,而其他层只需要与业务相关的类来接收数据既可。
为了达到这样的目的,首先创建一个UserInfo类。这个类包括两个属性,分别是userid和username。这样就可以用UserInfo这个类来接收数据库的查询结果了。通常情况下,我们都会新加一个实体层Mol.Calc.Model,这个层里定义了所有需要实例化的类。在这个层里加入UserInfo类,它的定义代码如下:
namespace Mol.Calc.Model { /// <summary> /// 定义一个用户实体类 /// </summary> public class UserInfo { public string userid { get; set; } public string username { get; set; } } }
接下来修改DAL层的代码,使得查询结果返回的是一个用户实体对象。修改后的代码如下:
/// <summary> /// 获取所有的订单信息 /// </summary> /// <returns></returns> public UserInfo GetOrder() { string connectionString = @"Data Source=.;Initial Catalog= ReportServer; User ID=sa;Password=000"; //定义返回对象 UserInfo re = new UserInfo(); DataSet ds = new DataSet(); using (SqlConnection con = new SqlConnection(connectionString)) { string sql = @"select userid,username from users"; SqlDataAdapter ada = new SqlDataAdapter(sql, con); ada.Fill(ds); } //为返回对象赋值 re.userid = ds.Tables[0].Rows[0]["userid"].ToString(); re.username = ds.Tables[0].Rows[0]["username"].ToString(); return re; }
这样就不用再去操作DataSet了,而是转而去操作UserInfo,这样做更贴近业务,也更符合面向对象的代码思路。
同样的,修改BLL层的代码,返回的也是业务实体UserInfo。代码如下:
/// <summary> /// 调用DAL获取数据 /// </summary> /// <returns></returns> public UserInfo GetOrder() { OrderDal dal = new OrderDal(); return dal.GetOrder(); }
修改UI层的代码如下:
public ActionResult Index() { //获取数据 OrderBll bll = new OrderBll(); UserInfo model = bll.GetOrder(); ViewBag.userid = model.userid; ViewBag.username = model.username; return View(); }
这样就完成了将数据库表实例化的过程。回过头来看一下,在实例化的过程中都做了哪些事情。
除此之外,别无其他。那么,多加一个“业务类”(实体类),对我们的开发有什么影响呢?
弊:
利:
利弊都分析完了,我们来解释一下比较抽象的两个概念“易复用”和“可扩展”。这两个概念是大家最常见到的,但又是最不容易理解的,好像只要谈到什么新技术,都是这两个优点。下面我们就把这两个概念说透。
易复用这个概念是指,我们在A场景的时候,定义了一种事物O。在类似的B场景中,我们可以直接调用事物O,而不是再去定义一个事物。
以上面的示例代码来看,在用户信息展示的功能里,我们定义了一个UserInfo类。如果还有一个页面是“订单信息”,订单页面中也需要展示用户信息,那么可以直接使用UserInfo类。
如果不定义实体类,那么在用户信息展示的功能里,需要用DataSet来接收查询数据,在订单页面,也需要再来一次查询,并放到DataSet中。随着业务复杂度的提交,这样的DataSet会越来越多,即使是再高明的工程师,也会崩溃的。
可扩展,是说如果业务规则有调整的话(在实际情况中会频繁出现),我们只需要修改少量代码,甚至不修改代码,就可以符合实际要求。比如,现在新增一个添加用户的需求,那么只需要在UserInfo中新增一个Insertuser()函数就可以实现了。
其实,上面一大堆让大家听起来似懂非懂的话,说得简单一点就是:SQL语句只出现在DAL层中,除了DAL层,其他层都不知道数据库的存在。
当业务足够复杂的时候,项目中一定会充斥着大量的SQL语句的操作,也就意味着,下面的代码会非常多。
using (SqlConnection con = new SqlConnection(connectionString)) { string sql = @"SQL语句"; SqlDataAdapter ada = new SqlDataAdapter(sql, con); ada.Fill(ds); }
也就是说,除了SQL语句,程序中有大量的相似代码,这显然是和DRY原则相违背的。
PS:DRY原则——Don't Repeat Yourself Principle,直译为“不要重复自己”原则,说白了就是不要写重复的代码。
可以把这些重复的代码都拿出来,然后将其写成一个公共的类,这个类就是数据库操作类。
PS:这样的思想非常重要,根据这样的思想,你可以写出很多帮助类,如http帮助类、文件帮助类……
大家都知道,关于数据库的操作无非就是CRUD(增、删、改、查),那么我们在数据库操作类中只需要实现这些功能的函数就可以了。例如,下面的代码要实现一个查询的功能。
public DataSet Get(string sql) { DataSet ds = new DataSet(); using (SqlConnection con = new SqlConnection(connectionString)) { //这里的SQL语句将会以参数的形式传递到本函数中 SqlDataAdapter ada = new SqlDataAdapter(sql,con); ada.Fill(ds); } return ds; }
当我们需要查询的时候,只需要把查询的SQL语句传到Get函数中就可以了。当然,这个帮助类还需要支持存储过程。
大家可以去网络上找找SqlHelper,网络上有很多版本的帮助类,但都大同小异。MOL在这里不会带大家去实现一个帮助类,不过大家一定要掌握这个抽象过程的思路。
MOL今天有点累了,就先讲到这里了。大家回去以后一定要在网络上找找SqlHelper,并且自己动手写一下。
又是一个阳光明媚的早晨,MOL刚坐在工位上,泡上一杯“千年养生”茶,打开网页准备看看新闻,这时,刘朋、鹏辉、冲冲3个人就杀气腾腾地向我走了过来。
刘朋:MOL,昨天我查了很多SqlHelper,它们确实把大部分的类似代码都抽象了出来,并进行了封装,但是老感觉哪里不对。
鹏辉:是的,你昨天讲的时候说,我们更希望用一个业务类去接收数据,但是SqlHelper的返回值都是DataSet类型的,我们没办法把所有的业务类都抽象出来啊。
冲冲:我在自己写代码的时候,也遇到了相同的问题,比如我利用SqlHelper类的查询函数,得到一个DataSet,还需要自己把这个DataSet转换成UserInfo类。而且这些转换的代码是有点类似的,不同的地方只是属性名称。那我们是不是可以把这个转换功能也抽象出来呢?
MOL:非常好,我就喜欢你们这没有见过世面的样子,今天我们就来说一下,如何做一个加强版的SqlHelper。
不管是你自己写,还是从网络上找,我们现在已经得到了一个SqlHelper。这个SqlHelper已经可以封装大部分相似的代码,但它还是一个面向SQL语句的类。如何让SqlHelper操作一个实体类呢?我们还以UserInfo来举例。
我们想让SqlHelper中的查询函数返回值不是DataSet,而是一个IList<UserInfo>集合。那么就需要在代码中为UserInfo对象的每一个属性赋值,这个功能在2.4.1节中已经实现。对SqlHelper的Get()方法进行修改如下(也可能你的查询方法不叫Get,只需要修改你对应的查询方法就可以):
public IList<UserInfo> Get(string sql) { DataSet ds = new DataSet(); using (SqlConnection con = new SqlConnection(connectionString)) { //这里的SQL语句将会以参数的形式传递到本函数中 SqlDataAdapter ada = new SqlDataAdapter(sql,con); ada.Fill(ds); } //定义一个要返回的实体集合 IList<UserInfo> userList = new List<UserInfo>(); //遍历数据库的查询结果 foreach (DataRow dr in ds.Tables[0].Rows) { //对于查询结果中的每一行记录,都对应一个业务实体对象 //定义这样一个对应的业务实体对象 UserInfo user = new UserInfo(); //为业务对象的属性赋值 user.userid = dr["userid"].ToString(); user.username = dr["username"].ToString(); //将这个业务对象添加到实体集合中 userList.Add(user); } return userList; }
这样做显然是不行的,我们写SqlHelper的目的是抽象出大部分的共同的、相似的代码,用来剥离相同的操作。
但是经过我们这样修改,SqlHelper的Get方法就只能返回UserInfo集合了,这样明显违背了开发的初衷。我们想要的是这样的Get方法:我让它输出什么,它就乖乖地输出什么。
刘朋:我知道,我们只需要输出Object类型就可以了,然后在需求的地方再进行类型转换,如在UserInfo的BLL中,我就把Object转换成IList<UserInfo>,在Order的BLL中,我就把Object转换成IList<Order>。
MOL:不错,朽木可雕。给你10分钟,把你的想法用代码实现。
MOL又可以看几分钟的新闻了,得意中……还没得意两分钟,刘朋垂头丧气地回来了。
刘朋:这想法根本就不现实嘛,说是返回Object,我连实体的属性都没办法抽象啊。而且在需要的时候再进行转换,这个转换的代码也会重复,这样违反了DRY原则。
MOL:非常好,碰个墙,长个见识。说明用Object作为返回类型也是行不通的。那么到底用什么来作为返回类型呢?MOL也不知道。
一片吐槽声响了起来……
MOL:安静,安静,大家都是有身份证的人,要注意文明用语。
MOL:既然我们都不知道应该用什么类型返回,那何不让调用方来决定它返回什么类型呢?
鹏辉:我知道了,用泛型!
MOL:非常好,就是用泛型。利用泛型,可以定义一个“假”的返回类型。它看起来像这样:
public IList<T> Get<T>(string sql)
这里出现了两个T,第一个T以IList<T>的形式出现,表示Get()函数要返回的是一个IList<T>的类型;第二个T以Get<T>的形式出现,表示调用Get()函数的时候就需要指定T是什么类型了。如果想要返回一个IList<Panda>集合,那么就需要这样调用:
SqlHelper helper=new SqlHelper(); IList<Panda> pandas=helper.Get<Panda>();
有了这样的思路以后,只需要去实现Get()函数就可以了。实现的时候,我们从数据库中查询数据得到DataSet,给T赋值,T的属性是什么呢?需要通过反射得到T的属性,并为之赋值。实现后的Get()函数如下:
01 public IList<T> Get<T>(string sql) where T:class,new() 02 { 03 DataSet ds = new DataSet(); 04 using (SqlConnection con = new SqlConnection(connectionString)) 05 { 06 //这里的SQL语句将会以参数的形式传递到本函数中 07 SqlDataAdapter ada = new SqlDataAdapter(sql, con); 08 ada.Fill(ds); 09 } 10 //定义一个业务对象集合 11 IList<T> modelList = new List<T>(); 12 //获取T类型实体的属性类型和值 13 PropertyInfo[] pis = typeof(T).GetProperties(); 14 foreach (DataRow dr in ds.Tables[0].Rows) 15 { 16 //定义一个业务对象 17 T model = new T(); 18 //为业务对象的属性赋值 19 //因为我们不知道具体的属性名,所以需要遍历业务对象的每一个属性,并为之赋值 20 foreach (PropertyInfo pi in pis) 21 { 22 //这样的赋值方法是反射机制特有的 23 pi.SetValue(model, dr[pi.Name], null); 24 } 25 modelList.Add(model); 26 } 27 return modelList; 28 }
注意看,在第1行的后面有这样的代码“where T:class,new()”,它是用来描述T这个类型是一个类(class),并且这个类是可以new的。如果不写这句代码,在函数体内,就不可以用T modle=new T();了。
这样就实现了一个可以返回实体集合的查询方法,接下来的一个小时,大家把现有的SqlHelper中的函数都改造成泛型方法,使得这些函数都可以返回泛型对象。
一个小时后,大家陆续都写完了自己的泛型编程,MOL把他们3个人的代码都看完以后,又提出了一个问题:每个函数后面都有描述T的代码where T:class,new(),这是非常明显的重复代码,是违反DRY原则的,所以最好把这一句代码放在SqlHelper类后面,这样,SqlHelper就变成了一个泛型类,而类里的函数后面也不用分别再去描述T类型了。修改后的SqlHelper是这样的:
public class SqlHelper<T> where T : class, new() { public IList<T> Get(string sql){……} public T Get(string sql){……} public bool Insert(T model){……} public bool update(T model){……} public bool delete(T model){……} }
到这里为止,一个相对完美的SqlHelper就完成了。
MOL不会给出SqlHelper的示例,正如前面所讲的,一千个人眼里就有一千个哈姆雷特,每个人对SqlHelper的理解不一样,实现也会不一样,只要适合自己的,就是好用的。
正当MOL说得眉飞色舞的时候,突然停电了,冲冲一声怒吼:“我的代码还没保存呢,那可是我一个小时的心血啊!”