第二章 让我们构建一个简单应用程序

本章的标题:让我们构建一个简单的应用程序,但这并不完全正确。事实是我们将开始构建一个应用程序。本章为之奠定了基础。因此,从本章开始,随着本书的展开,我将向您介绍我们将构建的应用程序。

然而,在我看来,仅仅了解应用程序是不够的。为了确保你能充分利用这本书,并在你还在训练的时候继续学习,我将带你了解我的开发设置,并介绍我将使用的各种工具。如果您尝试了本书中的样例代码,并坚持使用本章中提到的工具版本,那么您就可以确保一切都会顺利进行。但是,这并不意味着您必须使用我正在使用的工具和我在环境中安装的版本。现在.net生态系统中的大多数工具都是向后兼容的,所以如果你碰巧有某个工具的较低版本,没关系。只要你知道如何使用这个工具,我们就开干。

除了工具之外,我还将简要介绍一些方法,特别是测试驱动开发(TDD),这些方法将贯穿全书。如果您是TDD的新手,那么不必担心,因为我不会使用任何所谓的基本知识之外的知识。

开干!

 

1.NHibernate的学习方法

在我们真正谈论这个应用程序之前,我想我会写一些关于我将会采取什么样的方法来教你们NHibernate。
在本章中,我们将提出一个简单的问题陈述。我们的目标是使用NHibernate来实现一个有效的数据访问层来解决我们的问题。在这个过程中,你会学到一些重要的东西NHibernate的特性,并应用到一个实际的软件问题。当我们完成这本书时,我们可能没有满足数据访问的全部需求,但我们将以一定的深度完成需求的重要部分。这些足以让你使用NHibernate建立任何类型的数据访问解决方案。
 
从问题陈述直接跳到数据访问层似乎是不连贯的,因此我的第一步是提出一个捕获问题领域核心概念的领域模型。我将尽力遵循两个指导方针:面向对象分析与设计(OOAD)和领域驱动设计(DDD)。两者都是软件设计的专门领域,详细解释它们三天三夜也说不完。因此,我将尝试提出一个简单的领域模型,只针对本书我们的小例子项目。
 
在继续构建数据访问层时,还需要一种方法来验证实现的正确性。因为我们要一砖一瓦地构建它,很多时候,随着我们对NHibernate了解的更多,我们需要回过头来修改一些之前已经实现的东西。当我们改变事物时,我们需要一种方法来确保我们的改变没有改变结果,或者没有破坏之前正常工作的特性。我们将使用TDD来实现我们的解决方案。如果你已经使用过TDD,那么你就会知道我在说什么。如果您是TDD新手,那么不必担心。掌握TDD并不难,对于我们将要围绕TDD进行的工作,您不需要掌握这门艺术。我们将编写简单的测试来验证数据访问层的行为。然后我们将使用NHibernate来构建缺失的部分,并使我们的测试通过。我将确保对将要呈现的代码给出足够的解释。
 
在本书的大部分内容中,解决方案将以单元测试支持的类库的形式实现,以验证代码的正确性。在书的最后,我们应该能够看到完整的解决方案,并理解NHibernate是如何在现实生活中使用的。
 
 
2.业务陈述
我们要解决的问题与员工福利管理系统有关。在这个系统中,员工可以登录并查看他们有权获得的福利并使用这些福利。员工福利的一个非常简单的例子就是休假。每个员工都有权获得带薪或不带薪休假。员工有权在需要的时候休假,但员工的休假数量和休假方式是有规定的。
该系统有两个主要参与者或最终用户。第一个显然是雇员,第二个是管理员用户。员工只能看到他/她的福利。另一方面,管理员用户可以看到其他人的受益,并可以添加/删除受益。管理员本身也可以是员工。这是我们问题陈述的概要。如您所见,这可能是一个相当大而复杂的问题。现在雇主提供了大量的福利。关于谁有资格获得哪些福利、员工在什么情况下可以使用福利、具体如何使用福利、谁批准员工使用特定福利的请求,等等,都有复杂的规则。这本书的目的是学习NHibernate而不是建立一个成熟的员工福利管理系统,所以我将使用该系统需求的一个子集,一个共知的业务需求将帮助我们学习这本书的所有章节。我将尽量选择那些既能让我演示NHibernate大部分特性而又不用花时间去过多解释的需求。清楚了这一点,下面是我们将尝试添加的问题子集的主要需求点:
  • 在这个系统中只有两个参与者。一个是雇员,另一个是管理员用户。
  • 管理员用户可以是雇员,并有权获得他/她自己的福利信息。
  • 管理员可以向系统中添加新的福利,也可以更新现有的福利。
  • 由于审计和法律原因,福利不能从系统中删除。
  • 一些福利可能有与之相关的权利。例如,一名员工每年有20次带薪休假。我们的系统为每个员工保留了可获得的权利,但对休假的管理在我们的系统之外进行。例如,组织有不同的休假管理系统,员工可以申请他们的休假,由他们的直线经理批准。
  • 由于福利的使用发生在这个系统之外,它需要从这些系统导入数据,以反映正确的福利及其应享权利。这个导入可以是每天晚上运行的批处理作业。
  • 该系统支持以下福利:
    • 休假:带薪、病假和无薪休假。
    • 季票贷款:雇主每年一次性为你支付通勤的公交卡,你按月偿还雇主。
    • 技能提升津贴(或培训津贴):所有员工都可以参加由雇主支付的培训。

还有一些附加的系统要求,我将在本书的后面讨论。然而,现在值得一提。这些需求让我们有机会探索NHibernate内建的一些有趣的特性。如果没有NHibernate,这些功能将需要大量的时间投入才能得到正确的实现。

  • 两个管理员用户可以同时更新同一条记录。这种操作的结果是不可预测的,通常只有第一次更新的数据是正确的版本;其余的都在数据的陈旧副本上。我们的数据访问层需要能够检测何时更新陈旧的数据副本并放弃它。
  • 像这样的系统保存着重要的信息。此外,当有多个用户对数据进行操作时,可能会因为某种原因出现错误。公司通常不喜欢员工因为系统没有反映出他们有权获得的福利的正确信息而变得焦躁不安。如果发生这种情况,组织需要知道数据是如何处于这种状态的。这不仅有助于理解哪里出了问题,而且为快速处理任何系统缺陷提供了机会。出于这个原因,我们希望对通过该系统对任何数据进行的每次更改都进行数据审计
您现在不必担心最后三个需求。我们将在本书后面的章节中讨论这个问题。我在这里提出这些是为了使问题更完整。
 
如果你想知道这是基于网络的系统还是桌面应用程序,我建议你不要自找麻烦了。我们的目标只是为这个系统建立一个合适的数据访问层,可以被任何类型的应用程序使用。清楚了这些,让我们继续我们的开发设置,以了解更多关于工具。我将使用他们来实现这个解决方案。
 
 
3.开发环境
为了更有效地学习NHibernate,你必须遵循这本书中的概念和代码示例,并亲自尝试它们。虽然这本书中的代码可以在任何通用的开发环境中工作,但是要谨慎地指出我将特别使用哪些工具。如果由于工具不兼容,本书中的任何代码示例对您不起作用,您应该首先确认开发环境。有几个可选的工具,我将在一些章节中提到。这些工具帮助处理NHibernate,但不是必须的,因为还有其他可用的选择。这里没有列出这些工具。当本书第一次提到它们时,我会介绍它们。
下面是我们将使用的工具列表:
 
3.1 Visual Studio 2013
我将主要使用Visual Studio作为代码编辑器。我将使用VS 2013,这是撰写本书时Visual Studio的最新版本。最近,Visual Studio具有向后和向前的兼容性。这意味着你可以在Visual Studio 2012中打开Visual Studio 2013解决方案,反之亦然。所以,即使你使用的是稍旧或更新的Visual Studio版本也能正常工作。
 
3.2 SQL Server 2012 Express

我们将使用免费的SQL Server 2012版本。SQL的最新版本服务器是2014。但是,NHibernate的最新稳定版本正式支持SQL服务器版本只到2012年。这并不意味着NHibernate将不支持SQL Server 2014。除了SQL Server 2014新添加的特性之外,其他所有功能都可以正常工作。您可以从这里下载SQL Server 2012 Express: http://www.microsoft.com/en-gb/download/details.aspx?id=29062。

注意,SQL Server下载页面有多个可供下载的内容。您可以通过浏览同一页面上的详细信息,为您的硬件和操作系统选择正确的选项。

 

3.3 ReSharper

ReSharper是一个与Visual Studio完全集成的代码检查和重构工具。ReSharper使添加新代码、重构现有代码等变得非常容易,并且极大地提高了您的工作效率。虽然ReSharper在学习NHibernate中没有扮演任何角色,但是我认为它是值得一提的,因为我在准备书中的示例时广泛地使用了它。ReSharper是一个付费的工具,但是你可以从这里下载一个免费的试用版本:https://www.jetbrains.com/ ReSharper /download/

 

3.4 NUnit

我们将使用NUnit来编写单元测试。运行单元测试有两种方式。如果你有ReSharper或者类似的代码重构工具可用,那么你可以在Visual Studio中运行你的NUnit测试。对于其他用户,可以使用NUnit GUI运行程序。NUnit GUI runner是一个Windows应用程序,它没有与Visual Studio集成,但能够从加载的程序集检测和运行单元测试。我将使用ReSharper来运行测试,因为在我的经验中它是最有效率的。

你可以从这里下载NUnit GUI runner:http://www.nunit.org/index.php?p=download.

3.5 NuGet
为了使用NHibernate和其他依赖库,我们将使用NuGet。
NuGet是一个.net的包管理器。可以将NuGet看作您希望在项目中引用的第三方包和库的存储库。NuGet使向项目添加新库、管理引用库的版本等变得非常容易。Visual Studio的最新版本可以开箱即用地支持NuGet,并且不需要更改任何设置就可以启用NuGet。对于旧版本,您需要安装NuGet扩展。关于安装NuGet扩展的说明可以在这里找到:https://docs.nuget.org/consume/installing-nuget.

3.6 SQLite

SQLite是一种轻量级关系数据库,我们将在单元测试期间使用它。引用SQLite网站的原话:

“SQLite是一个软件库,它实现了一个自包含的、无服务器的、零配置的事务性SQL数据库引擎。”

如网站所言,SQLite是自包含的,这意味着极少或不依赖于外部库。它也是没有服务器的,所以它不需要一直运行的服务器来处理我们的数据请求。设置时绝对不需要任何配置。您只需向DLL添加一个引用,仅此而已。而且,最重要的是,它是一个支持SQL的事务和关系数据库引擎。这些属性以及它在内存模式中运行的能力使它适合在单元测试中使用。

 

但是,您必须记住,这是关系数据库的一个轻量级实现,因此它不支持SQL服务器的所有特性。但是,它支持最常用的特性。对于它不支持的部分,我们将在遇到它们时研究它们。

 

4.领域模型

现在我们的开发环境已经就绪,让我们为我们的业务创建一个领域模型。领域模型以一种可以在软件中实现的方式描述问题领域中的重要概念。准备领域模型有不同的技术,表示领域模型也有不同的方法。域模型最常用的表示是类图

我们将准备一个类图,并根据我们目前所掌握的业务知识,确定我们的类及其属性和关系。我将采用一种稍微敏捷的方法。我的意思是我不会在这里过多地分析任何东西。我们将准备一个非常简单的领域模型,在一定程度上满足我的大部分需求。随着我继续实现这些特性,我将发现许多隐藏的领域知识,这将帮助我更好地塑造我的领域模型。

 

小贴士:

类图中的类将具有属性、关联和方法。在这个阶段,我们更感兴趣的是属性和关联。这是因为属性和关联告诉我们类持有什么样的数据,而方法告诉我们类支持什么样的操作。为了将类保存在适当的数据库表中,您只需要知道类保存的是什么类型的数据。出于这个原因,我将集中讨论域模型中类的属性和关联。

让我们开始构建类图。让我们从表示雇员的类开始。

 

4.1 Employee

Employee类表示一个雇员,至少应该包含以下信息: 

First name

Last name

•电子邮件地址 

•出生日期 

•加入日期 

•地址 

•密码

4.2 休假

Leave类表示员工获得的休假福利。Leave类可以保存以下信息:

•名称 

•描述 

•权利 

•剩余 

•休假类型(带薪、病假或无薪)

4.3季票贷款

这是另一个员工福利。这个类可以保存以下信息:

•名称 

•描述 

•数量 

•开始日期 

•结束日期 

•按月付款

4.4技能提升经费

技能增强津贴津贴津贴。这个类应该有以下信息:

•名称 

•描述 

•权利 

•剩余权利

 

接下来,我们可以开始考虑这些类之间的关系或关联。根据我们所掌握的知识,可以得出以下联想:

•每个员工都有权享受不同类型的休假

•每个员工都可以借季票

•每位员工都有资格获得技能提升培训

•以上三个福利类都有名称描述属性,它们可以移动到一个公共基类中

•员工的地址不是基本类型,可以用类表示

•休假类型(带薪、无薪、病假)可以用枚举表示

 

这涵盖了我们开始旅程所需要的主要内容。下面的类图将前面的所有信息组合在一起:

 

 

 如果您仔细观察,就会发现我在Address类上添加了一些属性,以赋予它某种形状。我还向Employee类添加了一个名为IsAdmin的属性。此属性标识具有管理权限的员工。

我还添加了一个东西,即从Benefit类返回到Employee类的关联。这使得两个类之间的关联是双向的。这种双向关联的一个优点是,对于给定的福利类实例对象,很容易确定该实例属于哪个员工。

 

 

5.添加一些代码

现在是实现前面类图中的所有类并查看我们的整个源代码和项目结构的时候了。

让我们打开Visual Studio并创建一个名为employeebenefits .sln的空白解决方案。在这个解决方案中,让我们添加一个名为Domain的类库工程。

现在我们将把所有类添加到Domain领域工程中。为了尽量减少编译错误,我们将从不引用其他类的类开始。在Visual Studio中右键单击域项目并添加一个名为Address的类。使用下面的代码为类的主体:

 1 namespace Domain
 2 {
 3     public class Address
 4     {
 5         public string AddressLine1 { get; set; }
 6         public string AddressLine2 { get; set; }
 7         public string Postcode { get; set; }
 8         public string City { get; set; }
 9         public string Country { get; set; }
10     }
11 }        

让我们添加第二个名为Benefit的类,并使用下面的代码片段:这个类充当表示员工福利的任何类的基类:

1 namespace Domain
2 {
3     public class Benefit
4     {
5         public string Name { get; set; }
6         public string Description { get; set; }
7         public Employee Employee { get; set; }
8     }
9 }

接下来,我们可以添加表示三个福利的类。在这之前,定义休假枚举如下:

1 namespace Domain
2 {
3     public enum LeaveType
4     {
5         Casual,  //事假
6         Sick,    //病假
7         Unpaid   //无薪假
8     }
9 }

有了枚举后,下面是我们的三个福利类的代码片段:

 1 namespace Domain
 2 {
 3     public class SkillsEnhancementAllowance : Benefit
 4     {
 5         public int RemainingEntitlement { get; set; }
 6         public int Entitlement { get; set; }
 7     }
 8 }
 9 
10 namespace Domain
11 {
12     public class SeasonTicketLoan : Benefit
13     {
14         public int Amount { get; set; }
15         public double MonthlyInstalment { get; set; }
16         public DateTime StartDate { get; set; }
17         public DateTime EndDate { get; set; }
18     }
19 }
20 
21 namespace Domain
22 {
23     public class Leave : Benefit
24     {
25         public LeaveType Type { get; set; }
26         public int AvailableEntitlement { get; set; }
27         public int RemainingEntitlement { get; set; }
28     }
29 }

除了表示employee的类之外,我们已经准备好了所有的类。让我们再添加一个名为Employee的类,并使用以下代码片段:

 1 namespace Domain
 2 {
 3     public class Employee
 4     {
 5         public string EmployeeNumber { get; set; }
 6         public string Firstname { get; set; }
 7         public string Lastname { get; set; }
 8         public string EmailAddress { get; set; }
 9         public DateTime DateOfBirth { get; set; }
10         public DateTime DateOfJoining { get; set; }
11         public Address ResidentialAddress { get; set; }
12         public bool IsAdmin { get; set; }
13         public string Password { get; set; }
14         public ICollection<Benefit> Benefits { get; set; }
15     }
16 }

注意,我们在Employee类中有一组Benefit类对象。并不是每个员工都有资格获得每种类型的福利。所以,我们不能把所有员工的福利类型都联系起来。当你事先不知道某一雇员有资格领取哪一种福利时,福利的集合可以帮助你解决这种情况。

接下来,我们将为单元测试添加另外一个工程。因此,让我们添加另一个类库工程,并将其命名为Tests.Unit。此工程需要引用Domain工程。我们将使用NUnit来编写测试。让我们用NuGet获取NUnit。

 

6.将NuGet包添加到项目中

有两种方法可以将NuGet包添加到项目中。第一个使用Visual Studio的管理NuGet包菜单。这是一个容易操作的选项,您不需要离开Visual Studio。第二种方法使用Visual Studio的包管理器控制台特性。这有点复杂,但可以更好地控制正在安装的包。我们将在这里查看第一个选项。我将留下第二个选择供你们自己探索:

1. 右键单击要向其添加NuGet包的项目。在我们的例子中,这将是名为Tests.Unit的项目。您将看到以下菜单弹出:

 

 

2. 在上面的菜单中,点击管理NuGet包…在前面的截图中用黄色矩形高亮显示。这将引出管理测试的NuGet包对话框。单位的项目。对话框是这样的:

 

 

在这个对话框的右上角有一个搜索框。我们可以在这里输入我们想要搜索的包的名称。

在左侧窗格中,您可以看到各种选项。我不打算详细介绍这些内容,但值得注意的是,nuget.org被选中了。这意味着我们使用nuget.org作为NuGet包的来源。如果使用的是不同的源,那么可以从设置中更改此设置,但很少需要这样做。

3.接下来,我们在搜索框中输入NUnit, NUnit出现在结果列表的顶部:

 

 

 4. 接下来,我们单击出现在NUnit旁边的Install按钮。这将在我们的Tests.Unit工程中安装NUnit。成功安装NUnit后,管理NuGet包对话框变化如下图所示:

 

 

 5. 注意NUnit旁边的绿色标记,它告诉我们在Tests.Unit工程中安装了这个包。

 

7.回到我们的测试

如果你从未使用过NUnit,这里有一个简短的入门:

1. 每个单元测试都是作为一种方法编写的。

2. 特性[Test]被添加到测试方法中。这就是NUnit区分测试方法和普通方法的方式。

3.具有一个或多个测试方法的任何类都可以选择具有一个特性[TestFixture]添加到它。这就是NUnit GUI runner(或其他NUnit runners)寻找所有要运行的测试类/功能的方式。但是,对于最新版本的ReSharper,这个属性是不需要的。

4. 最后也是最重要的,使用语句引用NUnit.Framework名称空间应该添加到任何具有测试方法的类中。

下面是一个单元测试代码:

 1 using NUnit.Framework
 2 namespace Tests.Unit
 3 {
 4     [TestFixture]
 5     public class EmployeeTests
 6     {
 7         [Test]
 8         public void EmployeeIsEntitledToPaidLeaves()
 9         {
10             //Arrange 安排
11             var employee = new Employee();
12             
13             //Act 行动
14             employee.Leaves = new List<Leave>();
15         
16             employee.Leaves.Add(new Leave
17             {
18                 Type = LeaveType.Paid,
19                 AvailableEntitlement = 15
20             });
21             
22             //Assert 断言
23             var paidLeave = employee.Leaves.FirstOrDefault(l => l.Type == LeaveType.Paid);
24             Assert.That(paidLeave, Is.Not.Null);
25             Assert.That(paidLeave.AvailableEntitlement, Is.EqualTo(15));
26         }
27     }
28 }

这不是一个有意义的测试。目的是向您展示如何用NUnit编写测试。为了更好地理解它,让我带你完成这个测试。单元测试通常有三个部分——Arrange, Act, and Assert 安排、行动和断言。您可以在前面的测试中看到这些注释。

在安排部分中,您设置了运行测试所需的所有内容。在我们的示例中,我们创建了Employee类的一个新实例。

在行动部分中,我们执行要测试的代码。在前面的测试中,我们将一个Leave对象添加到Employee类的休假集合中。这个Leave对象属于Paid类型,权限为15。

在断言部分中,我们验证要测试的代码是否行为正确。在前面的测试中,我们通过从类型为Paid的休假列表中查找Leave对象来验证这一点。我们发现这样一个对象的确出现在列表中,并且AvailableEntitlement的值为15。

这很简单,不是吗?如果这是你第一次看单元测试,不要担心。我们将会看到很多这样的东西,我们会一起到达那里。

 

 

 

8.本章小结

当你在学习像NHibernate这样庞大的软件库时,很容易阅读手册时迷失。使用一个库来构建一个端到端的应用程序更加吸引人,并且提供了不同层次的学习体验。虽然我在这一章中提出的问题陈述听起来不是很现代,但它确实给了我们一个学习NHibernate一些重要特性的机会。花些时间在头脑中提炼出问题是很重要的。

 

开发人员在工作中真正的朋友是他/她的开发环境、他/她使用的工具和他/她遵循的方法。虽然敏捷已经变得司空见惯,但我知道TDD并不是司空见惯。在这本书中,我们将在某种程度上使用这两种方法。然而,正如我前面所说的,我将把TDD方面控制在完善手头工作所需的水平。我们的重点是使用NHibernate构建数据访问层。

 

我们已经了解了NHibernate的能力,我希望你能兴奋地了解更多。好吧,我们即将开始NHibernate之旅。翻过这一页,了解更多关于NHibernate最重要和最基本的功能,它可以让我们将我们在本章中定义的领域模型映射为一个合适的数据库结构(表)。

 

posted on 2020-07-11 16:48  困兽斗  阅读(191)  评论(0)    收藏  举报

导航