摘要
使用.Net开发的朋友,对于三层(N层)架构一定都不陌生,相信许多朋友也都曾细细研究过Duwamish/PetShop等经典案例。
采用分层的方式对系统进行设计和架构,也的确可以提升系统的可维护性、扩展性。不过三层架构其实只是系统的一种设计思想,为系统的设计、开发提供了一种新的思路,与其相关的Duwamish/PetShop等案例也应只被当成“案例”或“示例”,而不应作为“标准”或“模板”。
笔者并不认为三层架构有什么不好,但看到了太多的Duwamish/PetShop式的系统,基本是在照抄这些案例,而忽略了自身系统实际是否需要进行这样的设计。所以撰写此文,与大家一起探讨三层架构系统设计的一些技巧。
案例
就以“员工管理”为例吧,我们要求也很简单:系统中的“员工信息”和“部门信息”两个对象,数据库中有“Employee”和“Department”两张表,相关对象的属性定义如下:
员工信息(Employee): 自动编号,员工编号,真实姓名,性别,联系电话,所属部门,入职日期,备注
部门信息(Department): 自动编号,部门名称,经理姓名,年假基准天数,备注
要求:
建立两个功能模块分别实现“员工信息”和“部门信息”的增删改查,其中“员工信息”对象要求:
1. “所属部门”在界面上显示为汉字的“用户组名称”、数据库中存为整型的编号;
2. “性别”在界面上显示为“男/女”、数据库中存为整型的“1/2”;
3. “所属部门”与“性别”在编辑界面使用“下拉框”进行选择;
4. 在查看员工的详细信息时,可以显示员工的当前可享受的年假天数,公式为:年假天数 = 部门年假基准天数+工作年限*1。
实现
我们以传统的三层架构设计思想,先来分析和设计这个案例。首先,我们可以先建立一个Visual Studio解决方案,可以是一个多项目的解决方案,也可以是一个单项目的解决方案,如下图:
系统如何分层、层的数量,应根据系统的实际需求和应用场景来决定,而不应当生搬硬套。本案例中我们建立了5个层,之后我们可以针对相关的功能要求,设计出“员工信息”和“部门信息”两个类,并定义数据库中的相关表结构,如下图:
然后,我们可以为所设计的对象,编写数据实体层、业务逻辑层、数据访问层的代码 。其中数据访问层至少就包含:Insert、Update、Delete、GetDataById、GetAllList、GetPageList等方法,用于实现两个对象的增删改查和分页功能。为了实现“员工信息”在界面上显示部门名称和性别,而不是数据表里所存储的数字,我们可以将数据访问层的相关方法进行改写,通过级联查询和条件判断,根据编号获取相应的名称,并为Employee.SexName和Employee.DepartName两个string类型属性赋值。当然也可以通过在“员工信息”类数据访问层中,直接调用“部门信息”类数据访问层中的GetDataById方法来获取,而不使用级联查询。
业务逻辑层也应编写相关方法供用户界面层调用,有的朋友可能会觉得这个业务逻辑层似乎是多余的,其实不然。这是因为我们案例中的业务很简单,目前仅需增加一个CheckValid方法,用于对界面层传入的对象进行有效性检查。为了实现“在界面显示员工的当前可享受的年假天数”这一功能,我们可以再在业务逻辑层中增加一个GetVacationDays方法,用于根据员工的入职日期、所在部门、部门年假基准天数来进行计算。
最后,开始设计系统的界面,并将相关界面文件放到用户界面层中去。在员工信息列表界面中,我们可以指定数据控件中的“性别”、“所属部门”2列,分别与员工对象中的“性别显示名称”、“部门显示名称”进行绑定。在员工信息编辑界面中,指定“性别”下拉框的可选项、指定“所属部门”下拉框的DataSource=DepartmentBLL.GetAllList(),如下图:
为了实现“可以查看员工的当前可享受的年假天数”这一功能,我们可以在“查看详细”页面中,通增加一个Lable控件,并在后台指定其Text属性为:EmployeeBLL.GetVacationDays().ToString()来实现。至此,我们已经完成了一个三层框架系统的设计,并基本实现了相关的功能要求。
改进
在我们完成了系统的设计工作后,请考虑针对“员工信息类”的以下功能变更:
1.增加一个“所学专业”属性,所学专业需要存储于数据库并可以维护;
2.增加一个“政治面貌”属性,其值固定为团员、党员、群众、九三学社;
3.修改年假天数的算法,实现“性别”为女的员工,在现有算法基础上自动加1天。
稍加分析我们可以发现:员工信息类会随着关联对象的增多,而增加越来越多仅用于显示的冗余属性;“性别”、“政治面貌”这类存储于数据库显得浪费,直接在代码中进行硬编码又存在难以维护的问题,如果这类属性牵涉到业务方法,由于不能“自解释”本身的含义,业务代码也会别的晦涩难懂,虽然使用“枚举”类型可以缓解该问题,但是又存在与界面控件绑定不方便等问题。另外,还存在得到一个员工对象实例,仍需根据编号再次进行数据访问,才能获得关联的“所属部门”、“所学专业”等关联对象的完整信息,如果业务复杂的话,会增加大量的数据访问交互。
所以我们可以做出如下调整:删除“员工信息类”中所有仅用于显示的属性,如“性别显示名称”、“部门显示名称”,增加一个添的“Sex性别”类到数据实体层,修改员工类中“性别”和“所属部门”2个属性的类型,由string分别改为Sex和Department,也就是直接在员工类中引用“性别”和“部门”类作为其属性。并增加一个只有get方法的属性VacationDays,用于直接计算并获取员工实例的年假天数,使外界调用更为直观。调整后的“员工信息类”的属性定义,如下图:
其中Sex类中的代码如下:
public class Sex { #region EasyCode所生成的默认代码 //﹉﹉﹉﹉﹉﹉﹉﹉﹉﹉﹉﹉﹉﹉﹉﹉﹉﹉﹉﹉﹉﹉﹉﹉﹉﹉﹉﹉﹉﹉﹉﹉﹉﹉﹉﹉﹉﹉﹉﹉﹉﹉﹉﹉﹉﹉﹉﹉﹉﹉ // 此区域的代码为EasyCode所自动生成,主要用于定义该类的变量属性。请不要直接修改该区域中的任何代码, // 或在该区域中添加任何自定义代码,当该类发生变更时,您可以随时使用EasyCode重新生成覆盖其中的代码。 //﹍﹍﹍﹍﹍﹍﹍﹍﹍﹍﹍﹍﹍﹍﹍﹍﹍﹍﹍﹍﹍﹍﹍﹍﹍﹍﹍﹍﹍﹍﹍﹍﹍﹍﹍﹍﹍﹍﹍﹍﹍﹍﹍﹍﹍﹍﹍﹍﹍﹍ ///[变量]编号 private int? id; ///[变量]名称 private string name; ///[属性]编号 public int? Id { get { return id; } } ///[属性]名称 public string Name { get { return name; } } ///实例化对象方法。私有仅供内部访问 private Sex() { } public static readonly Sex Empty = new Sex(); public static readonly Sex Male = new Sex() { id = 1, name = "男" }; public static readonly Sex Female = new Sex() { id = 2, name = "女" }; ///所有“性别”对象列表。 public static readonly ListAllList = new List { Sex.Male, Sex.Female }; /// 根据“性别”编号,返回一个“性别”对象。 public static Sex GetDataById(int id) { foreach (Sex tmpSex in AllList) { if (tmpSex.Id == id) return tmpSex; } return null; } ///已重写的ToString方法,当该类作为其它类的一个属性时,在数据控件中可以直接绑定并显示其有意义的名称。 public override string ToString() { return Name; } #endregion EasyCode所生成的默认代码 }
其中员工信息类中VacationDays属性的代码如下:
////// [属性]员工可休年假天数 /// public int VactionDays { get { if (Sex == null || Department == null) // 如果性别或部门为空 return 0; if (this.Sex == Sex.Male) // 如果为男性 { // 部门年假基准天数+工作年限*1 return Department.BaseDays.Value + (DateTime.Now.Year - DateOfEntry.Value.Year); } else //如果为女性 { // 部门年假基准天数+工作年限*1 + 1 return Department.BaseDays.Value + (DateTime.Now.Year - DateOfEntry.Value.Year) + 1; } } }
我们再对“员工信息类”的数据访问层代码进行相关改写,如:
#region EasyCode所生成的默认代码 //﹉﹉﹉﹉﹉﹉﹉﹉﹉﹉﹉﹉﹉﹉﹉﹉﹉﹉﹉﹉﹉﹉﹉﹉﹉﹉﹉﹉﹉﹉﹉﹉﹉﹉﹉﹉﹉﹉﹉﹉﹉﹉﹉﹉﹉﹉﹉﹉﹉﹉ // 此区域的代码为EasyCode所自动生成,实现了父类中定义的抽象方法。请不要直接修改该区域中的任何代码, // 或在该区域中添加任何自定义代码,当该类发生变更时,您可以随时使用EasyCode重新生成覆盖其中的代码。 //﹍﹍﹍﹍﹍﹍﹍﹍﹍﹍﹍﹍﹍﹍﹍﹍﹍﹍﹍﹍﹍﹍﹍﹍﹍﹍﹍﹍﹍﹍﹍﹍﹍﹍﹍﹍﹍﹍﹍﹍﹍﹍﹍﹍﹍﹍﹍﹍﹍﹍ ////// 将员工信息(Employee)数据,采用INSERT操作插入到数据库中,并返回受影响的行数。 /// /// 员工信息(Employee)实例对象 public override int Insert(Employee employee) { string sqlText = "INSERT INTO [Employee]" + "([EmployeeNO],[RealName],[Sex],[ContactPhone],[Department],[DateOfEntry],[Notes])" + "VALUES" + "(@EmployeeNO,@RealName,@Sex,@ContactPhone,@Department,@DateOfEntry,@Notes)"; SqlParameter[] parameters = { new SqlParameter("@EmployeeNO" , SqlDbType.NVarChar , 50 ){ Value = employee.EmployeeNO }, new SqlParameter("@RealName" , SqlDbType.NVarChar , 50 ){ Value = employee.RealName }, new SqlParameter("@Sex" , SqlDbType.Int , 4 ){ Value = employee.Sex.Id }, new SqlParameter("@ContactPhone" , SqlDbType.NVarChar , 50 ){ Value = employee.ContactPhone }, new SqlParameter("@Department" , SqlDbType.Int , 4 ){ Value = employee.Department.Id }, new SqlParameter("@DateOfEntry" , SqlDbType.DateTime , 8 ){ Value = employee.DateOfEntry }, new SqlParameter("@Notes" , SqlDbType.NVarChar , 100){ Value = employee.Notes } }; return SFL.SqlHelper.ExecuteNonQuery(sqlText, parameters); }
然后,我们再对“员工信息类”的业务逻辑层,有效性检查代码进行相关改写,如:
#region EasyCode所生成的默认代码 //﹉﹉﹉﹉﹉﹉﹉﹉﹉﹉﹉﹉﹉﹉﹉﹉﹉﹉﹉﹉﹉﹉﹉﹉﹉﹉﹉﹉﹉﹉﹉﹉﹉﹉﹉﹉﹉﹉﹉﹉﹉﹉﹉﹉﹉﹉﹉﹉﹉﹉ // 此区域的代码为EasyCode所自动生成,主要提供该类的基本业务逻辑。请不要直接修改该区域中的任何代码, // 或在该区域中添加任何自定义代码,当该类发生变更时,您可以随时使用EasyCode重新生成覆盖其中的代码。 //﹍﹍﹍﹍﹍﹍﹍﹍﹍﹍﹍﹍﹍﹍﹍﹍﹍﹍﹍﹍﹍﹍﹍﹍﹍﹍﹍﹍﹍﹍﹍﹍﹍﹍﹍﹍﹍﹍﹍﹍﹍﹍﹍﹍﹍﹍﹍﹍﹍﹍ ////// 返回与本类相关联的数据访问类。通常本类需要访问自身关联的数据访问类,与数据库进行交互时,应优先使用该属性, /// 本类调用业务逻辑层其它业务逻辑类时,应当优先使用其它类中公开的方法,而不优先使用其它类中的DataAccess属性。 /// internal static DAL.Common.EmployeeDAL DataAccess { get { return DAL.Common.EmployeeDAL.Instance; } } ////// 对员工信息(Employee)实例对象,进行数据有效性检查。 /// /// 员工信息(Employee)实例对象 public static void CheckValid(Employee employee) { #region 检查各属性是否符合空值约束 if (DataValid.IsNull(employee.EmployeeNO)) throw new CustomException("“员工编号”不能为空,请您确认输入是否正确。"); if (DataValid.IsNull(employee.RealName)) throw new CustomException("“真实姓名”不能为空,请您确认输入是否正确。"); if (DataValid.IsNull(employee.Sex)) throw new CustomException("“性别”不能为空,请您确认输入是否正确。"); if (DataValid.IsNull(employee.Department)) throw new CustomException("“所属部门”不能为空,请您确认输入是否正确。"); if (DataValid.IsNull(employee.DateOfEntry)) throw new CustomException("“入职日期”不能为空,请您确认输入是否正确。");
最后,我们可以调整相关界面的数据绑定方法,因为我们重写了Sex和Department类的ToString()方法,所以可以直接将其绑定给界面显示控件。
通过如上改进,我们可以发现:系统的设计变的更加符合面向对象的特性;系统中的代码不但增加了可读性,而且数量也减少了许多;更关键的是系统可扩展性、可维护性也都得到了加强。关于三层架构的其它设计技巧,我也将陆续撰写一些系列文章与大家一起分享,感兴趣的朋友可以关注我的博客。
撰写本文时,笔者使用了自己所开发EasyCode .Net代码生成器,对案例进行了设计和生成。如果您想了解EasyCode的详细信息,请您单击下面的链接:
如果您需要本案例的源码,请从下面的链接下载: