正如我们在第一章中提到的,酱汁是一种由蛋黄和黄油制成的乳化酱汁,但这并不能神奇地灌输给你制造酱汁的能力。学习的最好方法是实践,但一个例子往往能弥合理论和实践之间的鸿沟。在你自己尝试之前,看一个专业的厨师做一个沙司是很有帮助的。
当我们在上一章介绍依赖注入(DI)时,我们介绍了一个高级教程,以帮助您理解其目的和一般原则。但是这个简单的解释对DI来说是不公平的。DI是实现松散耦合(Loose Coupling)的一种方法,松散耦合(Loose Coupling)首先是处理复杂性的一种有效方法。
大多数软件都很复杂,因为它必须同时处理许多问题。除了业务关注点本身可能很复杂之外,软件还必须解决与安全性、诊断、操作、性能和可扩展性相关的问题。松散耦合(Loose Coupling)鼓励您分别解决每个问题,而不是在一个大泥球中解决所有这些问题。单独解决每一个问题比较容易,但最终,您仍必须将这组复杂的问题组合到单个应用程序中。
在本章中,我们将看一个更复杂的例子。您将看到编写紧密耦合的代码是多么容易。您还将和我们一起分析为什么紧密耦合(tightly coupled)的代码从可维护性的角度来看是有问题的。在第3章中,我们将使用DI将这个紧密耦合(tightly coupled)的代码基完全重写为松散耦合(Loose Coupling)的代码基。如果您想马上看到松散耦合(Loose Coupling)的代码,您可能想跳过这一章。如果没有,当您读完本章后,您应该开始理解是什么使得紧密耦合(tightly coupled)的代码如此成问题。
构建紧密耦合的应用程序(Building a tightly coupled application)
构建松散耦合(Loose Coupling)代码的想法并不是特别有争议,但是理论和实践之间存在巨大的差距。在我们在下一章向您展示如何使用DI构建松散耦合(Loose Coupling)的应用程序之前,我们想向您展示它是多么容易出错。松散耦合(Loose Coupling)代码的常见尝试是构建分层应用程序。任何人都可以绘制一个三层应用程序图,图2.1也证明了这一点。
绘制三层图看起来很简单,但是绘制图的行为类似于声明您的牛排要有酱汁蛋黄酱:这是一份意向声明,对最终结果没有任何保证。 您可能会遇到其他问题,正如您很快就会看到的那样。
查看和设计灵活且可维护的复杂应用程序的方法不止一种,但是n层应用程序体系结构构成了一种众所周知的、经过测试的方法。挑战在于如何正确实施。使用如图2.1所示的三层图,您可以开始构建应用程序。
图2.1 标准三层应用程序体系结构。这是n层应用程序体系结构的最简单和最常见的变体,其中应用程序由n个不同的层组成。 |
---|
认识玛丽·罗恩(Mary Rowan)
Mary Rowan是一名专业的.NET开发人员,为一家本地认证的Microsoft合作伙伴工作,该合作伙伴主要开发web应用程序。她今年34岁,从事软件工作11年。这使她成为该公司经验丰富的开发人员之一。除了履行作为高级开发人员的常规职责外,她还经常担任初级开发人员的导师。总的来说,玛丽对自己所做的工作很满意,但里程碑常常被错过,这让她很沮丧,迫使她和同事们加班加点,周末加班,以赶在最后期限前完成工作。
她怀疑必须有更有效的方法来构建软件。为了学习效率,她购买了很多编程书籍,但是她很少有时间阅读它们,因为她的业余时间大部分都花在了丈夫和两个女孩身上。 玛丽喜欢在山上远足。 她还是一名热情的厨师,而且她绝对知道如何制作真正的酱汁贝娜酱。
已要求Mary在ASP上创建一个新的电子商务应用程序。.NET Core MVC
和Entity Framework Core
,其中SQL Server
作为数据存储。 为了最大化模块化,它必须是三层应用程序。
要实现的第一个功能应该是从数据库表中提取并在网页上显示的特色产品的简单列表(示例如图2.2所示)。 并且,如果查看列表的用户是首选客户,则所有产品的价格应打折5%。
图2.2 Mary被要求开发的电子商务web应用程序的屏幕截图。它提供了一个简单的特色产品及其价格列表。 |
---|
为了完成第一个功能,Mary必须实现以下功能:
- 数据层(A data layer)—包括数据库中的
Products
表(表示所有数据库行)和Product
类(表示单个数据库行) - 领域层(A domain layer)—包含检索特色产品的逻辑
- 具有MVC控制器的UI层(A UI Layer with an MVC controller)—处理传入的请求,从域层检索相关数据,并将其发送到Razor视图,最终呈现特色产品列表
让我们回顾一下Mary实现应用程序第一个特性的过程。
创建数据层(Creating the data layer)
因为Mary需要从数据库表中提取数据,所以她决定从实现数据层开始。第一步是定义数据库表本身。Mary使用SQL Server Management Studio
创建表2.1所示的表。
表2.1 Mary创建了包含以下列的Products表。
列名 | 数据类型 | 允许为空 | 主键 |
---|---|---|---|
Id | uniqueidentifier | No | Yes |
Name | nvarchar(50) | No | No |
Description | nvarchar(max) | No | No |
UnitPrice | money | No | No |
IsFeatured | bit | No | No |
为了实现数据访问层,Mary在她的解决方案中添加了一个新库。下面的列表显示了她的产品类。
清单2.1 Mary的Product类
public class Product
{
public Guid Id { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public decimal UnitPrice { get; set; }
public bool IsFeatured { get; set; }
}
Mary使用实体框架来满足她的数据访问需求。她把依赖添加到Microsoft.EntityFrameworkCore.SqlServer
文件NuGet
包,并实现一个特定于应用程序的DbContext
类,该类允许她的应用程序通过CommerceContext
类访问Products
表。下面的列表显示了她的CommerceContext
类。
清单2.2 Mary的
CommerceContext
类
public class CommerceContext : Microsoft.EntityFrameworkCore.DbContext
{
public DbSet<Product> Products { get; set; } <--- 在基础数据库的Product表上启用查询
protected override void OnConfiguring(
DbContextOptionsBuilder builder) <-- 调用创建的上下文的每个实例,以对其进行配置
{
var config = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json")
.Build(); <--- 加载配置文件(类似于清单1.2中的内容)
string connectionString = config.GetConnectionString( "CommerceConnectionString");
builder.UseSqlServer(connectionString);
<-- 从配置文件中读取连接字符串,并将其应用于DbContextOptionsBuilder。 这可以使用配置的连接字符串有效地配置应用程序的CommerceContext。
}
}
因为CommerceContext
从配置文件加载连接字符串,所以需要创建该文件。玛丽添加了一个名为appsettings.json
文件她的网络项目,包括以下内容:
{
"ConnectionStrings": {
"CommerceConnectionString":
"Server=.;Database=MaryCommerce;Trusted_Connection=True;"
}
}
Entity Framework Core 快速入门
Entity Framework Core是Microsoft的对象/关系映射器,简称
ORM
。 它弥合了关系数据库模型和面向对象的代码(如C#)之间的鸿沟。 它使开发人员可以在更高的抽象层次上工作,因为我们自己没有编写SQL查询:Entity Framework Core
将为我们完成从C#到SQL
的转换。实体框架的中心类是
DbContext
。DbContext
类是一个工作单元。1一个工作单元由单个业务事务所需的对象的本地缓存组成。DbContext
允许访问数据库中的数据,例如Products表中的示例。如果您不熟悉
Microsoft Entity Framework
,请不要担心。 在这种情况下,数据访问实施的细节不是那么重要,因此即使您更加熟悉其他数据访问技术,您也应该能够按照该示例进行操作。
警告
CommerceContext
从配置文件加载连接字符串-这是一个陷阱。它使每个新的CommerceContext
读取配置文件,即使在应用程序运行时配置文件通常不会更改。CommerceContext
不应包含硬编码的连接字符串,但也不应从配置系统加载配置值。第2.3.3节对此进行了讨论。
CommerceContext
和Product
是包含在同一程序集中的公共类型。Mary知道她以后需要向她的应用程序添加更多的特性,但是实现第一个特性所需的数据访问组件现在已经完成(图2.3)。
图2.3 Mary在实现图2.1中设想的分层体系结构方面取得了多大进展。 |
---|
既然已经实现了数据访问层,下一个逻辑步骤就是领域层(domain layer)。领域层也称为领域逻辑层、业务层或业务逻辑层。领域逻辑是应用程序需要具有的所有行为,特定于为其构建应用程序的域。
创建领域层(Creating the domain layer)
除了纯数据报告应用程序外,总是存在域逻辑。 您可能一开始可能并没有意识到它,但是当您了解该领域时,它的内在和隐含规则和假设将逐渐浮出水面。 在没有任何域逻辑的情况下,可以从技术上直接从UI
层使用CommerceContext
公开的产品列表。
警告 在
UI
或数据访问层中实现域逻辑将导致痛苦。帮你自己一个忙,从一开始就创建一个领域层。
Mary的申请要求规定,应向首选客户显示产品标价,并提供5%的折扣。Mary还没有弄清楚如何确定一个喜欢的客户,所以她向同事Jens寻求建议:
Mary:我需要实现这个业务逻辑,这样一个首选客户就可以得到5%的折扣。
Jens: 听起来很简单。乘以0.95。
Mary:谢谢,但那不是我想问你的。我想问你的是,我应该如何确定一个首选客户?
Jens: 我懂了。这是web应用程序还是桌面应用程序?
Mary:这是一个网络应用程序。
Jens: 好的,然后可以使用HttpContext
的User
属性检查当前用户是否处于PreferredCustomer
角色。
Mary:慢点,Jens。此代码必须在领域层中。这是一个图书馆。没有HttpContext
。
Jens: 哦。[思考了一会儿]我仍然认为您应该使用ASP.NET
查找用户的值。然后可以将该值作为布尔值传递给域逻辑。
Mary:我不知道。。。
Jens: 这也将确保您有良好的关注点分离,因为您的域逻辑不必处理安全性问题。你知道,单一责任原则!这是一种敏捷的方法!
Mary:我想你说得有道理。
Jens的建议基于他对ASP.NET
的技术知识。 由于讨论使他离开了自己的舒适区,他用三句话组合了Mary,使他蒸蒸日上。 请注意,Jens不知道他在说什么:
- 他误用了关注点分离的概念(He misuses the concept of Separation of Concerns.)。尽管将安全问题从领域逻辑中分离出来很重要,但将其移到表示层并不能帮助分离问题。
- 他之所以提到敏捷,是因为他最近听到别人热情地谈论敏捷。(He only mentions Agile because he recently heard someone else talk enthusiastically about it.)
- 他完全没有抓住单一责任原则的要点(He completely misses the point of the Single Responsibility Principle)。尽管敏捷方法提供的快速反馈周期可以帮助您相应地改进软件设计,但是作为软件设计原则的单一责任原则(Single Responsibility Principle)本身独立于所选择的软件开发方法。
单一责任原则(Single Responsibility Principle)
正如第一章所讨论的,单一责任原则(SRP)规定每个类应该只有一个单一的责任,或者更准确地说,一个类应该只有一个改变的理由。
如果我们将
SQL
语句放在包含HTML
标记的视图中,我们都会很快同意,与更改SQL
语句相比,对标记的更改将在不同的时间,以不同的速率发生,并且出于不同的原因。 当我们更改数据模型或需要进行性能调整时,我们的SQL
语句也会更改。 另一方面,当我们需要更改Web应用程序的外观时,我们的标记也会更改。 这些是由于不同原因而改变的不同关注点。 因此,将SQL
语句直接放入视图是违反SRP
的。但是,查看一个班级是否有多种变更理由通常会更具挑战性。 经常有帮助的是从代码内聚的角度来看待
SRP
。 内聚性定义为类或模块的元素的功能相关性。 关联性越低,凝聚力越低,类别违反SRP
的风险越高。能够检测到
SRP
违规是一回事,但是确定是否应该解决违规则是另一回事。 如果没有症状,则应用SRP
是不明智的。 不必要地拆分不会引起可维护性问题的类会增加额外的复杂性。 软件设计的诀窍是管理复杂性。
利用Jens糟糕的建议,Mary创建了一个新的C#库项目,并添加了一个名为ProductService
的类,如清单2.3所示。要使ProductService
类编译,她必须添加对数据访问层的引用,因为CommerceContext
类是在那里定义的。
清单2.3 Mary的
ProductService
类
public class ProductService
{
private readonly CommerceContext dbContext;
public ProductService()
{
this.dbContext = new CommerceContext(); <--- 创建一个新的CommerceContext实例供以后使用
}
public IEnumerable<Product> GetFeaturedProducts(
bool isCustomerPreferred)
{
decimal discount = isCustomerPreferred ? .95m : 1;
var featuredProducts =
from product in this.dbContext.Products
where product.IsFeatured
select product; <----- 从数据库中获取所有产品,并按特色产品过滤
return
from product in
featuredProducts.AsEnumerable()
select new Product
{
Id = product.Id,
Name = product.Name,
Description = product.Description,
IsFeatured = product.IsFeatured,
UnitPrice =
product.UnitPrice * discount
}; <---- 根据给定客户的折扣百分比创建折扣产品列表
}
}
Mary很高兴她在ProductService
类中封装了数据访问技术((Entity Framework Core)、配置和域逻辑。她通过传入isCustomerPreferred
参数将用户的知识委托给了调用者,并使用此值计算所有产品的折扣。
进一步的改进可能包括用可配置的数字替换硬编码的折扣值(.95),但是就目前而言,此实现就足够了。 玛丽快完成了。 唯一剩下的就是UI
层。 玛丽决定可以等到明天。 图2.4显示了Mary在实现图2.1中设想的体系结构方面走了多远。
图2.4与图2.3相比,Mary现在实现了数据访问层和领域层。UI 层仍有待实现。 |
---|
Mary没有意识到的是,通过让ProductService
依赖于数据访问层的CommerceContext
类,她将领域层与数据访问层紧密耦合。我们将在第2.2节中解释这有什么问题。
创建UI层(Creating the UI layer)
第二天,Mary继续使用电子商务应用程序,将新的ASP.NET Core MVC
应用程序添加到她的解决方案中。如果您不熟悉ASP.NET Core MV
C框架,请不要担心。MVC
框架如何运作的复杂细节不是本讨论的重点。重要的部分是如何使用依赖关系,这是相对平台无关的主题。
ASP.NET Core MVC 快速入门
ASP.NET Core MVC
的名称来自模型-视图-控制器设计模式(Model View Controller design pattern)。在这种情况下,最重要的是要理解的是,当web请求到达时,控制器处理请求,可能使用(领域)模型来处理它,并形成最终由视图呈现的响应。控制器通常是派生自抽象
Controller
类的类。它有一个或多个处理请求的操作方法;例如,HomeController
类通常有一个名为Index
的方法来处理对默认页的请求。当动作方法返回时,它通过ViewResult
实例将结果模型传递给视图。
下一个清单显示Mary如何在她的HomeController
类上实现Index
方法,从数据库中提取特色产品并将其传递给视图。要编译此代码,她必须同时向数据访问层和领域层添加引用。这是因为ProductService
类是在域层定义的,而Product
类是在数据访问层定义的。
清单2.4 默认controller类的Index方法
public ViewResult Index()
{
bool isPreferredCustomer =this.User.IsInRole("PreferredCustomer"); <---- 确定客户是否是首选客户
var service = new ProductService(); <--- 从领域层创建ProductService
var products = service.GetFeaturedProducts(isPreferredCustomer); <--- 从ProductService获取特色产品列表(在数据访问层中定义)
this.ViewData["Products"] = products; <-----将产品列表存储在控制器的通用ViewData词典中,以供视图稍后使用
return this.View();
}
作为ASP.NET Core MVC
生命周期的一部分,将使用正确的用户对象自动填充HomeController
类的User
属性,因此Mary使用它来确定当前用户是否是首选客户。有了这些信息,她就可以调用域逻辑来获取特色产品列表。
注意 当Mary创建领域层时,她再次创建了紧密耦合的代码。在这种情况下,
HomeController
与ProductService
紧密耦合。如果ProductService
是一个稳定的依赖关系,这也不会太糟糕,但是正如您在第1章中了解到的,ProductService
是一个不稳定的依赖关系。它是不稳定的,因为它引入了建立和配置关系数据库的需求。
在Mary的应用程序中,产品列表必须由Index
视图呈现。下面的列表显示了视图的标记。
清单2.5 Index 视图标记
<h2>Featured Products</h2>
<div>
@{
var products =
(IEnumerable<Product>)this.ViewData["Products"]; <-- 获取控制器填充的产品
foreach (Product product in products) <--- 循环浏览产品,设置其单价格式,并将其呈现为HTML
{
<div>@product.Name (@product.UnitPrice.ToString("C"))</div>
}
}
</div>
ASP.NET Core MVC
允许您编写标准HTML,其中嵌入了命令性代码,以访问由创建视图的控制器创建和分配的对象。 在这种情况下,HomeController
的Index
方法将特色产品列表分配给名为Mary的键,该键在Mary的视图中用于呈现产品列表。 图2.5显示了Mary现在如何实现图2.1中设想的体系结构。
图2.5 Mary现在已经实现了应用程序中的所有三个层。 |
---|
有了这三个层,应用程序理论上应该可以工作。但只有运行应用程序,她才能验证是否是这样。
评估紧耦合应用程序(Evaluating the tightly coupled application)
玛丽现在已经实现了所有三层,因此现在该看看该应用程序是否有效。她按F5键,将显示如图2.2所示的网页。 特色产品功能现已完成,Mary充满信心并准备在应用程序中实现下一个功能。毕竟,她遵循既定的最佳实践并创建了一个三层应用程序……还是吗?
Mary是否成功开发了适当的分层应用程序?不,她没有,尽管她当然有最好的意图。她创建了三个Visual Studio项目,它们与计划的体系结构中的三个层相对应。对于不经意的观察者来说,这看起来像是令人垂涎的分层体系结构,但是,正如您将看到的那样,代码是紧密耦合的。
通过Visual Studio,可以轻松自然地以这种方式使用解决方案和项目。如果需要其他库中的功能,则可以轻松地添加对它的引用,并编写代码来创建其他库中定义的类型的新实例。 但是,每次添加引用时,您就需要依赖。
评估依赖关系图(Evaluating the dependency graph)
在Visual Studio中使用解决方案时,很容易忘记重要的依赖关系。这是因为Visual Studio将它们与可能指向.NET基本类库(BCL)中的程序集的所有其他项目引用一起显示。为了了解Mary的应用程序中的模块之间如何相互联系,我们可以绘制依赖关系图(见图2.6)。
从图2.6中获得的最显着的见解是UI
层同时依赖于领域层和数据访问层。在某些情况下,UI
似乎可以绕过领域层。 这需要进一步调查。
图2.6 Mary应用程序的依赖关系图,显示了模块之间的相互依赖关系。箭头指向模块的依赖关系。 |
---|
评估可组合性(Evaluating composability)
构建三层应用程序的主要目标是关注点分离(Separation Of Concerns)。我们希望将领域模型与数据访问层和UI
层分开,这样就不会有任何问题污染领域模型。在大型应用程序中,必须能够独立地处理应用程序的每个区域为了评估Mary的实现,我们可以问一个简单的问题:是否可以单独使用每个模块?
理论上,我们应该能够以任何我们喜欢的方式组合模块。我们可能需要编写新的模块,以新的和意想不到的方式将现有模块绑定在一起,但是,理想情况下,我们应该能够这样做,而不必修改现有模块。我们能用新的和令人兴奋的方式使用玛丽应用程序中的模块吗?让我们看看一些可能的情况。
注意 下面的分析讨论了是否可以替换模块,但请注意,这是我们用来评估可组合性的技术。即使我们永远不想交换模块,这种分析也揭示了关于耦合的潜在问题。如果我们发现代码是紧密耦合的,那么松散耦合(Loose Coupling)的所有好处都将丢失。
构建新UI (Building a new UI)
如果Mary的应用程序获得成功,项目涉众希望她在Windows Presentation Foundation(WPF)中开发富客户机版本。在重用域和数据访问层时是否可以这样做?
当我们检查图2.6中的依赖关系图时,我们可以快速确定没有模块依赖于Web UI
,因此可以将其删除并替换为WPF UI
。 基于WPF
创建富客户端是一个新的应用程序,它与原始Web
应用程序共享其大部分实现。图2.7说明了WPF
应用程序将如何需要与Web
应用程序具有相同的依赖关系。原始Web
应用程序可以保持不变。
图2.7 用WPF UI 替换web UI是 可能的,因为没有模块依赖于web UI 。虚线框表示我们要更换的零件。 |
---|
Mary的实现当然可以替换UI
层。 让我们研究另一个有趣的分解。
构建新的数据访问层(Building a new data access layer)
Mary的市场分析师认为,为了优化利润,她的应用程序应该作为一个托管在Microsoft Azure.上的云应用程序。在Azure中,数据可以存储在高度可扩展的Azure表存储服务中。这种存储机制基于包含无约束数据的灵活数据容器。该服务不强制执行特定的数据库模式,并且没有引用完整性。
尽管.NET上最常见的数据访问技术是基于ADO.NET
数据服务,用于与表存储服务通信的协议是HTTP
。这种类型的数据库有时被称为键值数据库(key-value database),它与通过Entity Framework Core
访问的关系数据库不同。
要使电子商务应用程序成为云应用程序,必须用使用表存储服务的模块替换数据访问层。这可能吗?
从图2.6中的依赖关系图中,我们已经知道UI
层和域层都依赖于基于实体框架的数据访问层。 如果我们尝试删除数据访问层,则该解决方案将在不重构所有其他项目的情况下不再编译,因为缺少必需的依赖项。 在具有数十个模块的大型应用程序中,我们还可以尝试删除未编译的模块以查看剩余的内容。 对于Mary的应用程序,很明显,我们必须删除所有模块,而没有留下任何东西,如图2.8所示。
尽管可以开发一个模仿原始数据访问层公开的API
的Azure Table数据访问层,但是我们无法在不接触应用程序其他部分的情况下将其应用于应用程序。 该应用程序的可组合性几乎不如项目涉众所希望的那样。 启用利润最大化的云功能需要对应用程序进行重大重写,因为任何现有模块都无法重复使用。
图2.8 尝试替换关系数据访问层 |
---|
翻译
The domain layer depends on the relational data access layer.
领域层依赖于关系数据访问层。
Attempting to remove the relational data access layer leaves nothing left because all other layers depend on it. You'll have to replace all three layers together. There’s no place where you can instruct the domain layer to use the new Azure Table data access layer instead of the original
尝试删除关系数据访问层时,没有留下任何内容,因为所有其他层都依赖于它。你得把这三层都换掉。没有地方可以指示域层使用新的Azure表数据访问层而不是原始的
评估其他组合方式(Evaluating other combinations)
我们可以对应用程序进行模块组合的其他分析,但这将是一个有争议的问题,因为我们已经知道它无法支持重要的方案。 此外,并非所有组合都有意义。
例如,我们可能会问是否有可能用其他实现来替换领域模型。 但是,在大多数情况下,这是一个奇怪的问题,因为领域模型封装了应用程序的核心。 没有领域模型,大多数应用程序就没有理由存在。
缺失可组合性分析(Analysis of missing composability)
为什么Mary的实现没有达到预期的可组合性程度?是因为UI
直接依赖于数据访问层吗?让我们更详细地研究一下这种可能性。
依赖图分析(Dependency graph analysis)
为什么UI
依赖于数据访问库?罪魁祸首是此领域模型的方法签名:
ProductService
类的GetFeaturedProducts
方法返回一系列产品,但Product
类是在数据访问层中定义的。任何使用GetFeaturedProducts
方法的客户端都必须引用数据访问层才能编译。可以更改方法的签名以返回在域模型中定义的类型。它也会更正确,但它不能解决问题.
假设我们打破了UI
和数据访问库之间的依赖关系。修改后的依赖关系图如图2.9所示。
这样的更改是否使Mary可以用封装对Azure Table
服务的访问的层替换关系数据访问层? 不幸的是,没有,因为领域层仍然依赖于数据访问层。 反过来,UI
仍然取决于领域模型。 如果我们尝试删除原始数据访问层,则该应用程序将一无所有。 问题的根本原因在于其他地方。
图2.9 移除UI 对数据访问层的依赖关系的假设情况的依赖关系图 |
---|
数据访问接口分析(Data access interface analysis)
领域模型取决于数据访问层,因为整个数据模型都在此定义。 使用实体框架来实现数据访问层可能是一个合理的决定。 但是,从松散耦合(Loose Coupling)的角度来看,不是直接在领域模型中使用它。
可以在ProductService
类中找到有问题的代码。 构造函数创建CommerceContext
类的新实例,并将其分配给私有成员变量:
this.dbContext = new CommerceContext();
这将ProductService
类与数据访问层紧密耦合(tightly coupled)。没有合理的方法可以截取这段代码并用其他代码替换它。对数据访问层的引用被硬编码到ProductService
类中!
GetFeaturedProducts
方法的实现使用CommerceContext
从数据库中提取产品对象:
var featuredProducts =
from product in this.dbContext.Products
where product.IsFeatured
select product;
GetFeaturedProducts
中对CommerceContext
的引用加强了硬编码的依赖性,但此时,已经造成了损害。我们需要的是一种更好的方法来组合模块,而不需要这种紧密耦合(tightly coupled)。如果您回顾第一章中讨论的DI的好处,您会发现Mary的应用程序没有以下功能:
- 后期绑定(Late binding)(Late binding)—由于领域层与数据访问层紧密耦合(tightly coupled),因此无法部署同一应用程序的两个版本,其中一个连接到本地
SQL Server
数据库,另一个使用Azure
表存储托管在Microsoft Azure
上。换句话说,不可能使用后期绑定(Late binding)加载正确的数据访问层。 - 可扩展性(Extensibility)—因为应用程序中的所有类都彼此紧密耦合(tightly coupled),所以插入横切关注点(Cross-Cutting Concerns)(如第1章中的安全特性)会变得非常昂贵。这样做需要更改系统中的许多类。因此,这种紧密耦合(tightly coupled)的设计并不是特别可扩展的。
- 并行开发(Maintainability)—如果我们继续使用前面的应用横切关注点(Cross-Cutting Concerns)的示例,那么很容易理解,必须在整个代码库中进行彻底的更改会妨碍在单个应用程序上与多个开发人员并行工作的能力。与我们一样,在过去将工作提交到版本控制系统时,您可能也处理过痛苦的合并冲突。一个设计良好、松散耦合(Loose Coupling)的系统将减少合并冲突的数量。当越来越多的开发人员开始开发Mary的应用程序时,要想有效地工作就变得越来越难了,而不需要互相干涉。
- 可测试性(Parallel development)—我们已经确定,交换数据访问层目前是不可能的。但是,在没有数据库的情况下测试代码是进行单元测试(Unit testing)的先决条件。但是,即使使用集成测试,Mary也可能需要将代码的某些部分替换掉,而当前的设计使得这一点变得困难。因此,Mary的申请是不可测试的。
此时,您可能会问自己,所需的依赖图应该是什么样。 对于最高程度的重用,需要最少的依赖关系。 另一方面,如果根本没有依赖关系,该应用程序将变得毫无用处。
您需要哪些依赖项以及它们应该指向什么方向取决于需求。但是,因为我们已经确定,我们无意用完全不同的实现替换领域层,所以可以安全地假设其他层可以安全地依赖它。图2.10为松散耦合(Loose Coupling)的应用程序提供了一个大的扰流器,您将在下一章中编写它,但它确实显示了所需的依赖关系图。
图2.10 所需情况的依赖关系图 |
---|
翻译
The data access layer now depends on the domain layer instead of the other way around.
数据访问层现在依赖于领域层,而不是相反。
The UI layer only depends on the domain layer.
UI层仅依赖于领域层。
该图显示了如何反转领域层和数据访问层之间的依赖关系。我们将在下一章中详细介绍如何做到这一点。
其他杂项问题(Miscellaneous other issues)
我们想指出Mary的代码中应该解决的一些其他问题。
-
大多数域模型似乎是在数据访问层实现的(Most of the domain model seems to be implemented in the data access layer.)。领域层引用数据访问层是一个技术问题,而数据访问层定义一个类作为产品类则是一个概念问题。公共产品类属于领域模型。
-
在Jens的建议下,Mary决定在UI中实现确定用户是否是首选客户的代码。但是如何将客户识别为首选客户是业务逻辑的一部分,因此应该在领域模型中实现(On Jens'advice, Mary decided to implement in the UI the code that determines whether a user is a preferred customer.)。Jens关于关注点分离和单一责任原则的论点不是将代码放在错误位置的借口。在单个库中遵循单一责任原则是完全可能的-这是预期的方法。
-
Mary从CommerceContext类中的配置文件中加载了连接字符串(如清单2.2所示)(Mary loaded the connection string from the configuration file from within the CommerceContext class (shown in listing 2.2).)。从消费者的角度来看,对这个配置值的依赖是完全隐藏的。正如我们在讨论清单2.2时提到的,这种含蓄性包含一个陷阱。
尽管配置已编译应用程序的能力很重要,但只有完成的应用程序才应该依赖配置文件。对于可重用库来说,由它们的调用者强制配置更为灵活,而不是自己读取配置文件。最后,最终调用方是应用程序的入口点。此时,所有相关的配置数据都可以在启动时直接从配置文件中读取,并根据需要提供给底层库。我们希望
CommerceContext
要求的配置是显式的。 -
视图(如清单2.5所示)似乎包含了太多的功能(The view (as shown in listing 2.5) seems to contain too much functionality.)。它执行强制转换和特定的字符串格式。这样的功能应该转移到底层模型。
结论(Conclusion)
编写紧密耦合(tightly coupled)的代码非常容易。 即使Mary出于写三层应用程序的明确意图,它也变成了一大堆的意大利苗条代码。(当我们谈论分层时,我们称其为Lasagna。)
编写紧密耦合(tightly coupled)的代码如此容易的众多原因之一是,语言功能和我们的工具已经使我们朝着这个方向发展。 如果需要对象的新实例,则可以使用new
关键字。 如果您没有对所需程序集的引用,则Visual Studio使其易于添加。 但是,每次使用new
关键字时,都会引入紧密耦合(tightly coupled)。 如第一章所述,并非所有紧密耦合(tightly coupled)都是不好的,但您应努力防止与过度性依赖项(Volatile Dependency)的紧密耦合(tightly coupled)。
到现在为止,您应该开始理解紧密耦合(tightly coupled)的代码如此成问题的根本原因,但是我们尚未向您展示如何解决这些问题。 在下一章中,我们将向您展示一种更可组合的方式来构建具有与Mary所构建功能相同的功能的应用程序。 我们还将同时解决第2.3.3节中讨论的其他问题。
总结
- 复杂的软件必须解决许多不同的问题,例如安全性,诊断,操作,性能和可扩展性。
- 松散耦合(Loose Coupling)鼓励您独立地处理所有应用程序关注点,但最终您仍然必须组成这组复杂的关注点。
- 创建紧密耦合(tightly coupled)的代码很容易。尽管并非所有的紧密耦合(tightly coupled)都是不好的,但是与过度性依赖项(Volatile Dependency)的紧密耦合是并且应该避免的。
- 在Mary的应用程序中,由于领域层依赖于数据访问层,因此无法用不同的数据访问层替换数据访问层。应用程序中的紧耦合导致Mary失去了松散耦合(Loose Coupling)所提供的好处:后期绑定(Late binding)、可扩展性、可维护性、可测试性和并行开发。
- 只有完成的应用程序应该依赖于配置文件。应用程序的其他部分不应该从配置文件请求值,而是应该由它们的调用者进行配置。
- 单一责任原则规定每一类人只有一个改变的理由。
- 单一责任原则可以从衔接的角度来看待。内聚性是指类或模块中元素之间的功能关联性。关联度越低,内聚度越低;内聚度越低,类违反单一责任原则的可能性越大。