Skip to content

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


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


7.1. 应用程序总体架构

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

  • [1-dao] 层(dao = 数据访问对象)将负责数据访问。
  • [2-business] 层将处理应用程序的业务逻辑,具体而言是薪资计算。
  • [3-ui]层(ui = 用户界面)将负责向用户展示数据以及执行用户请求。我们将执行此功能的模块集合称为[应用程序]。它充当用户界面。
  • 这三个层将通过使用 .NET 接口实现独立
  • 不同层之间的集成将由 Spring IoC 负责

客户端请求的处理遵循以下步骤:

  1. 客户端向应用程序发起请求。
  2. 应用程序处理此请求。为此,它可能需要[业务]层的协助;如果需要与数据库交换数据,[业务]层本身可能又需要[DAO]层的协助。
  3. 应用程序从[业务]层接收响应。基于该响应,它向客户端发送相应的视图(即响应)。

让我们以计算保育员薪资为例。这需要几个步骤:

  1. [UI] 层需要向用户询问
    • 待计算薪资人员的身份
    • 该人员的工作天数
    • 该人员的工作小时数
  1. 为此,它需要向用户展示来自 [EMPLOYEES] 表的人员列表(姓、名、社会安全号),以便用户从中选择一人。 [ui]层将使用路径[2, 3, 4, 5, 6, 7]来检索这些信息。操作[2]是请求员工列表;操作[7]是对此请求的响应。完成此操作后,[ui]层可通过[8]向用户展示员工列表。
  2. 用户将向 [ui] 层发送工作天数和工作小时数。这就是上文中的操作 [1]。在此步骤中,用户仅与 [ui] 层进行交互。该层将负责验证输入数据的有效性。完成此步骤后,用户将请求进行工资计算。
  3. [ui]层将请求业务层执行此计算。为此,它会将从用户处接收的数据发送给业务层。这就是操作[2]。
  4. [业务]层需要某些信息来完成其任务:
    • 关于该人员的更完整信息(地址、索引号等)
    • 与其薪级相关的津贴
    • 从毛薪中扣除的各项社会保险缴费率

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

  1. 在获得所需的所有数据后,[业务]层计算用户所选人员的薪酬。
  2. [业务]层现在可以响应[ui]层在(d)中发出的请求。这是路径[7]。
  3. [ui]层将对这些结果进行格式化,以便以适当的形式呈现给用户,然后进行显示。这就是路径[8]。
  4. 可以设想,这些结果需要存储在文件或数据库中。这可以自动完成。 在此情况下,操作 (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 行中的构造函数,其用法如下所示:
throw new PamException("Problème d'accès aux données",5);
  • (待续)
    • 或者第 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] 类必须位于此命名空间中。
  • assemblypam-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
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名类型为[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 调用的 initdestroy 方法。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],并运行测试:

Image

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

实践练习


  • 在一台机器上为 [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. 表单初次显示时,[1]处应显示员工列表。简化的列表即可(姓、名、社会安全号)。社会安全号是访问所选员工详细信息(信息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 {
        // 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] 方法,当发生异常时

该测试程序使用以下配置文件 [ 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"/>
            <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
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 测试。

让我们回到 [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]

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 {
 
        // 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] 并运行测试:

Image

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

实践练习


  • 在本地机器上为 [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 版。

Image


问题:以版本 2 的 [Default.aspx.cs] 页面中的 C# 代码为参考,编写版本 3 的 [Default.aspx.cs] 代码。唯一的区别在于薪资计算部分。虽然版本 2 使用 ADO.NET API 从数据库中检索信息,但在此我们将使用 [business] 中的 GetSalaire 方法。


实践练习


  • 将前一个 Web 应用程序部署到一台机器上
  • 使用不同的 [Web.config] 配置文件来支持不同的数据库管理系统(Firebird、MySQL、Postgres、SQL Server)