购买
下载掌阅APP,畅读海量书库
立即打开
畅读海量书库
扫码下载掌阅APP

2.4 向三层代码中加入面向对象

面向对象其实是一个非常宽泛的概念,宽泛到不足以用一个章节甚至是一本书来说明面向对象,但MOL将尽量在书中的例子中浸透面向对象的思维。

前面已经讲述了通常的三层代码结构,本节将在三层代码中加入面向对象的元素。这种面向对象的思想在本节中将体现在两个地方:

2.4.1 实例化数据库表

通常意义上,从数据库中获取数据,一般都需要按照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层,其他层都不知道数据库的存在。

2.4.2 增加数据库操作类

当业务足够复杂的时候,项目中一定会充斥着大量的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,并且自己动手写一下。

2.4.3 加强版的数据库操作类

又是一个阳光明媚的早晨,MOL刚坐在工位上,泡上一杯“千年养生”茶,打开网页准备看看新闻,这时,刘朋、鹏辉、冲冲3个人就杀气腾腾地向我走了过来。

刘朋:MOL,昨天我查了很多SqlHelper,它们确实把大部分的类似代码都抽象了出来,并进行了封装,但是老感觉哪里不对。

鹏辉:是的,你昨天讲的时候说,我们更希望用一个业务类去接收数据,但是SqlHelper的返回值都是DataSet类型的,我们没办法把所有的业务类都抽象出来啊。

冲冲:我在自己写代码的时候,也遇到了相同的问题,比如我利用SqlHelper类的查询函数,得到一个DataSet,还需要自己把这个DataSet转换成UserInfo类。而且这些转换的代码是有点类似的,不同的地方只是属性名称。那我们是不是可以把这个转换功能也抽象出来呢?

MOL:非常好,我就喜欢你们这没有见过世面的样子,今天我们就来说一下,如何做一个加强版的SqlHelper。

不管是你自己写,还是从网络上找,我们现在已经得到了一个SqlHelper。这个SqlHelper已经可以封装大部分相似的代码,但它还是一个面向SQL语句的类。如何让SqlHelper操作一个实体类呢?我们还以UserInfo来举例。

1. 查询功能

我们想让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中的函数都改造成泛型方法,使得这些函数都可以返回泛型对象。

2. 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说得眉飞色舞的时候,突然停电了,冲冲一声怒吼:“我的代码还没保存呢,那可是我一个小时的心血啊!” wjcz8tnt6TCFqBMnNCwY77ia0EbrgcXHAD8oed03EAUcOZRrVdhpUgBxg0LEaaBw

点击中间区域
呼出菜单
上一章
目录
下一章
×