Skip to content

7. 应用程序 [SimuPaie] – 版本 3 – 基于 NHibernate 的三层架构


推荐阅读:《C# 2008,第 4 章:三层架构、NUnit 测试、Spring 框架》


7.1. 应用程序的总体架构

[SimuPaie]应用程序现将采用以下三层结构:

  • [1-dao]层(DAO=数据访问对象)负责数据访问。
  • [2-métier]层将负责应用程序的业务逻辑,即薪资计算。
  • [3-ui]层(ui=用户界面)负责向用户展示数据并执行其请求。我们将承担此功能的全部模块统称为[Application]。它是用户与系统的交互界面。
  • 通过使用接口,这三层将实现相互独立。NET
  • 各层之间的集成将由 Spring IoC 实现

处理客户端请求的流程如下:

  1. 客户端向应用程序发起请求。
  2. 应用程序处理该请求。为此,它可能需要 [métier] 层的协助;若需与数据库交换数据,该层又可能需要 [dao] 层的协助。
  3. 应用程序从 [métier] 层收到响应。根据该响应,它向客户发送相应的视图(即响应)。

以计算保育员的工资为例。这将需要几个步骤:

  1. [ui]层需要向用户询问
    • 需要计算工资的人员身份
    • 该人员的工作天数
    • 其工作小时数
  1. 为此,它需要向用户展示[EMPLOYES]表中的人员列表(姓名、名字、SS),以便用户从中选择一人。 [ui]层将通过[2, 3, 4, 5, 6, 7]路径获取这些信息。操作[2]用于请求员工列表,操作[7]则用于响应该请求。 完成上述步骤后,[ui]层可通过[8]向用户展示员工名单。
  2. 用户将向 [ui] 层提交工作天数及工作小时数。这就是上文提到的 [1] 操作。 在此步骤中,用户仅与 [ui] 层进行交互。该层将主要负责验证所输入数据的有效性。验证完成后,用户将请求进行薪资计算。
  3. [ui]层将请求业务层进行此计算。为此,它将向业务层传输从用户处接收到的数据。这就是操作[2]。
  4. [metier]层需要某些信息才能完成其工作:
    • 关于该人员的更完整信息(地址、指数等)
    • 与其指数相关的津贴
    • 从毛薪中扣除的各项社会保险费率

它将通过路径 [3, 4, 5, 6] 向 [dao] 层请求这些信息。[3] 是初始请求,而 [6] 是对此请求的响应。

  1. 在获取所需全部数据后,[metier]层计算用户所选人员的薪资。
  2. [metier] 层现在可以响应 [ui] 层在 (d) 中发出的请求。这就是路径 [7]。
  3. [ui] 层将对这些结果进行格式化,以便以适当的形式呈现给用户,然后进行展示。其路径为 [8]。
  4. 可以设想,这些结果需要存储在文件或数据库中。这可以自动完成。在这种情况下,在操作 (f) 之后,[metier] 层将请求 [dao] 层保存结果。 此时将采用路径 [3, 4, 5, 6]。该操作也可应用户请求执行,此时请求-响应循环将使用路径 [1-8]。

从上述描述中可以看出,一个层只会使用其右侧层的资源,绝不会使用其左侧层的资源。

我们对该三层架构的首次实现将是一个名为 ASP.NET 的应用程序,其中

  • [dao] 和 [metier] 层将由 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:用于 ORM 和 NHibernate
  • MySql.Data:SGBD的驱动程序,对应ADO.NET
  • Spring.Core:用于 Spring 框架
  • log4net:日志库
  • nunit.framework:单元测试库

这些引用取自文件夹 [lib] [4]。请确保所有这些引用的“本地副本”属性均设置为“True” [5]:

7.2.2. [dao]图层的实体

   

[dao]图层所需的实体(对象)已汇总到项目中的[entites]文件夹中。 其中一些我们已经知道:第 6.3.2.1 段描述的 [Cotisations]、第 6.3.2.3 段描述的 [Employe]、第 6.3.2.2 段描述的 [Indemnites]。 它们均位于 [Pam.Dao.Entites] 命名空间中。

类 [Employe] 的演变如下:


namespace Pam.Dao.Entites {
    public class Employe {
        // 自动属性
        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; }

        // 构建器
        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] 或其派生类型,则必须在方法签名中注明该方法会抛出(throws)异常。在 .NET 中,所有异常均为非受控异常,c.a.d。 因此,无需声明 [GetAllIdentitesEmployes, GetEmploye, GetCotisations] 方法可能抛出异常。

不过,能够区分不同的异常还是很有意义的,因为它们的处理方式可能不同。因此,处理各种异常类型的代码可以编写如下:

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] 层创建了一个异常类型。该类型如下所示:


using System;
namespace Pam.Dao.Entites {

    public class PamException : Exception {

        // 错误代码 
        public int Code { get; set; }

        // 构造函数 
        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.Entites] 命名空间
  • 第 4 行:该类继承自类 [Exception]
  • 第 7 行:它有一个公共属性 [Code],该属性是一个错误代码
  • 在我们的 [dao] 层中,我们将使用两种类型的构造函数:
    • 第18-21行中的构造函数,其用法如下所示:
throw new PamException("Problème d'accès aux données",5);
  • (续)
    • 或第 23-26 行中的构造函数,用于将已发生的异常封装为 [PamException] 类型的异常并上报:
try{
....
}catch (IOException ex){
     // 封装异常
    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] 建立了表 [EMPLOYES] 与类 [Employe] 之间的映射

<?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行)中,包含以下属性:

  • namespace : Pam.Dao.Entites。类 [Cotisations]、[Employe] 和 [Indemnites] 必须位于此命名空间中。
  • assemblypam-dao-nhibernate。 映射文件 [*.hbm.xml] 必须封装在一个名为 [pam-dao-nhibernate] 的 DLL 中。为实现此效果,C# 项目配置如下:
  • 在 [1] 中,项目程序集的名称为 [pam-dao-nhibernate]
  • 在 [2] 中,映射文件 [*.hbm.xml] 已集成到项目程序集 [3] 中

7.2.5. [IPamDao]接口 来自[dao]层

让我们回到应用程序的架构:

在简单的情况下,我们可以从 [metier] 层开始,以发现应用程序的接口。为了运行,它需要数据:

  • 这些数据可能已存在于文件、数据库中,或通过网络获取。它们由 [dao] 层提供。
  • 尚未可用。此时由 [ui] 层提供,该层从应用程序用户处获取数据。

[dao]层应向[metier]层提供哪些接口?这两个层之间可能存在哪些交互?[dao]层必须向[metier]层提供以下数据:

  • 保姆名单,以便用户从中选择特定的一位
  • 所选人员的完整信息(地址、指数等)
  • 与该人员指数相关的补贴
  • 各类社会保险费率

这些信息在计算工资前已知,因此可以预先存储。 在 [metier] -> [dao] 的方向上,[metier] 层可要求 [dao] 层保存薪资计算结果。但本文中我们将不进行此操作。

基于这些信息,我们可以尝试初步定义 [dao] 层的接口:


using Pam.Dao.Entites;

namespace Pam.Dao.Service {
    public interface IPamDao {
        // 所有员工身份信息列表 
        Employe[] GetAllIdentitesEmployes();
        // 某位特定员工及其津贴 
        Employe GetEmploye(string ss);
        // 所有缴费的列表 
        Cotisations GetCotisations();
    }
}
  • 第 1 行:导入 [dao] 层实体的命名空间。
  • 第 3 行:[dao] 层位于 [Pam.Dao.Service] 命名空间中。 命名空间 [Pam.Dao.Entites] 中的元素可以创建多个实例。命名空间 [Pam.Dao.Service] 中的元素仅创建一个实例(单例)。这就是选择这些命名空间名称的依据。
  • 第 4 行:该接口名为 [IPamDao]。它定义了三个方法:
    • 第 6 行,[GetAllIdentitesEmployes] 返回一个 [Employe] 类型的对象数组,该数组以简化形式(姓、名、SS)表示保育员列表。
    • 第 8 行,[GetEmploye] 返回一个 [Employe] 对象:该对象包含作为方法参数传入的社会保险号对应的员工及其与指数相关的津贴。
    • 第10行,[GetCotisations] 返回对象 [Cotisations],该对象封装了从毛薪中扣除的各项社会保险费率。

7.3. [dao] 层的实现与测试

7.3.1. Visual Studio 项目

Visual Studio 项目此前已介绍过。在此回顾一下:

  • 在 [1] 中,该项目整体
  • 在 [2] 中,项目中的各个类。文件夹 [entites] 包含由 [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 {
                // 实例化 [dao] 层
                IPamDao pamDao = (IPamDao)ContextRegistry.GetContext().GetObject("pamdao");
                // 员工身份列表 
                foreach (Employe Employe in pamDao.GetAllIdentitesEmployes()) {
                    Console.WriteLine(Employe.ToString());
                }
                // 某位员工及其津贴 
                Console.WriteLine("------------------------------------");
                Console.WriteLine(pamDao.GetEmploye("254104940426058"));
                Console.WriteLine("------------------------------------");
                // 缴费清单 
                Cotisations cotisations = pamDao.GetCotisations();
                Console.WriteLine(cotisations.ToString());
            } catch (Exception ex) {
                // 异常显示 
                Console.WriteLine(ex.ToString());
            }
            //休息 
            Console.ReadLine();
        }
    }
}
  • 第 11 行:向 Spring 请求 [dao] 层的引用。
  • 第13-15行:测试接口[IPamDao]中的方法[GetAllIdentitesEmployes]
  • 第 18 行:测试接口 [IPamDao] 中的方法 [GetEmploye]
  • 第21行:测试接口[IPamDao]中的方法[GetCotisations]

Spring、NHibernate 和 log4net 通过以下 [App.config] 文件 进行配置:


<?xml version="1.0" encoding="utf-8" ?>
<configuration>
    <!-- 配置部分 -->
    <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配置 -->
    <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>

    <!-- 配置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>

    <!-- 本节包含 log4net 配置设置 -->
    <!-- NOTE IMPORTANTE:日志默认处于非活动状态。必须通过程序将其激活
    avec l'instruction log4net.Config.XmlConfigurator.Configure();
    ! -->
    <log4net>
    ...
    </log4net>

</configuration>

NHibernate 的配置(第 10 行、第 25-36 行)已在第 6.3.1 节中进行说明。请注意第 34 行,该行指出映射文件位于 [pam-dao-nhibernate] 程序集内。这是该项目的程序集。

Spring的配置位于第6-9行、第15-22行。第20行定义了由控制台程序[Main.cs]使用的对象[pamdao]。此处的<object>标签具有以下属性:

  • type:指定要实例化的类。 [PamDaoNHibernate] 类实现了 [IPamDao] 接口。该类位于项目中的 DLL [pam-dao-nhibernate] 中。
  • init-method:在类实例化后需执行的 [PamDaoNHibernate] 类的方法
  • destroy-method:在项目执行结束时Spring容器被销毁时需执行的[PamDaoNHibernate]类的方法。

使用第 6.2 节所述的数据库进行执行后,控制台输出如下:

1
2
3
4
5
6
[254104940426058,Jouveinal,Marie,,,,]
[260124402111742,Laverti,Justine,,,,]
------------------------------------
[254104940426058,Jouveinal,Marie,5 rue des oiseaux,St Corentin,49203,[2, 2,1, 2,1, 3,1, 15]]
------------------------------------
[3,49,6,15,9,39,7,88]
  • 第 1-2 行:2 名类型为 [Employe] 的员工,仅包含 [SS, Nom, Prenom] 的信息
  • 第4行:类型为[Employe]的员工,其社会保险号为[254104940426058]
  • 第5行:缴费率

7.3.3. [PamDaoNHibernate] 类别的记账

由 [dao] 层实现的 [IPamDao] 接口如下:


using Pam.Dao.Entites;

namespace Pam.Dao.Service {
    public interface IPamDao {
        // 所有员工身份信息的列表 
        Employe[] GetAllIdentitesEmployes();
        // 特定员工及其津贴 
        Employe GetEmploye(string ss);
        // 所有缴费项目列表 
        Cotisations GetCotisations();
    }
}

问题:编写类 [PamDaoNHibernate] 的代码,该类需使用前文所述配置的 NHibernate 框架来实现上述 [IPamDao] 接口。 同时需实现由Spring调用的initdestroy方法。init方法将创建SessionFactory,并从中获取Session对象。 方法 destroy 将关闭该 SessionFactory。我们将参考第 6.5 节中的示例。


约束条件

假设从 [dao] 层请求的某些数据能够完全驻留于内存中。因此,为了提高性能,[PamDaoNHibernate] 类将存储:

  • [EMPLOYES]表,其形式为(SS、NOM、 PRENOM)——该表由方法 [GetAllIdentitesEmployes] 调用,以 [Employe] 类型对象数组的形式存储
  • 表 [COTISATIONS] 中的数据,该表以单个 [Cotisations] 类型对象的形式呈现

这将在该类的 [init] 方法中实现。[PamDaoNHibernate] 类的骨架可能如下所示:


using System;
...

namespace Pam.Dao.Service {
    class PamDaoNHibernate : IPamDao {
        // 私有字段 
        private Cotisations cotisations;
        private Employe[] employes;
        private ISessionFactory sessionFactory = null;

        // 初始化 
        public void init() {
            try {
                // 初始化工厂
                sessionFactory = new Configuration().Configure().BuildSessionFactory();
                // 获取缴费率及员工信息并缓存 
.......................
        }

        // 关闭SessionFactory
        public void destroy() {
            if (sessionFactory != null) {
                sessionFactory.Close();
            }
        }

        // 列出所有员工的身份信息 
        public Employe[] GetAllIdentitesEmployes() {
            return employes;
        }

        // 某名员工及其津贴 
        public Employe GetEmploye(string ss) {
................................
        }

        // 缴费清单 
        public Cotisations GetCotisations() {
            return cotisations;
        }
    }
}

7.3.4. 使用 NUnit 进行单元测试


推荐阅读《C# 2008 语言,第 4 章:三层架构、NUnit 测试、Spring 框架》


之前的测试是目视检查:通过屏幕确认是否得到了预期的结果。这种方法在专业环境中是不够的。测试必须尽可能实现自动化,并力求无需人工干预。 事实上,人类容易疲劳,其验证测试的能力会随着一天的推移而逐渐减弱。[NUnit]工具有助于实现这种自动化。该工具可在URL [http://www.nunit.org/]获取。

[dao] 层的 Visual Studio 项目将按以下方式演进:

  • 到 [1],测试程序 [NUnit.cs]
  • 将更新为 [2,3],该项目将生成一个名为 [pam-dao-nhibernate.dll] 的 DLL
  • 在 [4] 中,对框架 NUnit 中 DLL 的引用:[nunit.framework.dll]
  • 在 [5] 中,类 [Main.cs] 将不会包含在 DLL [pam-dao-nhibernate]
  • 在[6]中,[NUnit.cs]类将被包含在DLL和[pam-dao-nhibernate]中

测试类 NUnit如下:


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 {
        // 待测试的[dao]层 
        private IPamDao pamDao = null;

        // 生成器 
        public NunitPamDao() {
            // [dao] 层的实例化
            pamDao = (IPamDao)ContextRegistry.GetContext().GetObject("pamdao");
        }

        // 初始化 
        [SetUp]
        public void Init() {

        }

        [Test]
        public void GetAllIdentitesEmployes() {
            // 员工人数验证 
            Expect(2, EqualTo(pamDao.GetAllIdentitesEmployes().Length));
        }

        [Test]
        public void GetCotisations() {
            // 缴费率核查 
            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() {
            // 个人核验 
            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() {
            // 不存在的个人核查 
            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 行:测试方法 [GetAllIdentitesEmployes]
  • 第 35-42 行:测试方法 [GetCotisations]
  • 第 45-53 行:测试方法 [GetEmploye]
  • 第 56-65 行:在发生异常时测试方法 [GetEmploye]。

项目生成会在 [bin/Release] 文件夹中创建 DLL [pam-dao-nhibernate.dll]。

此外,[bin/Release]文件夹还包含:

  • 属于项目引用且 [Copie locale] 属性为 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] 的副本。该复制操作由 VS 执行。 运行时使用的是文件 [pam-dao-nhibernate.dll.config],而非 [App.config]。

使用版本 2.4.6 的 [NUnit-Gui] 工具加载 DLL 和 [pam-dao-nhibernate.dll],并执行测试:

如上所示,测试已成功通过。

实践操作


  • 在机器上实现类 [PamDaoNHibernate] 的测试。
  • 使用不同的[App.config]配置文件,以调用不同的SGBD(Firebird、MySQL、Postgres、SQL Server)

7.3.5. 生成 ,以及[dao]层的DLL

编写并测试完类 [PamDaoNHibernate] 后,将按以下方式从 [dao] 层生成 DLL:

  • [1],测试程序不包含在项目汇编中
  • [2,3],项目配置
  • [4],生成项目
  • DLL 生成在 [bin/Release] [5] 文件夹中。 我们将它添加到已存在于文件夹 [lib] [6] 中的 DLL 中:

7.4. 业务层

让我们回顾一下 [SimuPaie] 应用程序的总体架构:

我们现在认为 [dao] 层已实现,并被封装在 DLL 和 [pam-dao-nhibernate.dll] 中。 现在我们关注 [metier] 层。该层实现了业务规则,即此处的工资计算规则。

7.4.1. [metier]层的 Visual Studio项目

业务层的 Visual Studio 项目可能如下所示:

  • 在 [1] 中,由文件 [App.config] 配置的整个项目
  • 在 [2] 中,[metier] 层由两个 [entites, service] 文件夹组成。 文件夹 [tests] 包含一个控制台测试程序(Main.cs)和一个测试程序 NUnit(NUnit.cs)。
  • 在 [3] 中列出了该项目使用的引用。值得注意的是,此前研究的 [dao] 层包含 DLL 和 [pam-dao-nhibernate]。

7.4.2. [metier] 层中的 [IPamMetier] 接口

让我们回到应用程序的总体架构:

[metier]层应向[ui]层提供哪些接口?这两个层之间可能存在哪些交互?让我们回顾一下将呈现给用户的Web界面:

  1. 在表单初始显示时,[1]中应包含员工列表。简化的列表即可(姓、名、SS)。 编号 SS 用于访问所选员工的补充信息(信息 6 至 11)。
  2. 第12至15项信息为各项缴费率。
  3. 信息16至19为与员工指数相关的津贴
  4. 信息 20 至 24 是根据用户输入的 1 至 3 项计算得出的工资组成部分。

由 [metier] 层提供给 [ui] 层的 [IPamMetier] 接口必须满足上述要求。可能存在多种接口方案。我们建议采用以下方案:


using Pam.Dao.Entites;
using Pam.Metier.Entites;

namespace Pam.Metier.Service {
    public interface IPamMetier {
        // 所有员工身份信息列表 
        Employe[] GetAllIdentitesEmployes();

        // ------- 工资计算 
        FeuilleSalaire GetSalaire(string ss, double heuresTravaillées, int joursTravaillés);
    }
}
  • 第 7 行:用于填充下拉列表 [1] 的方法
  • 第10行:用于获取第6至24项信息的处理方法。这些信息已汇总到类型为[FeuilleSalaire]的对象中。

7.4.3. [metier] 层的实体

Visual Studio 项目中的 [entites] 文件夹包含业务类所操作的对象:[FeuilleSalaire] 和 [ElementsSalaire]。

类 [FeuilleSalaire] 封装了前一个表单中的第 6 至 24 项信息:


using Pam.Dao.Entites;

namespace Pam.Metier.Entites {

    public class FeuilleSalaire {

        // 自动属性 
        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。需注意,此处一个 [Employe] 对象封装了一个 [Indemnites] 对象,该对象代表员工的津贴。
  • 第9行:第12至15项信息
  • 第10行:第20至24项信息
  • 第13-15行:方法 [ToString]

类 [ElementsSalaire] 封装了表单中的第 20 至 24 项信息:


namespace Pam.Metier.Entites {
    public class ElementsSalaire {
        // 自动属性 
        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. [metier] 层的实现

我们将通过两个类来实现 [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 {

        // 数据访问对象 
        public IPamDao PamDao { get; set; }

        // 所有员工身份的列表 
        public Employe[] GetAllIdentitesEmployes() {
            return PamDao.GetAllIdentitesEmployes();
        }

        // 特定员工及其津贴 
        protected Employe GetEmploye(string ss) {
            return PamDao.GetEmploye(ss);
        }

        // 缴费 
        protected Cotisations GetCotisations() {
            return PamDao.GetCotisations();
        }

        // 工资计算 
        public abstract FeuilleSalaire GetSalaire(string ss, double heuresTravaillées, int joursTravaillés);
    }
}
  • 第 5 行:该类属于 [Pam.Metier.Service] 命名空间,与 [metier] 层的所有类和接口一样。
  • 第 6 行:该类是抽象类(属性 abstract),并实现了接口 [IPamMetier]
  • 第 9 行:该类通过公共属性持有对 [dao] 层的引用
  • 第12-14行:实现了接口[IPamMetier]中的方法[GetAllIdentitesEmployes]——调用[dao]层中同名的方法
  • 第 17-19 行:内部(受保护)方法 [GetEmploye],调用 [dao] 层中的同名方法——声明为 protected,以便派生类能够访问该方法,而无需将其设为公共。
  • 第 22-24 行:内部方法(protected)[GetCotisations],该方法调用了 [dao] 层中同名的方法
  • 第27行:接口[IPamMetier]中方法[GetSalaire]的抽象实现(属性abstract)。

薪资计算由以下类 [PamMetier] 实现:


using System;
using Pam.Dao.Entites;
using Pam.Metier.Entites;

namespace Pam.Metier.Service {

    public class PamMetier : AbstractBasePamMetier {

        // 工资计算 
        public override FeuilleSalaire GetSalaire(string ss, double heuresTravaillées, int joursTravaillés) {
            // SS:员工的SS编号 
            // HeuresTravaillées:工作小时数 
            // 工作日:工作日数 
            // 获取员工及其津贴 
            ...
            // 获取各项缴费率 
            ...
            // 计算工资构成 
            ...
            // 生成工资单 
            return ...;
        }
    }
}
  • 第 7 行:该类继承自 [AbstractBasePamMetier],因此实现了 [IPamMetier] 接口
  • 第10行:待实现的方法[GetSalaire]

问题:编写方法 [GetSalaire] 的代码。


7.4.5. [metier] 层的控制台测试

回顾 [metier] 层的 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 {
                // 实例化层 [metier]
                IPamMetier pamMetier = ContextRegistry.GetContext().GetObject("pammetier") as IPamMetier;
                // 计算工资单 
                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()));
            }
            // 暂停 
            Console.ReadLine();
        }
    }
}
  • 第 11 行:通过 Spring 实例化 [metier] 层。
  • 第 13-14 行:测试接口 [IPamMetier] 的方法 [GetSalaire]
  • 第 15-22 行:测试在发生异常时 [GetSalaire] 方法的行为

测试程序使用以下配置文件: [App.config]


<?xml version="1.0" encoding="utf-8" ?>
<configuration>
    <!-- 配置部分 -->
    <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 配置 -->
    <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>

    <!-- 配置NHibernate -->
    <hibernate-configuration xmlns="urn:nhibernate-configuration-2.2">
....
    </hibernate-configuration>

    <!-- 本节包含 log4net 配置设置 -->
    <!-- NOTE IMPORTANTE:日志默认处于非活动状态。必须通过程序将其激活
    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
3
[[260124402111742,Laverti,Justine,La Brûlerie,St Marcel,49014,[1, 1,93, 2, 3, 12]],[3,49,6,15,9,39,7,88],[1, 1,93, 2, 3, 12],[64,85 : 17,45 : 10 : 15 : 72,4 ]
[[254104940426058,Jouveinal,Marie,5 rue des oiseaux,St Corentin,49203,[2, 2,1, 2,1, 3,1, 15]],[3,49,6,15,9,39,7,88],[2, 2,1, 2,1, 3,1, 15],[362,25 : 97,48 : 42: 62 : 368,77 ]
PamException : L'employé de n° ss [xx] n'existe pas
  • 第1-2行:请求的2份工资单
  • 第3行:由不存在的员工引发的[PamException]类型异常。

7.4.6. 业务层的单元测试

上一个测试是视觉测试:我们通过屏幕验证是否获得了预期的结果。现在我们转到非视觉测试 NUnit。

让我们回到 [metier] 项目的 Visual Studio 项目中:

  • 在 [1] 中,测试程序 NUnit
  • 变为 [2],引用 DLL 时,[nunit.framework]
  • 生成 [3,4],项目生成将产生 DLL [pam-metier-dao-nhibernate.dll]。
  • 在 [5] 中,文件 [NUnit.cs] 将被包含在程序集 [pam-metier-dao-nhibernate.dll] 中,但不会包含 [Main.cs] 和 [6]

测试类 NUnit如下所示:


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 {

        // 待测试的 [metier] 层 
        private IPamMetier pamMetier;

        // 构造函数
        public NunitTestPamMetier() {
            // [dao] 层的实例化
            pamMetier = ContextRegistry.GetContext().GetObject("pammetier") as IPamMetier;
        }


        [Test]
        public void GetAllIdentitesEmployes() {
            // 员工人数核对 
            Expect(2, EqualTo(pamMetier.GetAllIdentitesEmployes().Length));
        }

        [Test]
        public void GetSalaire1() {
            // 计算工资单 
            FeuilleSalaire feuilleSalaire = pamMetier.GetSalaire("254104940426058", 150, 20);
            // 核查 
            Expect(368.77, EqualTo(feuilleSalaire.ElementsSalaire.SalaireNet).Within(1E-06));
            // 不存在的员工工资单 
            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]文件夹还包含:

  • 属于项目引用且 [Copie locale] 属性为 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, version 2.4.6] 加载 DLL 和 [pam-metier-dao-nhibernate.dll],并执行测试:

如上所示,测试已成功通过。

实践任务


  • 在机器上实现类 [PamMetier] 的测试。
  • 使用不同的 App.config 配置文件,以调用不同的 SGBD(Firebird、MySQL、Postgres、SQL Server)

7.4.7. 生成 [metier] 层的 DLL

编写并测试完类 [PamMetier] 后,将按照第 7.3 节所述的方法,从 [metier] 层生成 DLL 和 [pam-metier-dao-nhibernate.dll]。5 请注意,不要将测试程序 [Main.cs] 和 [NUnit.cs] 包含在 DLL 中。 随后将其放置在 [lib] 文件夹中,该文件夹位于 DLL 和 [1] 文件夹内。

7.5. [web] 层

让我们回顾一下 [SimuPaie] 应用程序的总体架构:

我们认为 [dao] 和 [métier] 层已构建完成,并封装在 DLL 和 [pam-dao-nhibernate, pam-metier-dao-nhibernate.dll] 中。现在我们来描述 Web 层。

7.5.1. [web] 层的 Visual Web Developer 项目

  • 在 [1] 中,整个项目:
    • [Global.asax]:在Web应用程序启动时实例化的类,负责应用程序的初始化
    • [Default.aspx]:Web表单页面
  • 在 [2] 中,包含 Web 应用程序所需的 DLL。 请注意先前构建的 [dao] 和 [metier] 层中的 DLL。

7.5.2. 应用程序配置

用于配置应用程序的文件 [Web.config] 定义的数据与之前研究过的 [metier] 层配置文件 [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配置 -->
  <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>

  <!-- 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>

  <!-- 本节包含 log4net 配置设置 -->
  <!-- NOTE IMPORTANTE:日志默认处于非活动状态。必须通过程序将其激活
    avec l'instruction log4net.Config.XmlConfigurator.Configure();
    ! -->
  <log4net>
....
  </log4net>

  <appSettings/>
  <connectionStrings/>

  <system.web>
....
....

</configuration>

在第 9-12 行、18-28 行和 31-44 行中,可以看到 [metier] 层中 [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
  {
    // --- 应用程序静态数据 ---
    public static Employe[] Employes;
    public static IPamMetier PamMetier = null;
    public static string Msg;
    public static bool Erreur = false;

    // 应用程序启动
    public void Application_Start(object sender, EventArgs e)
    {
      // 配置文件处理
      try
      {
        // [metier] 层实例化
        PamMetier = ContextRegistry.GetContext().GetObject("pammetier") as IPamMetier;
        // 员工简易列表
        Employes = PamMetier.GetAllIdentitesEmployes();
        // 成功
        Msg = "Base chargée...";
      }
      catch (Exception ex)
      {
        // 记录错误
        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 行:类型为 [Employe] 的对象数组,用于存储所有员工的简化列表(SS、NOM、PRENOM)
  • 第 12 行:对封装在 DLL 中的 [metier] 层的引用
  • 第13行:一条消息,指示初始化结果(成功或出错)
  • 第14行:一个布尔值,表示初始化是否以错误结束。

在 [Application_Start] 中:

  • 第 23 行:Spring 实例化了 [metier] 和 [dao] 层,并返回了 [metier] 层的引用。 该引用存储在第 12 行中的静态字段 [PamMetier] 中。
  • 第 25 行:向 [metier] 层请求员工表
  • 第 27 行:成功时的消息
  • 第 32 行:错误提示

7.5.3. 表单 [Default.aspx]

该表单属于第 2 版。


问题:参考第2版[Default.aspx.cs]页面的C#代码,编写第3版的[Default.aspx.cs]代码。唯一的区别在于工资的计算方式。 在第2版中,我们使用API和ADO.NET从数据库中检索信息,而在这里,我们将使用[metier]层中的GetSalaire方法


实践操作


  • 在机器上部署前面的Web应用程序
  • 使用不同的 [Web.config] 配置文件,以适配不同的 SGBD 数据库(Firebird、MySQL、Postgres、SQL Server)