7. [SimuPaie] 应用程序—— 版本 3——基于 NHibernate 的三层架构
推荐阅读:《C# 2008,第4章:三层架构、NUnit测试、Spring框架》。
7.1. 应用程序总体架构
[SimuPaie] 应用程序现在将采用以下三层结构:
![]() |
- [1-dao] 层(dao = 数据访问对象)将负责数据访问。
- [2-business] 层将处理应用程序的业务逻辑,具体而言是薪资计算。
- [3-ui]层(ui = 用户界面)将负责向用户展示数据以及执行用户请求。我们将执行此功能的模块集合称为[应用程序]。它充当用户界面。
- 这三个层将通过使用 .NET 接口实现独立
- 不同层之间的集成将由 Spring IoC 负责
客户端请求的处理遵循以下步骤:
- 客户端向应用程序发起请求。
- 应用程序处理此请求。为此,它可能需要[业务]层的协助;如果需要与数据库交换数据,[业务]层本身可能又需要[DAO]层的协助。
- 应用程序从[业务]层接收响应。基于该响应,它向客户端发送相应的视图(即响应)。
让我们以计算保育员薪资为例。这需要几个步骤:
![]() |
- [UI] 层需要向用户询问
- 待计算薪资人员的身份
- 该人员的工作天数
- 该人员的工作小时数
- 为此,它需要向用户展示来自 [EMPLOYEES] 表的人员列表(姓、名、社会安全号),以便用户从中选择一人。 [ui]层将使用路径[2, 3, 4, 5, 6, 7]来检索这些信息。操作[2]是请求员工列表;操作[7]是对此请求的响应。完成此操作后,[ui]层可通过[8]向用户展示员工列表。
- 用户将向 [ui] 层发送工作天数和工作小时数。这就是上文中的操作 [1]。在此步骤中,用户仅与 [ui] 层进行交互。该层将负责验证输入数据的有效性。完成此步骤后,用户将请求进行工资计算。
- [ui]层将请求业务层执行此计算。为此,它会将从用户处接收的数据发送给业务层。这就是操作[2]。
- [业务]层需要某些信息来完成其任务:
- 关于该人员的更完整信息(地址、索引号等)
- 与其薪级相关的津贴
- 从毛薪中扣除的各项社会保险缴费率
它将通过路径 [3, 4, 5, 6] 向 [DAO] 层请求这些信息。[3] 是初始请求,[6] 是对此请求的响应。
- 在获得所需的所有数据后,[业务]层计算用户所选人员的薪酬。
- [业务]层现在可以响应[ui]层在(d)中发出的请求。这是路径[7]。
- [ui]层将对这些结果进行格式化,以便以适当的形式呈现给用户,然后进行显示。这就是路径[8]。
- 可以设想,这些结果需要存储在文件或数据库中。这可以自动完成。 在此情况下,操作 (f) 完成后,[业务] 层将请求 [DAO] 层保存结果。这将走路径 [3, 4, 5, 6]。此操作也可应用户请求执行。路径 [1-8] 将被请求-响应循环所使用。
如上所述,每一层仅使用其右侧层的资源,绝不使用其左侧层的资源。
我们对这种三层架构的首次实现将是一个 ASP.NET 应用程序,其中
- [DAO] 和 [business] 层将通过 DLL 实现
- ,而 [ui] 层将由第 1 版中的 Web 表单实现(参见第 4.2.1 节)。
我们将首先使用 NHibernate 框架实现 [DAO] 层。
7.2. [DAO] 数据访问层
![]() |
7.2.1. [DAO]层的Visual Studio C#项目
[DAO]层的Visual Studio项目如下:
![]() |
- 在 [1] 中,整个项目
- 在 [2] 中,项目的各个类
- 在 [3] 中,项目的引用。
- 在 [4] 中,有一个名为 [lib] 的文件夹,其中包含后续各个项目所需的 DLL 文件
在项目引用 [3] 中,包含以下 DLL:
- NHibernate:用于 NHibernate ORM
- MySql.Data:用于 MySQL 数据库管理系统(DBMS)的 ADO.NET 驱动程序
- Spring.Core:用于 Spring 框架
- log4net:日志记录库
- nunit.framework:一个单元测试库
这些引用取自 [lib] 文件夹 [4]。请确保所有这些引用的“本地副本”属性均设置为“True” [5]:
![]() |
7.2.2. [dao] 层中的实体
![]() |
[dao] 层所需的实体(对象)已收集在项目的 [entities] 文件夹中。 其中一些我们已经很熟悉:第 6.3.2.1 节中描述的 [Contributions]、第 6.3.2.3 节中描述的 [Employee],以及第 6.3.2.2 节中描述的 [Allowances]。它们都位于 [Pam.Dao.Entities] 命名空间中。
[Employee] 类的定义如下:
namespace Pam.Dao.Entites {
public class Employe {
// automatic properties
public virtual int Id { get; set; }
public virtual int Version { get; set; }
public virtual string SS { get; set; }
public virtual string Nom { get; set; }
public virtual string Prenom { get; set; }
public virtual string Adresse { get; set; }
public virtual string Ville { get; set; }
public virtual string CodePostal { get; set; }
public virtual Indemnites Indemnites { get; set; }
// manufacturers
public Employe() {
}
// ToString
public override string ToString() {
return string.Format("[{0},{1},{2},{3},{4},{5},{6}]", SS, Nom, Prenom, Adresse, Ville, CodePostal, Indemnites);
}
}
}
7.2.3. [PamException] 类
[dao] 层负责与外部数据源进行数据交换。这种交换可能会失败。例如,如果向互联网上的远程服务请求信息,可能会因网络中断而导致检索失败。对于此类错误,Java 中的标准做法是抛出异常。如果异常不是 [RunTimeException] 类型或其派生类型,则必须在方法签名中注明该方法会抛出异常。 在 .NET 中,所有异常均为未处理异常,即等同于 Java 的 [RunTimeException] 类型。因此,无需声明 [GetAllEmployeeIDs、GetEmployee、GetContributions] 方法可能会抛出异常。
然而,能够区分不同的异常是有用的,因为它们的处理方式可能不同。因此,处理各种类型异常的代码可以编写如下:
try{
... code pouvant générer divers types d'exceptions
}catch (Exception1 ex1){
...on gère un type d'exceptions
}catch (Exception2 ex2){
...on gère un autre type d'exceptions
}finally{
...
}
因此,我们为应用程序的 [DAO] 层创建了一个异常类型。该类型如下所示:[PamException]:
using System;
namespace Pam.Dao.Entites {
public class PamException : Exception {
// the error code
public int Code { get; set; }
// manufacturers
public PamException() {
}
public PamException(int Code)
: base() {
this.Code = Code;
}
public PamException(string message, int Code)
: base(message) {
this.Code = Code;
}
public PamException(string message, Exception ex, int Code)
: base(message, ex) {
this.Code = Code;
}
}
}
- 第 2 行:该类属于 [Pam.Dao.Entities] 命名空间
- 第 4 行:该类继承自 [Exception] 类
- 第 7 行:它有一个公共属性 [Code],该属性是一个错误代码
- 在我们的 [dao] 层中,我们将使用两种类型的构造函数:
- 第 18–21 行中的构造函数,其用法如下所示:
- (待续)
- 或者第 23–26 行中的代码,其设计目的是通过将已发生的异常包装在 [PamException] 异常中来传播该异常:
try{
....
}catch (IOException ex){
// on encapsule l'exception
throw new PamException("Problème d'accès aux données",ex,10);
}
这种第二种方法的优势在于不会丢失第一个异常中包含的信息。
7.2.4. NHibernate 映射文件 <--> 类
让我们回到应用程序架构:
![]() |
在读取时,NHibernate 框架从数据库中检索数据并将其转换为对象,这些对象对应的就是我们刚才介绍的类。在写入时,它则反其道而行之:以对象为起点,在数据库表中创建、更新和删除行。负责表 <--> 类转换的文件已经介绍过了:
![]() |
- 第 6.3.2.1 节中介绍的 [Cotisations.hbm.xml] 文件将 [COTISATIONS] 表映射到了 [Cotisations] 类
<?xml version="1.0" encoding="utf-8" ?>
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2"
namespace="Pam.Dao.Entites" assembly="pam-dao-nhibernate">
<class name="Cotisations" table="COTISATIONS">
<id name="Id" column="ID">
<generator class="native" />
</id>
<version name="Version" column="VERSION"/>
<property name="CsgRds" column="CSGRDS" not-null="true"/>
<property name="Csgd" column="CSGD" not-null="true"/>
<property name="Retraite" column="RETRAITE" not-null="true"/>
<property name="Secu" column="SECU" not-null="true"/>
</class>
</hibernate-mapping>
- 第 6.3.2.3 节中展示的 [Employe.hbm.xml] 文件将 [EMPLOYEES] 表映射到了 [Employee] 类
<?xml version="1.0" encoding="utf-8" ?>
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2"
namespace="Pam.Dao.Entites" assembly="pam-dao-nhibernate">
<class name="Employe" table="EMPLOYES">
<id name="Id" column="ID">
<generator class="native" />
</id>
<version name="Version" column="VERSION"/>
<property name="SS" column="SS" length="15" not-null="true" unique="true"/>
<property name="Nom" column="NOM" length="30" not-null="true"/>
<property name="Prenom" column="PRENOM" length="20" not-null="true"/>
<property name="Adresse" column="ADRESSE" length="50" not-null="true" />
<property name="Ville" column="VILLE" length="30" not-null="true"/>
<property name="CodePostal" column="CP" length="5" not-null="true"/>
<many-to-one name="Indemnites" column="INDEMNITE_ID" cascade="save-update" lazy="false"/>
</class>
</hibernate-mapping>
- 第 6.3.2.2 节中展示的 [Indemnites.hbm.xml] 文件将 [INDEMNITES] 表映射到了 [Indemnites] 类
<?xml version="1.0" encoding="utf-8" ?>
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2"
namespace="Pam.Dao.Entites" assembly="pam-dao-nhibernate">
<class name="Indemnites" table="INDEMNITES">
<id name="Id" column="ID">
<generator class="native" />
</id>
<version name="Version" column="VERSION"/>
<property name="Indice" column="INDICE" not-null="true" unique="true"/>
<property name="BaseHeure" column="BASE_HEURE" not-null="true"/>
<property name="EntretienJour" column="ENTRETIEN_JOUR" not-null="true"/>
<property name="RepasJour" column="REPAS_JOUR" not-null="true" />
<property name="IndemnitesCp" column="INDEMNITES_CP" not-null="true"/>
</class>
</hibernate-mapping>
请注意,在这些文件的 <hibernate-mapping> 标签中(第 2 行),存在以下属性:
- 命名空间:Pam.Dao.Entities。[Contributions]、[Employee] 和 [Allowances] 类必须位于此命名空间中。
- assembly:pam-dao-nhibernate。映射文件 [*.hbm.xml] 必须封装在名为 [pam-dao-nhibernate] 的 DLL 中。为此,需按以下方式配置 C# 项目:
![]() |
- 在 [1] 中,项目程序集命名为 [pam-dao-nhibernate]
- 在 [2] 中,将映射文件 [*.hbm.xml] 包含 [3] 到项目程序集
7.2.5. [DAO] 层的 [ IPamDao] 接口
让我们回到应用程序的架构:
![]() |
在简单的情况下,我们可以从 [业务] 层开始探索应用程序的接口。要正常运行,它需要数据:
- 这些数据可能已存在于文件、数据库中,或通过网络获取。它们由[DAO]层提供。
- 尚未可用。此时由[ui]层提供,该层从应用程序用户处获取数据。
[DAO]层应向[业务]层提供哪些接口?这两个层之间可能进行哪些交互?[DAO]层必须向[业务]层提供以下数据:
- 托儿服务提供商列表,以便用户选择特定的一家
- 所选人员的完整信息(地址、索引号等)
- 与该人员索引号关联的补贴
- 各类社会保险缴费的费率
这些信息在薪资计算前已知,因此可以存储。在 [业务] -> [DAO] 方向上,[业务] 层可以要求 [DAO] 层记录薪资计算的结果。我们在此不进行此操作。
基于这些信息,我们可以尝试对 [DAO] 层接口进行初步定义:
using Pam.Dao.Entites;
namespace Pam.Dao.Service {
public interface IPamDao {
// list of all employee identities
Employe[] GetAllIdentitesEmployes();
// an individual employee with benefits
Employe GetEmploye(string ss);
// list of all contributions
Cotisations GetCotisations();
}
}
- 第 1 行:我们导入了 [dao] 层中实体的命名空间。
- 第 3 行:[dao] 层位于 [Pam.Dao.Service] 命名空间中。[Pam.Dao.Entities] 命名空间中的元素可以创建多个实例,而 [Pam.Dao.Service] 命名空间中的元素则作为单例(singleton)创建。这正是命名空间命名选择的依据。
- 第 4 行:接口命名为 [IPamDao]。它定义了三个方法:
- 第 6 行:[GetAllIdentitesEmployes] 返回一个 [Employe] 类型的对象数组,以简化格式(姓、名、社保号)表示托儿服务提供者的列表。
- 第 8 行:[GetEmploye] 返回一个 [Employe] 对象:该对象对应的是作为方法参数传入的社会保险号所属的员工,并包含与其薪级相关的福利。
- 第 10 行,[GetCotisations] 返回 [Cotisations] 对象,该对象封装了从毛薪中扣除的各项社会保障缴费率。
7.3. [dao] 层的实现与测试
7.3.1. Visual Studio 项目
Visual Studio 项目已在前文介绍过。在此重申:
![]() |
- 在 [1] 中,整个项目
- 在 [2] 中,展示了该项目的各个类。 [entities] 文件夹包含由 [dao] 层处理的实体以及 NHibernate 映射文件。[service] 文件夹包含 [IPamDao] 接口及其实现类 [PamDaoNHibernate]。[tests] 文件夹包含一个控制台测试 [Main.cs] 和一个单元测试 [NUnit.cs]。
- 在 [3] 中,是项目的引用。
7.3.2. 控制台测试程序 [Main.cs]
测试程序 [Main.cs] 在以下架构中运行:
![]() |
该程序负责测试 [IPamDao] 接口的方法。一个基本示例如下:
using System;
using Pam.Dao.Entites;
using Pam.Dao.Service;
using Spring.Context.Support;
namespace Pam.Dao.Tests {
public class MainPamDaoTests {
public static void Main() {
try {
// layer instantiation [dao]
IPamDao pamDao = (IPamDao)ContextRegistry.GetContext().GetObject("pamdao");
// list of employee identities
foreach (Employe Employe in pamDao.GetAllIdentitesEmployes()) {
Console.WriteLine(Employe.ToString());
}
// an employee with benefits
Console.WriteLine("------------------------------------");
Console.WriteLine(pamDao.GetEmploye("254104940426058"));
Console.WriteLine("------------------------------------");
// list of contributions
Cotisations cotisations = pamDao.GetCotisations();
Console.WriteLine(cotisations.ToString());
} catch (Exception ex) {
// exception display
Console.WriteLine(ex.ToString());
}
//break
Console.ReadLine();
}
}
}
- 第 11 行:我们向 Spring 请求 [dao] 层的引用。
- 第 13–15 行:测试 [IPamDao] 接口的 [GetAllEmployeeIDs] 方法
- 第 18 行:测试 [IPamDao] 接口的 [GetEmployee] 方法
- 第 21 行:测试 [IPamDao] 接口的 [GetCotisations] 方法
Spring、NHibernate 和 log4net 通过以下 [ App.config] 文件进行配置:
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<!-- configuration sections -->
<configSections>
<section name="log4net" type="log4net.Config.Log4NetConfigurationSectionHandler,log4net" />
<sectionGroup name="spring">
<section name="objects" type="Spring.Context.Support.DefaultSectionHandler, Spring.Core" />
<section name="context" type="Spring.Context.Support.ContextHandler, Spring.Core" />
</sectionGroup>
<section name="hibernate-configuration" type="NHibernate.Cfg.ConfigurationSectionHandler, NHibernate" />
</configSections>
<!-- spring configuration -->
<spring>
<context>
<resource uri="config://spring/objects" />
</context>
<objects xmlns="http://www.springframework.net">
<object id="pamdao" type="Pam.Dao.Service.PamDaoNHibernate, pam-dao-nhibernate" init-method="init" destroy-method="destroy"/>
</objects>
</spring>
<!-- configuration NHibernate -->
<hibernate-configuration xmlns="urn:nhibernate-configuration-2.2">
<session-factory>
<property name="connection.provider">NHibernate.Connection.DriverConnectionProvider</property>
<property name="connection.driver_class">NHibernate.Driver.MySqlDataDriver</property>
<property name="dialect">NHibernate.Dialect.MySQLDialect</property>
<property name="connection.connection_string">
Server=localhost;Database=dbpam_nhibernate;Uid=root;Pwd=;
</property>
<property name="show_sql">false</property>
<mapping assembly="pam-dao-nhibernate"/>
</session-factory>
</hibernate-configuration>
<!-- This section contains the log4net configuration settings -->
<!-- NOTE IMPORTANTE: logs are not active by default. They must be activated by program
avec l'instruction log4net.Config.XmlConfigurator.Configure();
! -->
<log4net>
...
</log4net>
</configuration>
第 6.3.1 节已对 NHibernate 配置(第 10 行、第 25–36 行)进行了说明。请注意第 34 行,该行指定映射文件位于 [pam-dao-nhibernate] 程序集内。这是该项目的程序集。
Spring 配置定义在第 6–9 行和第 15–22 行。第 20 行定义了控制台程序 [Main.cs] 所使用的 [pamdao] 对象。此处的 <object> 标签具有以下属性:
- type:指定要实例化的类。此处为 [PamDaoNHibernate] 类,该类实现了 [IPamDao] 接口。该类位于项目的 [pam-dao-nhibernate] DLL 中。
- init-method:类实例化后要执行的 [PamDaoNHibernate] 类的方法
- destroy-method:在项目执行结束时Spring容器销毁时需执行的[PamDaoNHibernate]类的方法。
使用第 6.2 节所述的数据库进行执行时,控制台输出如下:
- 第1-2行:2名类型为[Employee]的员工,仅包含[SS, 姓, 名]信息
- 第4行:类型为[Employee]的员工,其社会保险号为[254104940426058]
- 第 5 行:缴费率
7.3.3. [PamDaoNHibernate] 类的定义
![]() |
由 [dao] 层实现的 [IPamDao] 接口如下:
using Pam.Dao.Entites;
namespace Pam.Dao.Service {
public interface IPamDao {
// list of all employee identities
Employe[] GetAllIdentitesEmployes();
// an individual employee with benefits
Employe GetEmploye(string ss);
// list of all contributions
Cotisations GetCotisations();
}
}
问题:编写 [PamDaoNHibernate] 类的代码,使其实现上述 [IPamDao] 接口,并使用之前描述的配置好的 NHibernate 框架。我们还将实现由 Spring 调用的 init 和 destroy 方法。init 方法将创建 SessionFactory,我们从中获取 Session 对象。destroy 方法将关闭该 SessionFactory。我们将使用第 6.5 节中的示例。
约束条件:
我们将假设从 [dao] 层请求的某些数据可以完全装入内存。因此,为了提高性能,[PamDaoNHibernate] 类将以 [GetAllEmployeeIDs] 方法要求的格式 (SS, LAST_NAME, FIRST_NAME) 存储
- [EMPLOYEES] 表,以 [GetAllEmployeeIDs] 方法所需的 (SS, LAST_NAME, FIRST_NAME) 格式存储,作为 [Employee] 对象的数组
- 将 [COTISATIONS] 表以单个 [Cotisations] 类型的对象形式存储
这将在该类的 [init] 方法中完成。[PamDaoNHibernate] 类的骨架可能如下所示:
using System;
...
namespace Pam.Dao.Service {
class PamDaoNHibernate : IPamDao {
// private fields
private Cotisations cotisations;
private Employe[] employes;
private ISessionFactory sessionFactory = null;
// init
public void init() {
try {
// factory initialization
sessionFactory = new Configuration().Configure().BuildSessionFactory();
// retrieve contribution rates and employees for caching
.......................
}
// closure SessionFactory
public void destroy() {
if (sessionFactory != null) {
sessionFactory.Close();
}
}
// list of all employee identities
public Employe[] GetAllIdentitesEmployes() {
return employes;
}
// an individual employee with benefits
public Employe GetEmploye(string ss) {
................................
}
// list of contributions
public Cotisations GetCotisations() {
return cotisations;
}
}
}
7.3.4. 使用 NUnit 进行单元测试
推荐阅读:《C# 2008,第 4 章:三层架构、NUnit 测试、Spring 框架》。
之前的测试是目视检查:我们通过屏幕确认是否得到了预期的结果。这种方法在专业环境中是不够的。测试应尽可能实现自动化,并力求无需人工干预。 人类确实容易疲劳,且其验证测试的能力会随着一天的推移而下降。[NUnit] 工具有助于实现这种自动化。该工具可在以下网址获取:[http://www.nunit.org/]。
[dao] 层的 Visual Studio 项目将按以下方式演进:
![]() |
- 在 [1] 中,测试程序 [NUnit.cs]
- 在 [2,3] 中,项目将生成一个名为 [pam-dao-hibernate.dll] 的 DLL
- 在 [4] 中,对 NUnit 框架 DLL 的引用:[nunit.framework.dll]
- 在 [5] 中,[Main.cs] 类将不会包含在 [pam-dao-hibernate] DLL 中
- 在 [6] 中,[NUnit.cs] 类将被包含在 [pam-dao-hibernate] DLL 中
NUnit 测试类 <a id="pam-dao-nhibernate-nunit"></a> 如下所示:
using System.Collections;
using NUnit.Framework;
using Pam.Dao.Service;
using Pam.Dao.Entites;
using Spring.Objects.Factory.Xml;
using Spring.Core.IO;
using Spring.Context.Support;
namespace Pam.Dao.Tests {
[TestFixture]
public class NunitPamDao : AssertionHelper {
// the [dao] layer to be tested
private IPamDao pamDao = null;
// manufacturer
public NunitPamDao() {
// layer instantiation [dao]
pamDao = (IPamDao)ContextRegistry.GetContext().GetObject("pamdao");
}
// init
[SetUp]
public void Init() {
}
[Test]
public void GetAllIdentitesEmployes() {
// audit no. of employees
Expect(2, EqualTo(pamDao.GetAllIdentitesEmployes().Length));
}
[Test]
public void GetCotisations() {
// checking contribution rates
Cotisations cotisations = pamDao.GetCotisations();
Expect(3.49, EqualTo(cotisations.CsgRds).Within(1E-06));
Expect(6.15, EqualTo(cotisations.Csgd).Within(1E-06));
Expect(9.39, EqualTo(cotisations.Secu).Within(1E-06));
Expect(7.88, EqualTo(cotisations.Retraite).Within(1E-06));
}
[Test]
public void GetEmployeIdemnites() {
// individual verification
Employe employe1 = pamDao.GetEmploye("254104940426058");
Employe employe2 = pamDao.GetEmploye("260124402111742");
Expect("Jouveinal", EqualTo(employe1.Nom));
Expect(2.1, EqualTo(employe1.Indemnites.BaseHeure).Within(1E-06));
Expect("Laverti", EqualTo(employe2.Nom));
Expect(1.93, EqualTo(employe2.Indemnites.BaseHeure).Within(1E-06));
}
[Test]
public void GetEmployeIdemnites2() {
// non-existent individual verification
bool erreur = false;
try {
Employe employe1 = pamDao.GetEmploye("xx");
} catch {
erreur = true;
}
Expect(erreur, True);
}
}
}
- 第 11 行:该类带有 [TestFixture] 属性,这使其成为一个 [NUnit] 测试类。
- 第 12 行:该类继承自 NUnit 框架(从 2.4.6 版本开始)的 AssertionHelper 工具类。
- 第 14 行:私有字段 [pamDao] 是提供对 [dao] 层访问权限的接口的实例。请注意,该字段的类型是接口,而非类。这意味着 [pamDao] 实例仅提供方法的访问权限——具体而言,即 [IPamDao] 接口中的方法。
- 该类中被测试的方法是带有 [Test] 属性的方法。对于所有这些方法,测试过程如下:
- 首先执行带有 [SetUp] 属性的方法。该方法用于准备测试所需的资源(网络连接、数据库连接等)。
- 然后执行待测试的方法
- 最后,执行带有 [TearDown] 属性的方法。该方法通常用于释放由带有 [SetUp] 属性的方法所分配的资源。
- 在我们的测试中,每个测试之前无需分配资源,测试后也无需释放资源。因此,我们不需要带有 [SetUp] 和 [TearDown] 属性的方法。为了示例,我们在第 23–26 行中包含了一个带有 [SetUp] 属性的方法。
- 第 17–20 行:类构造函数使用 Spring 和 [App.config] 初始化私有字段 [pamDao]。
- 第 29–32 行:测试 [GetAllIdentitiesEmployees] 方法
- 第 35–42 行:测试 [GetCotisations] 方法
- 第 45–53 行:测试 [GetEmploye] 方法
- 第 56–65 行:测试 [GetEmploye] 方法在发生异常时的行为。
项目构建会在 [bin/Release] 文件夹中生成 DLL 文件 [pam-dao-nhibernate.dll]。
![]() |
[bin/Release] 文件夹还包含:
- 作为项目引用的一部分且 [Local Copy] 属性设置为 true 的 DLL:[Spring.Core, MySql.data, NHibernate, log4net]。这些 DLL 还附带了它们自身所使用的 DLL 的副本:
- [CastleDynamicProxy, Iesi.Collections] 用于 NHibernate 工具
- [antlr.runtime, Common.Logging](用于 Spring 工具)
- 文件 [pam-dao-nhibernate.dll.config] 是配置文件 [App.config] 的副本。此复制操作由 Visual Studio 自动完成。运行时将使用 [pam-dao-nhibernate.dll.config] 文件,而非 [App.config]。
我们使用 [NUnit-Gui] 工具(版本 2.4.6)加载 DLL [pam-dao-nhibernate.dll],并运行测试:

如上所示,测试已成功通过。
实践练习:
在一台机器上为 [PamDaoNHibernate] 类实现测试。- 使用不同的 [App.config] 配置文件来使用不同的数据库管理系统(Firebird、MySQL、Postgres、SQL Server)
7.3.5. 生成 和[dao]层DLL
在编写并测试完 [PamDaoNHibernate] 类后,将按以下方式生成 [dao] 层 DLL:
![]() |
- [1],测试程序不包含在项目程序集中
- [2,3],项目配置
- [4],项目构建
- 生成的 DLL 位于 [bin/Release] 文件夹中 [5]。我们将它添加到 [lib] 文件夹中已有的 DLL 中 [6]:
![]() |
7.4. 业务层
让我们重新审视 [SimuPaie] 应用程序的总体架构:
![]() |
我们现在假设 [dao] 层已经完成,并封装在 [pam-dao-nhibernate.dll] DLL 中。接下来我们将重点关注 [business] 层。该层实现了业务规则,在本例中即计算薪资的规则。
7.4.1. [ ] 用于 [business] 层的 Visual Studio 项目
业务层的 Visual Studio 项目可能如下所示:
![]() |
- 在 [1] 中,整个项目由 [App.config] 文件进行配置
- 在 [2] 中,[业务] 层由两个文件夹 [entities] 和 [service] 组成。[tests] 文件夹包含一个控制台测试程序 (Main.cs) 和一个 NUnit 测试程序 (NUnit.cs)。
- 在 [3] 中是项目所引用的程序集。请注意来自先前讨论的 [DAO] 层的 [pam-dao-hibernate] DLL。
7.4.2. [business] 层的 [IPamM tier] 接口
让我们回到该应用程序的整体架构:
![]() |
[业务]层应向[UI]层提供哪些接口?这两个层之间可能存在哪些交互?让我们回顾一下将呈现给用户的Web界面:
![]() |
- 表单初次显示时,[1]处应显示员工列表。简化的列表即可(姓、名、社会安全号)。社会安全号是访问所选员工详细信息(信息6至11)的必填项。
- 信息12至15为各项缴费率。
- 信息16至19是与员工指数挂钩的津贴
- 信息20至24由根据用户输入1至3计算得出的薪资组成部分构成。
由[metier]层提供给[ui]层的[IPamMetier]接口必须满足上述要求。可能的接口形式多种多样。我们建议采用以下方案:
using Pam.Dao.Entites;
using Pam.Metier.Entites;
namespace Pam.Metier.Service {
public interface IPamMetier {
// list of all employee identities
Employe[] GetAllIdentitesEmployes();
// ------- salary calculation
FeuilleSalaire GetSalaire(string ss, double heuresTravaillées, int joursTravaillés);
}
}
- 第 7 行:用于填充下拉列表 [1] 的方法
- 第 10 行:用于检索第 6 至 24 项信息的方法。这些信息已收集在一个类型为 [PayrollSheet] 的对象中。
7.4.3. [业务]层中的实体
Visual Studio 项目中的 [entities] 文件夹包含由业务类处理的对象:[Payroll] 和 [PayrollItems]。
![]() |
[FeuilleS al] 类封装了前一个表单中的第 6 至 24 字段:
using Pam.Dao.Entites;
namespace Pam.Metier.Entites {
public class FeuilleSalaire {
// automatic properties
public Employe Employe { get; set; }
public Cotisations Cotisations { get; set; }
public ElementsSalaire ElementsSalaire { get; set; }
// ToString
public override string ToString() {
return string.Format("[{0},{1},{2}", Employe, Cotisations, ElementsSalaire);
}
}
}
- 第 8 行:关于正在计算薪资的员工的第 6 至 11 项信息,以及关于其津贴的第 16 至 19 项信息。请注意,[Employee] 对象封装了一个 [Allowances] 对象,该对象表示该员工的津贴。
- 第 9 行:第 12 至 15 项信息
- 第 10 行:第 20 至 24 项信息
- 第 13–15 行:[ToString] 方法
[Elements Salaire] 类封装了表单中的第 20 至 24 项信息:
namespace Pam.Metier.Entites {
public class ElementsSalaire {
// automatic properties
public double SalaireBase { get; set; }
public double CotisationsSociales { get; set; }
public double IndemnitesEntretien { get; set; }
public double IndemnitesRepas { get; set; }
public double SalaireNet { get; set; }
// ToString
public override string ToString() {
return string.Format("[{0} : {1} : {2} : {3} : {4} ]", SalaireBase, CotisationsSociales, IndemnitesEntretien, IndemnitesRepas, SalaireNet);
}
}
}
- 第4–8行:薪资构成,具体说明参见第3.2节所述的业务规则。
- 第4行:根据工作小时数计算的员工基本工资
- 第5行:从该基本工资中扣除的各项扣款
- 第 6 和 7 行:根据员工职级和实际工作日数,加到基本工资上的津贴
- 第 8 行:应支付的净工资
- 第 12–15 行:该类的 [ToString] 方法。
7.4.4. [业务]层的实现
![]() |
我们将通过两个类来实现 [IPamMetier] 接口:
- [AbstractBasePamMetier],这是一个抽象类,我们将在此类中实现 [IPamMetier] 接口的数据访问功能。该类将引用 [dao] 层。
- [PamMetier],一个从 [AbstractBasePamMetier] 派生的类,将实现 [IPamMetier] 接口的业务规则。它将不涉及 [dao] 层。
[AbstractBasePamMetier] 类的定义如下:
using Pam.Dao.Entites;
using Pam.Dao.Service;
using Pam.Metier.Entites;
namespace Pam.Metier.Service {
public abstract class AbstractBasePamMetier : IPamMetier {
// data access object
public IPamDao PamDao { get; set; }
// list of all employee identities
public Employe[] GetAllIdentitesEmployes() {
return PamDao.GetAllIdentitesEmployes();
}
// an individual employee with benefits
protected Employe GetEmploye(string ss) {
return PamDao.GetEmploye(ss);
}
// contributions
protected Cotisations GetCotisations() {
return PamDao.GetCotisations();
}
// salary calculation
public abstract FeuilleSalaire GetSalaire(string ss, double heuresTravaillées, int joursTravaillés);
}
}
- 第 5 行:该类属于 [Pam.Metier.Service] 命名空间,与 [business] 层中的所有类和接口一样。
- 第 6 行:该类是抽象类(abstract 属性),并实现了 [IPamMetier] 接口
- 第 9 行:该类通过一个公共属性持有对 [dao] 层的引用
- 第 12–14 行:[IPamMetier] 接口中 [GetAllEmployeIDs] 方法的实现——调用 [dao] 层中同名的方法
- 第 17–19 行:内部(受保护)方法 [GetEmployee],该方法调用 [dao] 层中同名的方法——声明为受保护,以便派生类可以访问它,而无需将其设为 public
- 第 22–24 行:内部(受保护)方法 [GetContributions],调用 [dao] 层中的同名方法
- 第 27 行:[IPamMetier] 接口中 [GetSalary] 方法的抽象实现(abstract 属性)。
薪资计算由以下 [PamMetier] 类实现:
using System;
using Pam.Dao.Entites;
using Pam.Metier.Entites;
namespace Pam.Metier.Service {
public class PamMetier : AbstractBasePamMetier {
// wage calculation
public override FeuilleSalaire GetSalaire(string ss, double heuresTravaillées, int joursTravaillés) {
// SS : employee's SS number
// HeuresTravaillées: number of hours worked
// Days worked: number of days worked
// we get the employee back with his benefits
...
// we recover the various contribution rates
...
// salary components are calculated
...
// we return the payslip
return ...;
}
}
}
- 第 7 行:该类继承自 [AbstractBasePamMetier],因此实现了 [IPamMetier] 接口
- 第 10 行:待实现的 [GetSalary] 方法
问题:编写 [GetSalaire] 方法的代码。
7.4.5. [business] 层的控制台测试
让我们回顾一下 [business] 层的 Visual Studio 项目:
![]() |
上方的 [Main] 测试程序用于测试 [IPamMetier] 接口的方法。一个基本示例如下:
using System;
using Pam.Dao.Entites;
using Pam.Metier.Service;
using Spring.Context.Support;
namespace Pam.Metier.Tests {
class MainPamMetierTests {
public static void Main() {
try {
// instantiation layer [metier]
IPamMetier pamMetier = ContextRegistry.GetContext().GetObject("pammetier") as IPamMetier;
// payslip calculations
Console.WriteLine(pamMetier.GetSalaire("260124402111742", 30, 5));
Console.WriteLine(pamMetier.GetSalaire("254104940426058", 150, 20));
try {
Console.WriteLine(pamMetier.GetSalaire("xx", 150, 20));
} catch (PamException ex) {
Console.WriteLine(string.Format("PamException : {0}", ex.Message));
}
} catch (Exception ex) {
Console.WriteLine(string.Format("Exception : {0}", ex.ToString()));
}
// break
Console.ReadLine();
}
}
}
- 第 11 行:Spring 对 [business] 层的实例化。
- 第 13–14 行:测试 [IPamMetier] 接口的 [GetSalary] 方法
- 第 15–22 行:测试 [GetSalaire] 方法,当发生异常时
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<!-- configuration sections -->
<configSections>
<section name="log4net" type="log4net.Config.Log4NetConfigurationSectionHandler,log4net" />
<sectionGroup name="spring">
<section name="objects" type="Spring.Context.Support.DefaultSectionHandler, Spring.Core" />
<section name="context" type="Spring.Context.Support.ContextHandler, Spring.Core" />
</sectionGroup>
<section name="hibernate-configuration" type="NHibernate.Cfg.ConfigurationSectionHandler, NHibernate" />
</configSections>
<!-- spring configuration -->
<spring>
<context>
<resource uri="config://spring/objects" />
</context>
<objects xmlns="http://www.springframework.net">
<object id="pamdao" type="Pam.Dao.Service.PamDaoNHibernate, pam-dao-nhibernate" init-method="init" destroy-method="destroy"/>
<object id="pammetier" type="Pam.Metier.Service.PamMetier, pam-metier-dao-nhibernate" >
<property name="PamDao" ref="pamdao"/>
</object>
</objects>
</spring>
<!-- configuration NHibernate -->
<hibernate-configuration xmlns="urn:nhibernate-configuration-2.2">
....
</hibernate-configuration>
<!-- This section contains the log4net configuration settings -->
<!-- NOTE IMPORTANTE: logs are not active by default. They must be activated by program
avec l'instruction log4net.Config.XmlConfigurator.Configure();
! -->
<log4net>
...
</log4net>
</configuration>
该文件与用于 [dao] 层项目的 [App.config] 文件(参见第 7.3.2 节)完全相同,仅有以下细微差异:
- 第 20 行:ID 为 "pamdao" 的对象类型为 [Pam.Dao.Service.PamDaoNHibernate],位于 [pam-dao-nhibernate] 程序集内。[dao] 层即前文所述的层。
- 第 21–23 行:ID 为 "pammetier" 的对象类型为 [Pam.Metier.Service.PamMetier],位于 [pam-metier-dao-nhibernate] 程序集内。该项目必须按以下方式进行配置:
![]() |
- 第 22 行:由 Spring 实例化的 [PamMetier] 对象具有一个公共属性 [PamDao],该属性指向 [dao] 层。此属性使用第 20 行创建的 [dao] 层引用进行初始化。
使用第 6.2 节中描述的数据库进行执行时,控制台输出如下:
- 第 1-2 行:请求的 2 份工资单
- 第 3 行:因员工不存在而引发的 [PamException] 异常。
7.4.6. 业务层的单元测试
之前的测试是可视化的:我们通过屏幕验证了是否确实得到了预期的结果。现在我们将转向非可视化的 NUnit 测试。
让我们回到 [business] 项目的 Visual Studio 项目中:
![]() |
- 在 [1] 中,NUnit 测试程序
- 在 [2] 中,对 [nunit.framework] DLL 的引用
![]() |
- 在 [3,4] 中,构建项目将生成 DLL [pam-metier-dao-nhibernate.dll]。
- 在 [5] 中,[NUnit.cs] 文件将被包含在 [pam-metier-dao-nhibernate.dll] 程序集内,但 [Main.cs] 不会 [6]
using NUnit.Framework;
using Pam.Dao.Entites;
using Pam.Metier.Entites;
using Pam.Metier.Service;
using Spring.Context.Support;
namespace Pam.Metier.Tests {
[TestFixture()]
public class NunitTestPamMetier : AssertionHelper {
// the [metier] layer to test
private IPamMetier pamMetier;
// manufacturer
public NunitTestPamMetier() {
// layer instantiation [dao]
pamMetier = ContextRegistry.GetContext().GetObject("pammetier") as IPamMetier;
}
[Test]
public void GetAllIdentitesEmployes() {
// audit no. of employees
Expect(2, EqualTo(pamMetier.GetAllIdentitesEmployes().Length));
}
[Test]
public void GetSalaire1() {
// wage sheet calculation
FeuilleSalaire feuilleSalaire = pamMetier.GetSalaire("254104940426058", 150, 20);
// checks
Expect(368.77, EqualTo(feuilleSalaire.ElementsSalaire.SalaireNet).Within(1E-06));
// non-existent employee payslip
bool erreur = false;
try {
feuilleSalaire = pamMetier.GetSalaire("xx", 150, 20);
} catch (PamException) {
erreur = true;
}
Expect(erreur, True);
}
}
}
- 第 13 行:私有字段 [pamMetier] 是提供对 [metier] 层访问权限的接口的实例。请注意,该字段的类型是接口,而非类。这意味着 [PamMetier] 实例仅提供 [IPamMetier] 接口的方法。
- 第 16–19 行:类构造函数使用 Spring 和配置文件 [App.config] 初始化私有字段 [pamMetier]。
- 第 23–26 行:测试 [GetAllIdentitesEmployes] 方法
- 第 29–42 行:测试 [GetSalaire] 方法
上述项目会在 [bin/Release] 文件夹中生成 DLL 文件 [pam-metier.dll]。
![]() |
[bin/Release] 文件夹还包含:
- 作为项目引用的一部分且 [Local Copy] 属性设置为 true 的 DLL:[Spring.Core, MySql.data, NHibernate, log4net, pam-dao-nhibernate]。这些 DLL 还附带了它们自身所使用的 DLL 的副本:
- [CastleDynamicProxy, Iesi.Collections](用于 NHibernate 工具)
- [antlr.runtime, Common.Logging] 用于 Spring 工具
- 文件 [pam-metier-dao-nhibernate.dll.config] 是配置文件 [App.config] 的副本。
我们使用 [NUnit-Gui(版本 2.4.6)] 工具加载 DLL [pam-metier-dao-nhibernate.dll] 并运行测试:

如上所示,测试已成功通过。
实践练习:
在本地机器上为 [PamMetier] 类实现测试。- 使用不同的 App.config 配置文件来使用不同的数据库管理系统(Firebird、MySQL、Postgres、SQL Server)
7.4.7. 生成 [business] 层 DLL
在编写并测试完 [PamMetier] 类后,我们将按照第 7.3.5 节所述的方法,为 [business] 层生成 [pam-metier-dao-hibernate.dll] DLL。 我们将注意不要将测试程序 [Main.cs] 和 [NUnit.cs] 包含在 DLL 中。随后,我们将该 DLL 放置在 DLL 文件夹 [lib] 中 [1]。
![]() |
7.5. [Web] 层
让我们重新审视 [SimuPaie] 应用程序的总体架构:
![]() |
我们假设 [dao] 和 [business] 层已经完成,并封装在 DLL 文件 [pam-dao-hibernate, pam-business-dao-hibernate.dll] 中。接下来我们将描述 Web 层。
7.5.1. [web] 层的 Visual Web Developer 项目
![]() |
- 在 [1] 中,整个项目:
- [Global.asax]:Web 应用程序启动时实例化的类,负责处理应用程序的初始化
- [Default.aspx]:Web 表单页面
- 在 [2] 中,是 Web 应用程序所需的 DLL。请注意之前构建的 [DAO] 和 [business] 层的 DLL。
7.5.2. 应用程序配置
用于配置应用程序的 [Web.config] 文件定义的数据与前文讨论的用于配置 [business] 层的 [App.config] 文件相同。这些数据必须放置在 [Web.config] 文件的预生成代码中:
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<configSections>
<sectionGroup name="system.web.extensions" type="System.Web.Configuration.SystemWebExtensionsSectionGroup, System.Web.Extensions, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35">
..........
</sectionGroup>
<sectionGroup name="spring">
<section name="context" type="Spring.Context.Support.ContextHandler, Spring.Core" />
<section name="objects" type="Spring.Context.Support.DefaultSectionHandler, Spring.Core" />
</sectionGroup>
<section name="hibernate-configuration" type="NHibernate.Cfg.ConfigurationSectionHandler, NHibernate" />
<section name="log4net" type="log4net.Config.Log4NetConfigurationSectionHandler,log4net" />
</configSections>
<!-- spring configuration -->
<spring>
<context>
<resource uri="config://spring/objects" />
</context>
<objects xmlns="http://www.springframework.net">
<object id="pamdao" type="Pam.Dao.Service.PamDaoNHibernate, pam-dao-nhibernate" init-method="init" destroy-method="destroy"/>
<object id="pammetier" type="Pam.Metier.Service.PamMetier, pam-metier-dao-nhibernate" >
<property name="PamDao" ref="pamdao"/>
</object>
</objects>
</spring>
<!-- configuration NHibernate -->
<hibernate-configuration xmlns="urn:nhibernate-configuration-2.2">
<session-factory>
<property name="connection.provider">NHibernate.Connection.DriverConnectionProvider</property>
<!--
<property name="connection.driver_class">NHibernate.Driver.MySqlDataDriver</property>
-->
<property name="dialect">NHibernate.Dialect.MySQLDialect</property>
<property name="connection.connection_string">
Server=localhost;Database=dbpam_nhibernate;Uid=root;Pwd=;
</property>
<property name="show_sql">false</property>
<mapping assembly="pam-dao-nhibernate"/>
</session-factory>
</hibernate-configuration>
<!-- This section contains the log4net configuration settings -->
<!-- NOTE IMPORTANTE: logs are not active by default. They must be activated by program
avec l'instruction log4net.Config.XmlConfigurator.Configure();
! -->
<log4net>
....
</log4net>
<appSettings/>
<connectionStrings/>
<system.web>
....
....
</configuration>
第 9–12 行、第 18–28 行以及第 31–44 行包含 [business] 层 [App.config] 文件中描述的 Spring 和 NHibernate 配置(参见第 7.4.5 节)。
Global.asax.cs
using System;
using Pam.Dao.Entites;
using Pam.Metier.Service;
using Spring.Context.Support;
namespace pam_v3
{
public class Global : System.Web.HttpApplication
{
// --- static application data ---
public static Employe[] Employes;
public static IPamMetier PamMetier = null;
public static string Msg;
public static bool Erreur = false;
// application startup
public void Application_Start(object sender, EventArgs e)
{
// using the configuration file
try
{
// instantiation layer [metier]
PamMetier = ContextRegistry.GetContext().GetObject("pammetier") as IPamMetier;
// simplified list of employees
Employes = PamMetier.GetAllIdentitesEmployes();
// we succeeded
Msg = "Base chargée...";
}
catch (Exception ex)
{
// we note the error
Msg = string.Format("L'erreur suivante s'est produite lors de l'accès à la base de données : {0}", ex);
Erreur = true;
}
}
}
}
请注意:
- [Global.asax.cs] 类在应用程序启动时被实例化,且该实例对所有用户的所有请求均可见。因此,第 11–14 行中的静态字段将被所有用户共享。
- [Application_Start] 方法仅在类实例化后执行一次。该方法通常用于初始化应用程序。
所有用户共享的数据如下:
- 第 11 行:一个 [Employee] 对象数组,用于存储所有员工的简化列表(SS、LAST_NAME、FIRST_NAME)
- 第 12 行:指向封装在 DLL [pam-metier-dao-nhibernate.dll] 中的 [business] 层的引用
- 第 13 行:一条消息,用于指示初始化是否成功完成或出现错误
- 第 14 行:一个布尔值,用于指示初始化是否因错误而失败。
在 [Application_Start] 中:
- 第 23 行:Spring 实例化 [business] 和 [DAO] 层,并返回 [business] 层的引用。该引用存储在第 12 行定义的静态字段 [PamMetier] 中。
- 第 25 行:从 [business] 层请求员工数组
- 第 27 行:显示成功消息
- 第 32 行:错误消息
7.5.3. [Default.a spx] 表单
该表单来自第 2 版。

问题:以版本 2 的 [Default.aspx.cs] 页面中的 C# 代码为参考,编写版本 3 的 [Default.aspx.cs] 代码。唯一的区别在于薪资计算部分。虽然版本 2 使用 ADO.NET API 从数据库中检索信息,但在此我们将使用 [business] 中的 GetSalaire 方法。
实践练习:
将前一个 Web 应用程序部署到一台机器上- 使用不同的 [Web.config] 配置文件来支持不同的数据库管理系统(Firebird、MySQL、Postgres、SQL Server)





























