第3 章
系统架构 
在进行具有一定规模的数据库应用系统开发时,如果仍采用传统的代码编写方式,将一
个功能模块的所有代码都写在Form 中的做法是非常不合适的。因为以这种方式编写的代
码,在面临需求变更时,所引发的修改往往是灾难性的。即使是一个数据库IP地址的变更
都可能会导致数十甚至上百处的代码变化(每个涉及连接数据库的代码都要修改连接字符
串)。所有的代码堆砌在一个窗体内,职责繁杂造成代码可读性、可维护性、可移植性差,违
反了单一职责原则;在面临需求变更时,必须通读所有代码,再在合适的位置做修改,违反了
对修改关闭、对扩展开放的开放-关闭原则;所有代码没有任何抽象,不对接口编程,代码耦
合度太高,无法适应较大的需求变更,违反了依赖倒转原则。
例如,第8章的在线考试系统属于中小型应用系统,该系统分为两个子系统:教师端子
系统和学生端子系统。首先,两个子系统都访问同一个数据库,其中有许多访问数据库的操
作及业务逻辑都是相同的,如果再像传统的编写方式一样将代码都放在各个Form 中,那就
无法实现代码的复用,必然造成大量的冗余,为后续扩展和维护带来麻烦。此外,本在线考
试系统面对的客户不同,其对于软件产品的经济投入也会不同,必然涉及不同的用户采用不
同类型数据库的情况,如果代码不做到充分解耦,在面对数据库类型变更时,几乎要把所有
访问数据库的代码修改一遍,显然是不现实的。
采用一些典型架构开发软件,其目的就是为了使软件代码更具可维护性、可扩展性、可
复用性,从而令系统更为灵活,可以适应任何合理的需求变更。
3.1 三层架构简介
在软件体系架构设计中,分层式结构是最常见也是最重要的一种结构,微软推荐的分层
式结构一般分为三层,即三层架构(3-TierApplication),这三层从下至上分别为:数据访问
层(DataAccessLayer,DAL)、业务逻辑层(BusinessLogicLayer,BLL)以及表示层(User 
Interface,UI),如图3-1所示。
1.数据访问层
数据访问层又称持久层,该层负责访问数据库,通俗点讲就是该层负责实现对数据库各
表的增、删、改、查操作,当然数据存储方式不一定是数据库,也可能是文本文件、XML文档
等。该层不负责任何业务逻辑的处理,更不涉及任何界面元素。
2.业务逻辑层
业务逻辑层又称领域层。该层是整个系统的核心,负责处理所有的业务流程,从简单的
数据有效性验证到复杂的对一整条业务链的处理。例如,商城购物,从查询商品到添加购物

68
基于C# 
的管理信息系统开发(第
3 
版)


图3-
1 
三层架构示意图

车,再到下订单,直至付款结束等过程。当然,不排除个别软件项目业务逻辑简单,导致业务
逻辑层代码较少的情况。例如,本章由于篇幅所限,所提供的示例业务逻辑简单,就会出现
业务逻辑层“瘦小”的现象。当业务逻辑层需要访问数据库时,它会通过调用数据访问层来
实现,而不直接访问数据库。这样可以使业务逻辑层的实现与具体数据库无关,从而有效解
耦。业务逻辑层同样不涉及任何界面元素。

3. 
表示层
表示层即用户界面,表示层可以是WinForm 、WebForm 甚至是控制台,该层负责用户
与系统的交互,接收用户的输入及事件触发。理想状态下,该层不应包含系统的业务逻辑, 
即使有逻辑代码,也应只与界面元素有关。例如,根据用户的身份控制按钮的可用性等。具
体的业务逻辑可通过调用业务逻辑层来完成,该层不能直接调用数据访问层,更不能直接访
问数据库。

如此分层具有以下优点。

(1)分散关注:开发人员可以只关注自己所负责一层的技术实现。例如,负责数据访
问层的开发人员,可以不需要关心系统的任何业务逻辑,更不用关心界面的设计。只需要关
心所访问的数据库类型及表结构,最大限度地实现对各数据表的增、删、改、查操作即可。如
此,对于开发人员的技术要求可以降到最低,项目经理也可以根据团队成员的专长,合理为
其分配擅长的领域工作。
(2)松散耦合:三层之间呈线性调用,业务逻辑层的实现不依赖于数据访问层的具体
实现,表示层的实现同样不依赖于业务逻辑层的具体实现,可以很容易用新的实现来替换原
有层次的实现,而不会对其他层造成影响。
(3)逻辑复用:个别层代码所生成的组件(动态链接库文件DLL)可以直接被其他项目
所使用。例如,某系统最初只有WinForm 版本,随着业务扩展,逐渐有了Web版、手机版等
需求,但各版本功能一致,业务逻辑相同。这样在新建Web版和手机版项目时,只需将原
WinForm 版项目中的业务逻辑层和数据访问层组件引用到新项目中,直接使用即可,无须
再重写代码。
当然,在获得优点的同时,分层也不可避免地会付出一些“代价”,其缺点如下。

(1)降低了系统性能:原本在不采用分层的情况下,UI 可以直接访问数据库,但现在要
通过层层调用才能达到同样的目的,必然会在运行效率上有所降低。

第
3 
章 系统架构

(2)容易导致级联修改:用户某些需求的变化,如用户觉得某个功能模块目前所维护
的信息不够,需要再加入一些信息,势必导致UI的变化,为了存储这些数据也会导致相应
数据表字段的增加,从而数据访问层和业务逻辑层都会受到影响,这就是级联修改。
3.简单三层架构
2 

简单三层架构即基本三层架构,其结构如图3-1所示。现以一个小示例介绍简单三层
架构的代码编写方式。

示例所用数据库及业务需求如下。

要求设计一个通讯录软件,实现对联系人类型的定制以及联系人的管理,即实现对联系
人类型和联系人的增、删、改、查功能。
数据库名为MyDb,其中有两个数据表,名为LinkmanType(联系人类型)表和
Linkmen(联系人)表,表结构分别如表3-1和表3-2所示。

表3-
1 
LinkmanType(联系人类型)表结构

字段名数据类型长度主键含义
typeId nvarchar 20 是联系人类型编号
typeName nvarchar 20 否联系人类型名称

表3-
2 
Linkmen(联系人)表结构

字段名数据类型长度主键含义
lkmId int 4 是联系人编号(自动加1) 
lkmName nvarchar 10 否联系人姓名
lkmMPNum nvarchar 12 否联系人移动电话
lkmOPNum nvarchar 20 否联系人办公室电话
lkmEmail nvarchar 30 否联系人电子邮箱
lkmCompName nvarchar 50 否联系人单位名称
typeId nvarchar 20 否联系人类型编号

3.2.1 
数据访问层
1.创建一个空解决方案
打开VisualStudio2019,单击“创建新项目”,在弹出的“创建新项目”对话框中的“搜索
模板”搜索框内输入“空白解决方案”,查询“空白解决方案”模板,如图3-2所示。选择查询
到的“空白解决方案”模板,单击“下一步”按钮,打开“配置新项目”窗口,修改项目名称为
“ThreLayer,(”) 选择好位置,单击“创建”按钮,完成空解决方案的创建,如图3-3所示。

2.创建数据访问层项目DAL 
在图3-3中右侧的“解决方案资源管理器”子窗口中的“解决方案ThreLayer”节点上单


70
基于C# 
的管理信息系统开发(第
3 
版)


图3-
2 
“创建新项目”对话框


图3-
3 
创建后的空解决方案

击鼠标右键,选择“添加”→“新建项目”,会再次弹出类似图3-2的“添加新项目”对话框,在
其中搜索“类库”模板,选择“类库(.NETFramework)模(”) 板,单击“下一步”按钮,将项目名
称改为“DAL”,单击“创建”按钮,此时,图3-3的解决方案会变成如图3-4所示状态。

3.编写数据访问层代码
右击DAL项目中的Clas1.s文件,在弹出菜单中选择“删除”命令,将DAL默认创建

c
的Clas1.s文件删除。右击DAL项目,在弹出菜单中选择“添加”→“类”,弹出如图3-5所

c


第
3 
章 系统架构


图3-
4 
创建DAL 
项目之后的解决方案

示的“添加新项”对话框。


图3-
5 
“添加新项”对话框
在图3-5中,将名称命名为“LikmanTyp.s”,单击“添加”按钮,在DAL 项目中,会出

nec
现名为“Linec”的类文件。

kmanTyp.s


72 基于C# 的管理信息系统开发(第3 版) 
在3.1节提到数据访问层的任务就是实现对数据库的操作,实现对各数据表数据的增
加、删除、修改、查询。作为LinkmanType类,其功能就是要实现对LinkmanType数据表的
增加、删除、修改、查询操作。
DAL项目中的LinkmanType.cs文件代码如下。 
namespace DAL //需要手动引用System.Data 和System.Data.SqlClient 命名空间
{ 
//LinkmanType 类默认无访问修饰符,此处必须声明为public,因BLL 在调用DAL 中该类时, 
//属于跨项目访问,只有声明为public 才能够被BLL 所访问。 
public class LinkmanType 
{ 
///<summary> 
///数据库连接字符串,server 为数据库服务器的名字或IP 地址,database 为数据库的名字, 
///uid 为数据库用户名,pwd 为该用户的密码 
///</summary> 
public string connString = "server=(local);database=mydb;uid=test;pwd= 
test"; 
///<summary> 
///插入数据的方法 
///</summary> 
///<param name="typeId">要插入的联系人类型编号</param> 
///<param name="typeName">要插入的联系人类型名称</param> 
///<returns>插入成功返回true,插入失败返回false</returns> 
public bool insert(string typeId,string typeName) 
{ 
try 
{ 
SqlConnection conn = new SqlConnection(); 
conn.ConnectionString =connString ; 
conn.Open(); 
SqlCommand cmd = new SqlCommand(); 
cmd.Connection = conn; 
cmd.CommandText="insert into linkmantype(typeid,typename) 
values(@typeid,@typename)"; //参数形式的SQL 语句 
//以下两句为SQL 参数赋值 
cmd.Parameters.Add(new SqlParameter("@typeid", typeId)); 
cmd.Parameters.Add(new SqlParameter("@typename", typeName)); 
cmd.ExecuteNonQuery(); 
conn.Close(); 
return true; 
} 
catch 
{ 
return false; 
} 
} 
///<summary> 
///修改数据的方法 
///</summary> 
///<param name="typeId">要修改的联系人类型新编号</param> 
///<param name="typeName">要修改的联系人类型新名称</param> 
///<param name="oldTypeId">要修改的联系人类型原编号</param>

第3 章 系统架构 73 
///<returns>修改成功返回true,修改失败返回false</returns> 
public bool update(string typeId,string typeName,string oldTypeId) 
{ 
try 
{ 
SqlConnection conn = new SqlConnection(); 
conn.ConnectionString = connString; 
conn.Open(); 
SqlCommand cmd = new SqlCommand(); 
cmd.Connection = conn; 
cmd.CommandText="update linkmantype set typeid=@typeid, 
typename=@typename where typeid=@oldTypeId"; 
cmd.Parameters.Add(new SqlParameter("@typeid", typeId)); 
cmd.Parameters.Add(new SqlParameter("@typename", typeName)); 
cmd.Parameters.Add(new SqlParameter("@oldTypeId", oldTypeId)); 
cmd.ExecuteNonQuery(); 
conn.Close(); 
return true; 
} 
catch 
{ 
return false; 
} 
} 
///<summary> 
///删除数据的方法 
///</summary> 
///<param name="typeId">要删除的联系人类型编号</param> 
///<returns>删除成功返回true,删除失败返回false</returns> 
public bool delete(string typeId) 
{ 
try 
{ 
SqlConnection conn = new SqlConnection(); 
conn.ConnectionString = connString; 
conn.Open(); 
SqlCommand cmd = new SqlCommand(); 
cmd.Connection = conn; 
cmd.CommandText = "delete from linkmantype where typeid=@typeid"; 
cmd.Parameters.Add(new SqlParameter("typeid", typeId)); 
cmd.ExecuteNonQuery(); 
conn.Close(); 
return true; 
} 
catch 
{ 
return false; 
} 
} 
///<summary> 
///查询数据的方法 
///</summary> 
///<param name="strWhere">strWhere 为空字符串时,代表查询表中所有数据

74 基于C# 的管理信息系统开发(第3 版) 
///为非空时应传入查询依据,即where 子句的内容,如"typeName='朋友'"等</param> 
///<returns>返回null 代表查询失败,返回非null 代表查询成功 
///且结果存于返回的DataTable 中</returns> 
public DataTable select(string strWhere) 
{ 
try 
{ 
string sql = "select * from linkmantype"; 
if (strWhere != "") 
{ 
sql += " where " + strWhere; 
} 
SqlConnection conn = new SqlConnection(); 
conn.ConnectionString = connString; 
SqlDataAdapter da = new SqlDataAdapter(sql, conn); 
DataSet ds = new DataSet(); 
da.Fill(ds); 
return ds.Tables[0]; 
} 
catch 
{ 
return null; 
} 
} 
} 
}
接下来实现Linkmen表的数据访问层代码。采用与创建LinkmanType.cs类文件同样
的方法,在DAL项目中创建Linkmen.cs类文件。在其中编写对Linkmen表的访问代码
如下。 
namespace DAL //需要手动引用System.Data 和System.Data.SqlClient 命名空间
{ 
public class Linkmen 
{ 
public string connString = "server=(local);database=mydb;uid=test; 
pwd=test"; 
///<summary> 
///插入数据的方法 
///</summary> 
///<param name="lkmName">姓名</param> 
///<param name="lkmMPNum">移动电话</param> 
///<param name="lkmOPNum">固定电话</param> 
///<param name="lkmEmail">电子邮箱</param> 
///<param name="lkmCompName">单位名称</param> 
///<param name="typeId">联系人类型编号</param> 
///<returns>插入成功返回true,插入失败返回false</returns> 
public bool insert(string lkmName,string lkmMPNum,string lkmOPNum, 
string lkmEmail,string lkmCompName,string typeId) 
{ 
try 
{ 
SqlConnection conn = new SqlConnection();

第3 章 系统架构 75 
conn.ConnectionString = connString; 
conn.Open(); 
SqlCommand cmd = new SqlCommand(); 
cmd.Connection = conn; 
cmd.CommandText = "insert into linkmen(lkmname, lkmMPNum, lkmOPNum, 
lkmEmail, lkmCompName, typeId) values(@lkmname, 
@lkmMPNum, @lkmOPNum, @lkmEmail, @lkmCompName, 
@typeId)"; 
cmd.Parameters.Add(new SqlParameter("@lkmname", lkmName)); 
cmd.Parameters.Add(new SqlParameter("@lkmMPNum", lkmMPNum)); 
cmd.Parameters.Add(new SqlParameter("@lkmOPNum", lkmOPNum)); 
cmd.Parameters.Add(new SqlParameter("@lkmEmail", lkmEmail)); 
cmd.Parameters.Add(new SqlParameter("@lkmCompName", lkmCompName)); 
cmd.Parameters.Add(new SqlParameter("@typeId", typeId)); 
cmd.ExecuteNonQuery(); 
conn.Close(); 
return true; 
} 
catch 
{ 
return false; 
} 
} 
///<summary> 
///修改数据的方法 
///</summary> 
///<param name="lkmId">联系人编号</param> 
///<param name="lkmName">姓名</param> 
///<param name="lkmMPNum">移动电话</param> 
///<param name="lkmOPNum">固定电话</param> 
///<param name="lkmEmail">电子邮箱</param> 
///<param name="lkmCompName">单位名称</param> 
///<param name="typeId">联系人类型编号</param> 
///<returns>修改成功返回true,修改失败返回false</returns> 
public bool update(string lkmId,string lkmName, string lkmMPNum, 
string lkmOPNum,string lkmEmail, string lkmCompName, string typeId) 
{ 
try 
{ 
SqlConnection conn = new SqlConnection(); 
conn.ConnectionString = connString; 
conn.Open(); 
SqlCommand cmd = new SqlCommand(); 
cmd.Connection = conn; 
cmd.CommandText = "update linkmen set lkmname=@lkmname, 
lkmMPNum=@lkmMPNum, lkmOPNum=@lkmOPNum, 
lkmEmail=@lkmEmail, lkmCompName=@lkmCompName, 
typeId=@typeId where lkmid=@lkmid"; 
cmd.Parameters.Add(new SqlParameter("@lkmname", lkmName)); 
cmd.Parameters.Add(new SqlParameter("@lkmMPNum", lkmMPNum)); 
cmd.Parameters.Add(new SqlParameter("@lkmOPNum", lkmOPNum)); 
cmd.Parameters.Add(new SqlParameter("@lkmEmail", lkmEmail));

76 基于C# 的管理信息系统开发(第3 版) 
cmd.Parameters.Add(new SqlParameter("@lkmCompName", lkmCompName)); 
cmd.Parameters.Add(new SqlParameter("@typeId", typeId)); 
cmd.Parameters.Add(new SqlParameter("@lkmid", lkmId)); 
cmd.ExecuteNonQuery(); 
conn.Close(); 
return true; 
} 
catch 
{ 
return false; 
} 
} 
///<summary> 
///删除数据的方法 
///</summary> 
///<param name="lkmId">要删除的联系人的编号</param> 
///<returns>删除成功返回true,删除失败返回false</returns> 
public bool delete(string lkmId) 
{ 
try 
{ 
SqlConnection conn = new SqlConnection(); 
conn.ConnectionString = connString; 
conn.Open(); 
SqlCommand cmd = new SqlCommand(); 
cmd.Connection = conn; 
cmd.CommandText = "delete from linkmen where lkmid=@lkmid"; 
cmd.Parameters.Add(new SqlParameter("@lkmid", lkmId)); 
cmd.ExecuteNonQuery(); 
conn.Close(); 
return true; 
} 
catch 
{ 
return false; 
} 
} 
///<summary> 
///查询数据的方法 
///</summary> 
///<param name="strWhere">strWhere 为空字符串时,代表查询表中所有数据 
///为非空时应传入查询依据,即where 子句的内容,如"lkmName='张三'"等</param> 
///<returns>返回null 代表查询失败,返回非null 代表查询成功 
///且结果存于返回的DataTable 中</returns> 
public DataTable select(string strWhere) 
{ 
try 
{ 
string sql = "select linkmen.typeid as typeid, typename, lkmname, 
lkmid,lkmMPNum, lkmOPNum, lkmEmail, lkmCompName from linkmen 
left join linkmantype on linkmen.typeid=linkmantype.typeid"; 
if (strWhere != "")

第3 章 系统架构 77 
{ 
sql += " where " + strWhere; 
} 
SqlConnection conn = new SqlConnection(); 
conn.ConnectionString = connString; 
SqlDataAdapter da = new SqlDataAdapter(sql, conn); 
DataSet ds = new DataSet(); 
da.Fill(ds); 
return ds.Tables[0]; 
} 
catch 
{ 
return null; 
} 
} 
} 
}
上述代码为数据访问层的最基本实现,但还存在如下诸多不足。
(1)增加、删除、修改功能的代码基本类似,存在大量的冗余。事实上,除了所执行的
SQL语句以及所传递的SQL参数不同外,其他代码是一样的。此外,各类的查询功能实现
也极其相似,只是Select语句不同罢了。再者,每个类中都维护着同一个数据库连接字符
串,不但冗余,更会给数据库迁移带来莫大的灾难,每次数据库迁移,都要修改每个类的连接
字符串,何其麻烦! 因此,完全可以对这些冗余的代码进行精简。
(2)以各类的insert函数为例,操作联系人类型表的LinkmanType类中的insert函数
有两个形参:typeId和typeName,分别代表要插入类型的两个字段,而操作联系人表的
Linkmen类中的insert函数则有6个参数:lkmName、lkmMPNum、lkmOPNum、lkmEmail、
lkmCompName和typeId,可见,函数的形参个数与数据表的字段数是成正比的,如果一个
数据表的字段数过多,势必导致相关函数的形参数过于冗长,非常不利于调用和规范化
管理。
3.2.2 数据访问通用类库
图3-6 添加DBUtility类库
后的解决方案
解决以上第一个不足,精简冗余的数据库访问代
码,可考虑再创建一个数据库访问辅助类库,将重复的
代码进行封装。
1.创建数据访问通用类库DBUtility 
右击解决方案,单击“添加”→“新建项目”,创建一
个名为“DBUtility”的类库项目。添加项目之后的解决
方案列表如图3-6所示。
2.编写数据访问通用类库中的类代码
在图3-6中,删除DBUtitlity项目中的Class1.cs类
文件,新建一个名为“DbHelperSQL.cs”的类文件, 
DbHelperSQL.cs类代码如下。

78 基于C# 的管理信息系统开发(第3 版) 
namespace DBUtility //需要手动引用命名空间System.Data 和System.Data.SqlClient 
{ 
public class DbHelperSQL 
{ 
public static string connString = "server=(local);database=mydb; 
uid=test;pwd=test"; 
///<summary> 
///数据增、删、改要调用的通用方法 
///</summary> 
///<param name="sql">要执行的SQL 语句</param> 
///<param name="sqlParams">所有要传给SQL 语句的参数集合</param> 
///<returns>返回true 表示执行成功,false 为执行失败</returns> 
public static bool ExecuteSql(string sql,List<SqlParameter> sqlParams) 
{ 
try 
{ 
SqlConnection conn = new SqlConnection(); 
conn.ConnectionString = connString; 
conn.Open(); 
SqlCommand cmd = new SqlCommand(); 
cmd.Connection = conn; 
cmd.CommandText = sql; 
//遍历传过来的SQL 参数集合,将其逐一加到SqlCommand 对象的参数集合中 
for(int i=0;i<sqlParams.Count ;i++) 
cmd.Parameters.Add(sqlParams[i]); 
cmd.ExecuteNonQuery(); 
conn.Close(); 
return true; 
} 
catch 
{ 
return false; 
} 
} 
///<summary> 
///查询数据的通用方法 
///</summary> 
///<param name="sql">select 语句</param> 
///<returns>返回null 代表查询失败,返回非null 代表查询成功 
///且结果存于返回的DataTable 中</returns> 
public static DataTable Query(string sql) 
{ 
try 
{ 
SqlConnection conn = new SqlConnection(); 
conn.ConnectionString = connString; 
SqlDataAdapter da = new SqlDataAdapter(sql, conn); 
DataSet ds = new DataSet(); 
da.Fill(ds); //将sql 语句的查询结果填充到ds 中 
return ds.Tables[0]; 
} 
catch 
{

第3 章 系统架构 79 
return null; 
} 
} 
} 
}
如此一来,数据访问层DAL项目中的各函数,只需要调用DBUtility中的函数即可实
现对数据库的访问。
3.为数据访问层项目DAL添加引用
要在DAL中调用DBUtility中的类和函数,需将DBUtility项目引入到DAL项目中: 
右击DAL项目节点下的“引用”节点,在弹出式菜单中选择“添加引用”命令,会弹出如图3-7 
所示的“引用管理器”对话框。
图3-7 “引用管理器”对话框
在图3-7中,选择“项目”→“解决方案”选项,勾选DBUtility项目,单击“确定”按钮,会
发现DAL项目的引用列表中,多出一个名为“DBUtility”的引用,如图3-8所示。
图3-8 添加引用后的DAL引用列表

80 基于C# 的管理信息系统开发(第3 版) 
4.修改3.2.1节中的数据访问层代码
现在,可以在DAL 项目中调用DBUtility 中所有公开(public)的类和函数。以
Linkmen类的insert函数为例,此时,insert函数可通过调用DBUtility的DbHelperSQL类
中的ExecuteSql函数来实现数据的插入,而无须书写重复冗余的代码,修改后的insert函
数如下。 
public bool insert(string lkmName,string lkmMPNum,string lkmOPNum, 
string lkmEmail,string lkmCompName,string typeId) 
{ 
string sql = "insert into linkmen(lkmname,lkmMPNum,lkmOPNum,lkmEmail, 
lkmCompName, typeId)values(@lkmname, @lkmMPNum, @lkmOPNum, 
@lkmEmail, @lkmCompName, @typeId)"; 
List<SqlParameter> sqlParams = new List<SqlParameter>(); 
//以下六句代码把要传给SQL 语句的参数存入集合,以便传递给函数 
sqlParams.Add(new SqlParameter("@lkmname", lkmName)); 
sqlParams.Add(new SqlParameter("@lkmMPNum", lkmMPNum)); 
sqlParams.Add(new SqlParameter("@lkmOPNum", lkmOPNum)); 
sqlParams.Add(new SqlParameter("@lkmEmail", lkmEmail)); 
sqlParams.Add(new SqlParameter("@lkmCompName", lkmCompName)); 
sqlParams.Add(new SqlParameter("@typeId", typeId)); 
//调用通用类库中的ExecuteSql 方法执行SQL 语句 
return DBUtility.DbHelperSQL.ExecuteSql(sql, sqlParams); 
}
由上述代码可见,在insert函数中,已无须再编写SqlConnection、SqlCommand对象的
创建和属性赋值、方法调用等代码。同样,update、delete等函数也不必再写这些重复代码, 
代码复用度更高。此外,由于连接字符串已写在了DbHelperSQL类中,不会在其他类中出
现,对于数据库迁移所造成的修改量降到了最低———只需修改DbHelperSQL中的连接字
符串即可。完整的数据访问层代码将在解决第二个不足之后给出。
3.2.3 实体类库
针对第二个不足———函数参数个数因数据表字段过多而变得复杂的问题,可以利用面
向对象的思想,将数据表封装成类。如对于LinkmanType 表,可以定义Model. 
LinkmanType类,其中包括两个成员变量typeId和typeName,分别对应LinkmanType表
的两个字段。而对于Linkmen表,可以定义Model.Linkmen类,其中包含7个成员变量
lkmId、lkmName、lkmMPNum、lkmOPNum、lkmEmail、lkmCompName和typeId,分别对
应Linkmen表的7个字段。这样在设计对应的insert和update函数时,可以用对应的类类
型来作为形参,例如Linkmen类的insert函数,只需用以下方式定义即可。 
public bool insert(Model.Linkmen mLKM); 
如此大大缩减了形参数量,简化了函数结构。
可以创建一个专门的类库,用于设计这些针对数据表结构而产生的类,我们称为实体
(Model)类库。
1.创建实体类库Model 
右击解决方案,单击“添加”→“新建项目”,创建一个名为“Model”的类库项目。添加项

第3 章 系统架构 81 
目之后的解决方案列表如图3-9所示。
图3-9 添加Model之后的解决方案资源管理器
2.编写实体类库中的类代码
在图3-9中,删除Model项目中的Class1.cs类文件,新建两个分别名为“LinkmanType.cs” 
和“Linkmen.cs”的类文件,各自代码如下。
LinkmanType.cs类代码如下。 
namespace Model 
{ 
public class LinkmanType 
{ 
public string typeId; 
public string typeName; 
} 
} 
Linkmen.cs类代码如下。 
namespace Model 
{ 
public class Linkmen 
{ 
public string lkmId; 
public string lkmName; 
public string lkmMPNum; 
public string lkmOPNum; 
public string lkmEmail; 
public string lkmCompName; 
public string typeId; 
} 
} 
3.修改数据访问层代码
首先,与引用DBUtility项目相同的方法,将Model项目引入到DAL项目中。
修改LinkmanType.cs类文件如下。

82 基于C# 的管理信息系统开发(第3 版) 
namespace DAL //需要引入System.Data 和System.Data.SqlClient 命名空间
{ 
public class LinkmanType 
{ 
public bool insert(Model.LinkmanType mLKMT) 
{ 
string sql= "insert into linkmantype(typeid,typename)values 
(@typeid,@typename)"; 
List<SqlParameter > sqlParams=new List<SqlParameter>(); 
sqlParams.Add(new SqlParameter("@typeid", mLKMT.typeId)); 
sqlParams.Add(new SqlParameter("@typename", mLKMT.typeName)); 
return DBUtility.DbHelperSQL.ExecuteSql(sql, sqlParams); 
} 
public bool update(Model.LinkmanType mLKMT,string oldTypeId) 
{ 
string sql= "update linkmantype set typeid=@typeid, 
typename=@typename where typeid=@oldTypeId"; 
List<SqlParameter > sqlParams=new List<SqlParameter>(); 
sqlParams.Add(new SqlParameter("@typeid", mLKMT.typeId)); 
sqlParams.Add(new SqlParameter("@typename", mLKMT.typeName)); 
sqlParams.Add(new SqlParameter("@oldTypeId", oldTypeId)); 
return DBUtility.DbHelperSQL.ExecuteSql(sql, sqlParams); 
} 
public bool delete(string typeId) 
{ 
string sql= "delete from linkmantype where typeid=@typeid"; 
List<SqlParameter> sqlParams=new List<SqlParameter>(); 
sqlParams.Add(new SqlParameter("typeid",typeId)); 
return DBUtility.DbHelperSQL.ExecuteSql(sql, sqlParams); 
} 
public DataTable select(string strWhere) 
{ 
string sql = "select * from linkmantype"; 
if (strWhere != "") 
{ 
sql += " where " + strWhere; 
} 
return DBUtility.DbHelperSQL.Query(sql); 
} 
} 
}
修改Linkmen类文件如下。 
namespace DAL 
{ 
public class Linkmen 
{ 
public bool insert(Model.Linkmen mLKM) 
{ 
string sql = "insert into linkmen(lkmname,lkmMPNum,lkmOPNum, 
lkmEmail, lkmCompName,typeId)values(@lkmname,@lkmMPNum,@lkmOPNum,

第3 章 系统架构 83 
@lkmEmail,@lkmCompName,@typeId)"; 
List<SqlParameter> sqlParams = new List<SqlParameter>(); 
sqlParams.Add(new SqlParameter("@lkmname", mLKM.lkmName)); 
sqlParams.Add(new SqlParameter("@lkmMPNum", mLKM.lkmMPNum)); 
sqlParams.Add(new SqlParameter("@lkmOPNum", mLKM.lkmOPNum)); 
sqlParams.Add(new SqlParameter("@lkmEmail", mLKM.lkmEmail)); 
sqlParams.Add(new SqlParameter("@lkmCompName", mLKM.lkmCompName)); 
sqlParams.Add(new SqlParameter("@typeId", mLKM.typeId)); 
return DBUtility.DbHelperSQL.ExecuteSql(sql, sqlParams); 
} 
public bool update(Model.Linkmen mLKM) 
{ 
string sql = "update linkmen set lkmname=@lkmname, 
lkmMPNum=@lkmMPNum,lkmOPNum=@lkmOPNum,lkmEmail=@lkmEmail, 
lkmCompName=@lkmCompName,typeId=@typeId where lkmid=@lkmid"; 
List<SqlParameter> sqlParams = new List<SqlParameter>(); 
sqlParams.Add(new SqlParameter("@lkmname", mLKM.lkmName)); 
sqlParams.Add(new SqlParameter("@lkmMPNum", mLKM.lkmMPNum)); 
sqlParams.Add(new SqlParameter("@lkmOPNum", mLKM.lkmOPNum)); 
sqlParams.Add(new SqlParameter("@lkmEmail", mLKM.lkmEmail)); 
sqlParams.Add(new SqlParameter("@lkmCompName", mLKM.lkmCompName)); 
sqlParams.Add(new SqlParameter("@typeId", mLKM.typeId)); 
sqlParams.Add(new SqlParameter("@lkmid", mLKM.lkmId)); 
return DBUtility.DbHelperSQL.ExecuteSql(sql, sqlParams); 
} 
public bool delete(string lkmId) 
{ 
string sql= "delete from linkmen where lkmid=@lkmid"; 
List<SqlParameter> sqlParams = new List<SqlParameter>(); 
sqlParams.Add(new SqlParameter("@lkmid", lkmId)); 
return DBUtility.DbHelperSQL.ExecuteSql(sql, sqlParams); 
} 
public DataTable select(string strWhere) 
{ 
string sql = "select linkmen.typeid as typeid,typename,lkmname, 
lkmid,lkmMPNum, lkmOPNum,lkmEmail,lkmCompName from linkmen 
left join linkmantype on linkmen.typeid=linkmantype.typeid"; 
if (strWhere != "") 
{ 
sql += " where " + strWhere; 
} 
return DBUtility.DbHelperSQL.Query(sql); 
} 
} 
}
上述LinkmanType类和Linkmen类,明显较上一版本要精简得多,函数实现得到精
简,函数参数也得到了精简。此代码为本书简单三层架构终态的数据访问层代码结构。其
中,DAL调用了DBUtility项目的代码,但DBUtility类库不能称为一层,它的存在只是提
供一套通用的访问数据库的方法。而Model类库更不能称为一层,在后面章节中可以看

84 基于C# 的管理信息系统开发(第3 版) 
到,Model实际上是贯穿了数据访问层、业务逻辑层和表示层这三层的,因为三层均要调用
图3-10 添加BLL之后的解决方案
资源管理器
Model中的类。
3.2.4 业务逻辑层
1.创建业务逻辑层项目BLL 
右击解决方案,单击“添加”→“新建项目”,创建一
个名为“BLL”的类库项目。添加项目之后的解决方案
列表如图3-10所示。
2.编写业务逻辑层代码
先引用DAL 和Model项目,删除BLL 项目中的
Class1.cs文件,然后新建名为“LinkmanType.cs”的类文
件,以实现联系人类型管理的业务逻辑。联系人类型管
理的业务逻辑非常简单,只有基本的对联系人类型的增
加、删除、修改、查询功能。因此,BLL的LinkmanType类
中的代码也只是简单地调用DAL中的LinkmanType类
来实现这些功能。
LinkmanType.cs类文件代码如下。 
namespace BLL 
{ 
public class LinkmanType 
{ 
DAL.LinkmanType dLKMT = new DAL.LinkmanType(); 
public bool insert(Model.LinkmanType mLKMT) 
{ 
return dLKMT.insert(mLKMT); 
} 
public bool update(Model.LinkmanType mLKMT, string oldTypeId) 
{ 
return dLKMT.update(mLKMT, oldTypeId); 
} 
public bool delete(string typeId) 
{ 
return dLKMT.delete(typeId); ; 
} 
public DataTable select(string strWhere) 
{ 
return dLKMT.select(strWhere); 
} 
} 
}
现在来实现联系人管理的业务逻辑层代码,与联系人类型管理的业务逻辑相比,在实现
联系人增加、删除、修改、查询功能之外,还需实现随机抽取一名联系人的功能,这就是一种
业务逻辑,需在业务逻辑层实现。
在BLL项目中新建名为“Linkmen.cs”的类文件,添加代码如下。

第3 章 系统架构 85 
namespace BLL 
{ 
public class Linkmen 
{ 
DAL.Linkmen dLKM = new DAL.Linkmen(); 
public bool insert(Model.Linkmen mLKM) 
{ 
return dLKM.insert(mLKM); 
} 
public bool update(Model.Linkmen mLKM) 
{ 
return dLKM.update(mLKM); 
} 
public bool delete(string stuId) 
{ 
return dLKM.delete(stuId); 
} 
public DataTable select(string strWhere) 
{ 
return dLKM.select(strWhere); 
} 
///<summary> 
///随机抽取一名联系人 
///</summary> 
///<returns>返回抽取到的联系人对象</returns> 
public Model.Linkmen randomFriend() 
{ 
DataTable dt=select(""); 
if (dt.Rows.Count > 0) 
{ 
Random random = new Random(); 
int currNum = random.Next(0, dt.Rows.Count); 
Model.Linkmen mLKM = new Model.Linkmen(); 
mLKM.lkmId = dt.Rows[currNum]["lkmid"].ToString(); 
mLKM.lkmName = dt.Rows[currNum]["lkmname"].ToString(); 
mLKM.lkmMPNum = dt.Rows[currNum]["lkmmpnum"].ToString(); 
mLKM.lkmOPNum = dt.Rows[currNum]["lkmopnum"].ToString(); 
mLKM.lkmEmail= dt.Rows[currNum]["lkmemail"].ToString(); 
mLKM.lkmCompName = dt.Rows[currNum]["lkmcompname"].ToString(); 
mLKM.typeId = dt.Rows[currNum]["typeid"].ToString(); 
return mLKM; 
} 
else 
{ 
return null; 
} 
} 
} 
}

86
基于C# 
的管理信息系统开发(第
3 
版)

可见随着业务逻辑的复杂度增加,业务逻辑层代码也会相应增加。

3.2.5 
表示层
1. 
创建表示层项目UI 
1节提到表示层其实就是用户界面层,所以表示
层的项目类型不是DAL 、BLL 这样的类库,而是
Windows窗体应用程序。

右击解决方案,选择“添加”→“新建项目”命令,打
开“添加新项目”对话框,搜索“Windows窗体应用”模
板,选择“Windows窗体应用(.NETFramework)模(”) 板,单击“下一步”按钮,将项目名称改为“UI,(”) 单击“创
建”按钮。此时,解决方案资源管理器中的项目结构如
图3-11 所示。

3.
2. 
编写表示层代码
表示层需要引用BLL 和Model项目,无须引用
DAL 。删除UI 项目中的Foc右击

rm1.s窗体文件后, 
UI 项目,在弹出式菜单中选择“添加”→“窗体
(Windows窗体)命(”) 令,在弹出的“添加新项”窗体中, 
将名称改为MainForm.s,单击“添加” 

c按钮完成窗体的
添加。用同样的方法再添加四个Windows窗体,分别图3-11 
添加UI 
之后的解决方案
命名为LinkmanTypeManae.cs、LinkmanTypeEdit. 资源管理器
cs、nnges以及
gLikmeEdtc

LikmenMaacnni.s。各窗体
的作用如表3-3所示。(.) 

表3-
3 
各窗体作用


窗体名

MainForm 

LinkmanTypeManage 
LinkmanTypeEdit 
LinkmenManage 
LinkmenEdit 

作用

入口主窗体,程序的MDI 父窗体

联系人类型管理窗体,显示联系人类型列表并且是编辑入口

联系人类型编辑窗体

联系人管理窗体,显示联系人列表并且是编辑入口

联系人编辑窗体

将UI 项目中的Progam.s文件的Main方法中的Appiain.n(nwFrm1()) 语

rclctoRueo
句改为Appliain.n(nwiFrm()), 指明应用程序运行时首先运行MaiFrm 

ctoRueManono
窗体。接
下来设置MainForm 窗体的若干属性及其值,如表3-4所示。