伙计,我的业务逻辑在哪儿?






4.79/5 (162投票s)
多年来,我们已经从桌面应用发展到客户端服务器,再到三层架构、N层架构,直至面向服务。在这个过程中,许多事情都发生了变化,但许多习惯却保留了下来。本文讨论了我们正在犯的错误以及可能的解决方案。
目录
引言
多年来,我们已经从桌面应用发展到客户端服务器,再到三层架构、N层架构,直至面向服务。在这个过程中,许多事情都发生了变化,但许多旧习惯却依然存在。这种抵制变革的情况往往是习惯性的。然而,很多时候它是程序性的。本文讨论了我们正在犯的错误以及可能的解决方案。
关于本文
我在这里介绍的是从设计和架构角度构建N层系统的一种方式。本文不关注代码。实现N层系统的方法有很多,这只是其中之一。我希望您能通过这种方法找到一些好的建议、实践和模式。
尽管本文可能建议偏离一些“标准实践”,但本文中的所有内容都符合 Microsoft Patterns and Practices 中所述,如设计数据层组件并通过层传递数据以及其他文档。
即使您不决定采用这里提出的所有实践,您也应该能够采纳其中的一些。
目标
问任何开发人员业务逻辑应该放在哪里,可能的答案是:“当然在业务层。”
问同一个开发人员,他们公司的业务逻辑在哪里,他们会再次回答:“当然在业务层。”
毋庸置疑,业务逻辑当然应该放在业务层。然而,不仅仅是一些业务逻辑,而是所有的业务逻辑都应该放在业务层。阅读本文后,许多开发人员会意识到他们认为对系统真实的情况,实际上并非如此。
术语
这些术语经常混用,但为了本文的目的,我将按照此处定义的它们来指代。
层
由一台物理服务器或一组执行相同功能但旨在扩展容量的物理服务器定义的物理层。
Layer
系统的一个部分,包含在其自身的进程或部署单元中。多个层可以共存于一个层上,但如果使用某种远程处理能力,则可以轻松地移动到另一个层。
问题演变
桌面
在许多桌面应用程序中,业务逻辑与其他所有层都包含在一个层中。因为不需要分离这些层,所以这些层通常是混合的,没有可定义的边界。
客户端服务器
在客户端服务器系统中,有两层,因此强制实现至少两层。早期,服务器被简单地视为远程数据库,划分被视为应用程序(客户端)和存储(服务器)。通常,所有业务逻辑都保留在客户端,与其他层(如用户界面)混合在一起。
很快人们就意识到,通过将大部分业务逻辑从客户端移出,可以减少网络带宽并集中逻辑,从而减少客户端持续重新部署的需求。由于只有两层,唯一可以将业务逻辑移到的地方就是服务器。从架构上讲,服务器在客户端服务器系统中是一个非常合适的地方,但数据库作为平台是一个糟糕的选择。数据库设计用于存储和检索,其可扩展性围绕这些需求进行设计。数据库存储过程语言旨在进行基本数据转换,以补充 SQL 中无法完成的功能。存储过程语言旨在非常快速地执行,而不是用于复杂的业务逻辑。
但这是两种选择中更好的一种,所以业务逻辑被移到了存储过程中。事实上,我会认为业务逻辑是出于实用主义的考虑而被硬塞(强制塞入,使其适应)到存储过程中的。在两层世界中,它并不完美,但有所改进。
三层架构
随着客户端服务器方法的缺点显现出来,三层设计开始流行。当时最大和最紧迫的问题是连接数问题。虽然如今有些数据库可以处理数千个并发连接,但在1990年代,大多数数据库在达到500个连接左右时就开始承受巨大的压力。服务器通常按客户端连接数许可。这些因素使得减少数据库连接数变得可取。
连接池变得流行,然而,要在具有许多不同客户端的系统中实现连接池,需要在客户端和服务器之间插入第三层。这个中间层被称为“中间层”。在许多情况下,中间层仅用于协调连接池,而在其他情况下,业务逻辑开始移到中间层,因为可以使用比存储过程语言更适合的开发语言(C++、VB、Delphi、Java)。很快,中间层成为放置业务逻辑的最佳位置。
中间层还为带宽较低的客户端连接提供支持,因为直接数据库连接通常需要高速低延迟的网络连接。
什么是业务逻辑?
在进一步讨论之前,让我们准确定义什么是业务逻辑。在会议和公司中展示此内容时,我意识到并非所有人都同意业务逻辑到底是什么,并且在许多情况下,甚至没有认真思考业务逻辑是什么以及它不是什么。
数据库服务器是存储层。数据库旨在尽可能快速高效地存储、检索和更新数据。此功能通常被称为 CRUD(创建、检索、更新、删除)。当然,有些数据库是 CRUD,但那是另一个话题。
数据库被设计为在 CRUD 操作方面非常快速和高效。它们不是为格式化电话号码、计算公用事业使用的最佳使用量和高峰期、确定货物的地理目的地和路线等而设计的。然而,我见过所有这些情况,甚至更复杂的情况,大部分甚至全部都在存储过程中实现。
删除客户
然而,这不仅仅是复杂的情况。让我们考虑一个简单的情况,一个通常甚至不被认为是业务逻辑的情况。这个情况是“删除客户”。在我所见过的几乎所有系统中,删除客户都是完全由存储过程处理的。然而,在删除客户时,需要做出许多业务逻辑决策。客户可以被删除吗?在删除之前和之后必须执行哪些流程?必须首先检查哪些安全措施?作为结果,哪些表中的记录需要被删除或更新?
数据库不应该知道客户是什么,而只知道用于存储客户的元素。数据库不应该能够关联哪些表应该存储客户对象,它应该独立于客户对象来处理这些表。数据库的工作是存储代表客户的表中的行。除了参照完整性约束、数据类型、可空性以及使数据检索更快的索引之外,数据库不应该对业务层中确切构成客户的内容有任何功能性了解。
存储过程(如果存在)应仅对一个表进行操作,但通过 SQL SELECT
连接表以返回数据的存储过程除外。在这种情况下,存储过程充当视图。视图和存储过程也应用于基本的值制表,但仅为了促进业务层更快、更高效的数据检索和更新。
即使在许多以最新设计和技术为傲的公司,以及那些吹嘘所有业务逻辑都在业务层的公司中,对他们的数据库进行快速审查,也会立即发现删除客户、添加客户、停用客户、暂停客户等操作,不仅限于客户,还包括许多其他业务对象。
我经常看到执行类似以下操作的存储过程
sp_DeleteCustomer(x)
Select row in customer table, is Locked field
If true then throw error
Sum total of customer billing table
If balance > 0 then throw error
Delete rows in customer billing table (A detail table)
if Customer table Created field older than one year then
Insert row in survey table
Delete row in customer table
很多时候,部分业务逻辑被移到了业务层。
Business Layer (C#, etc)
Select row in customer table, is Locked field
If true then throw error.
Sum total of customer billing table
If balance > 0 then throw error.
if Customer table Created field older than one year then
Insert row in survey table
Call sp_DeleteCustomer
sp_DeleteCustomer(x)
Delete rows in customer billing table (A detail table)
Delete row in customer table
在这种情况下,部分业务逻辑已经移动,但并非全部。哪些表受到影响也是业务逻辑。数据库不应知道哪些表在业务级别上构成一个客户。这些最好在业务层中处理。对于这三个操作中的每一个,业务层都会发出一个 SQL 或调用三个独立的存储过程来执行最后一个 sp_DeleteCustomer
中的功能。
将所有业务逻辑移至业务层,我们有
Business Layer (C#, etc)
Select row in customer table, is Locked field
If true then throw error.
Sum total of customer billing table
If balance > 0 then throw error.
if Customer table Created field older than one year then
Insert row in survey table
Call sp_DeleteCustomer
Delete rows in customer billing table (A detail table)
Delete row in customer table
如果删除的行只涉及一个表,则可以使用存储过程,但对于现代数据库,使用查询计划缓存,性能提升微乎其微。此外,此类系统发送的 SQL 非常简单,因为它只操作单个表,因此计划也非常简单,几乎不需要进行优化。事实上,一些数据库由于加载了过多的存储过程而受到的影响,比执行简化的 SQL 语句所受的影响还要大。
通过将表修改也移至业务层,可以获得以下优点:
- 系统变得更易于移植到不同的数据库,因为这些存储过程不需要移植到每个数据库。
- 未来的修改更容易,因为所有逻辑都集中在一个地方,而不是两个地方。
- 调试更容易,因为逻辑没有分散在两个地方。
- 其他业务逻辑不会因为“更容易”而“溜进”存储过程。
虽然这需要对数据库进行三次连续调用而不是一次,但您的业务层应通过单独的高速段(例如1千兆位段)连接到数据库。发送300字节而不是100字节不会产生显著差异。大多数数据库也支持SQL的批处理提交,并且这三条语句可以以一个批次发送到数据库,从而减少网络调用。应使用数据访问层来发出此类SQL,而不是将SQL语句嵌入到代码中。
一些数据库管理员甚至开发人员可能不接受这种程度的集成,并坚持将此类批处理更新保留在存储过程中。**这是您需要做出的选择,很大程度上取决于您的数据库以及您的优先级。**由于几乎所有现代数据库现在都根据查询计划缓存优化提交的 SQL,因此在大多数情况下性能差异很小,但肯定有技术原因不将逻辑放入存储过程中。如果您选择将此类批处理更新保留在存储过程中,您应该非常小心,不要让其他业务逻辑混入存储过程中,并使您的存储过程功能纯粹用于 CRUD 操作,不嵌入条件操作和其他业务逻辑。
格式化
让我们考虑另一个我在开发人员中发现关于其是否为业务逻辑存在很大分歧的项目。我将论证为什么我认为它是业务逻辑,而不是用户界面或存储。该项目是非简单格式化。我将使用的例子是电话号码。
每个国家都有自己的方式来以视觉上令人愉悦的方式显示电话号码。在大多数国家,甚至不止一种常见的方式。一些示例如下:
Cyprus:
+357 (25) 66 00 34
+357 (25) 660 034
+357 25 660 034
+357 2566 0034
Germany:
+49 211 123456
+49 211 1234-0
North America (USA, Canada, some parts of Caribbean)
+1 (423) 235-2423
+1-423-235-2423
Russia:
+7 (812) 438-46-02
+7 (812) 438-4602
德国甚至有一项官方规定,名为DIN 5008,用于指定格式。
当然,国家代码通常不包含在本地。但是假设我们的系统是国际化的,所以我们也会存储和显示国家代码。对于每个国家,我们将选择一种格式来显示号码。
电话号码格式化的注意事项
- 输入将以各种格式提供。
- 每个国家都有自己独特的号码显示方式。
- 有些国家的格式不简单,并且会根据前几位数字而变化。
- 前几位数字(通常是区域/区号)并不总是固定位数。在俄罗斯的例子中,812是圣彼得堡市的区号。095是莫斯科,但西伯利亚和其他地区的一些地方是4位(3952)。这导致电话号码的总长度和格式根据区号而改变。
- 随着新的可移植性法律、新的移动运营商、欧盟一体化、电话系统升级以及更多的电话号码格式和长度在全球范围内相当频繁地变化。近年来,塞浦路斯两次更改了区号方案,以适应首先是一个更具扩展性的系统,然后是多个移动电话运营商。全球有数百个国家,您可以预期变化会定期发生。
通常在输入时会剥离所有非数字字符,使电话号码变为如下所示:
Phone: 35725660034
有时国家代码会被分离并存储在一个单独的字段中,如下所示:
PhoneCountry: 357
PhoneLocal: 25660034
这看起来很简单,但这又带来了一个业务逻辑问题。并非所有国家代码都具有相同位数的数字。国家代码长度为1到3位。
通常,输入解析(如果国家代码分离)和显示逻辑是在客户端实现的,因为客户端是使用适合的传统语言编写的。问题是客户端需要大量数据来确定国家代码的长度,并且每次需要更新显示例程时,都需要重新分发客户端。
有时格式化是在存储过程中完成的。这种方法的缺点是存储过程语言不适合这种类型的逻辑,并且经常导致错误以及实际逻辑的低效处理。
电话号码通常存储两次。首先以原始格式存储,以便可以索引和轻松搜索,然后以格式化的形式再次存储,以便于显示。除了前面提到的问题外,还存在数据重复和为适应新格式而更新的其他问题。
在某些极端情况下,而且令人惊讶的是,相当频繁地,电话号码以输入时的任何格式存储。这带来的问题是显而易见的;电话号码无法轻松定位、索引或用于排序。
重要的是,尽管它是格式化,但它不是用户界面;尽管任何集中化的冲动最终都常在数据库中,但这显然是业务逻辑。在业务层中实现格式化消除了重复数据,并允许使用开发语言而不是将其硬塞到数据语言中来实现。
异常
某些批量更新在存储过程中实现时,速度会快很多。大多数情况可以直接由 SQL 处理,但少数类型的批量更新需要循环行为,如果在业务层中实现,将创建数千条 SQL 语句。在这些罕见的情况下,即使需要在存储过程中实现一些业务逻辑,也应使用存储过程。应特别注意在此存储过程中尽可能少地实现内容。
我将在本文后面再次谈到这个主题。
当今系统
客户端服务器
在客户端服务器系统中,业务逻辑最常分散在客户端和服务器之间。
实际百分比因应用程序和企业而异,前面的示例很好地涵盖了客户端服务器应用程序。大部分业务逻辑已在存储过程和视图中实现,以试图集中业务逻辑。然而,许多业务规则无法轻易在 SQL 或存储过程中实现,或者由于与用户界面相关,在客户端执行速度更快。由于这些相互矛盾的因素,业务规则被分散在客户端和服务器之间。
N-Tier
出于各种原因,我将在后面的“障碍”主题中介绍,当构建N层系统时,业务逻辑的整合情况往往会变得更糟。业务逻辑反而变得更加碎片化。
当然,每个系统根据业务逻辑在各层之间的分布方式而有所不同,但有一点是共通的。业务逻辑现在分布在三层而不是两层。接下来我将介绍一些常见场景。
场景 1
N层系统中业务逻辑的常见分布如下:
在这种情况下,业务层不包含任何业务规则。它不是一个真正的业务层,而仅仅是数据库结果集的 XML(或其他流格式)格式化器和映射器。虽然可以获得一些优势,例如数据库连接池、数据库独立性以及与数据库一定程度的分离,但这不是一个真正的业务逻辑层。它更像是一个没有人为逻辑层的物理层。
场景 2
另一个常见场景如下:
通常,应用程序中的一些规则会被移到业务层,而数据库中的内容大多保留在那里。
当在这种设计中重用业务层时,保留在应用程序中的业务规则必须重复。这违背了实现业务层的主要目标之一。
调用应用程序甚至可能通过不实现业务规则或直接忽略它们来违反业务规则。对于真正的业务层,这是不可能的。
整合
相反,业务层应该包含所有的业务规则。
这样的设计具有以下优点:
- 所有业务逻辑都存在于一个单一位置,可以轻松验证、调试和修改。
- 可以使用真正的开发语言来实现业务规则。这种语言比 SQL 和存储过程更灵活,也更适合这些业务规则。
- 数据库成为存储层,可以专注于高效地检索和存储数据,而不受任何与业务逻辑实现或表示层相关的约束。
上述场景是目标;然而,一些重复,特别是为了验证目的,也应该存在于客户端。这些规则应该由业务层加强。此外,在某些系统中,为了极致的性能优势,例如批量更新,可能会导致覆盖性异常,并应放置在数据库中。因此,一个更现实的方法如下所示。请注意,100% 仍然存在于业务层中,并且其他层中存在的最小部分实际上是重复的,仅仅是为了性能或根据选择禁用用户界面字段等目的而存在。
迁移到中间层
滑坡
在迁移到中间层时,总是有一种冲动,想“我们把这一部分放在存储过程中吧”。然后是“另一个”和“另一个”。很快,您就会回到以前的境地,并没有太多改变。
存储过程应该用于执行 SQL 并返回结果集,在那些比视图更能优化存储过程的数据库中(例如 SQL Server)。但是存储过程除了在返回数据时连接数据之外,不应该做任何其他事情。对于更新数据,它应该准确地只做那件事,并且不应该以任何方式解释数据。
在某些情况下,出于大幅提高性能的原因,某些项目应该移到存储过程中。然而,这些情况实际上很少见,应该是一个例外,而不是规则。每个例外都应该经过审查和批准,而不是由开发人员或数据库管理员随意执行。
更便宜
购买更多硬件可能更便宜,这听起来可能很奇怪。但是,当您添加额外的中间层服务器时,除了操作系统之外,几乎不需要其他软件。
增加数据库服务器容量的成本很高,原因有二:
- 数据库服务器硬件通常比中间层服务器档次更高,也更昂贵。
- 数据库通常按 CPU 许可,添加额外的 CPU 或服务器在许可费用方面成本很高。许可费用可能从每个 CPU 5,000 美元到每个 CPU 40,000 美元不等。
通过将逻辑迁移到中间层,您可以显著降低数据库服务器的负载,并防止数据库过早扩展。
更容易
除了成本之外,升级中间层通常比升级数据库更容易。
数据库在仅仅通过增加更多硬件来扩展方面存在固有限制。在某个时候,必须使用分区、集群、复制等其他技术。但这些技术都不是简单的,而且都涉及硬件、迁移和/或对现有系统的重大投入。
然而,中间层服务器易于扩展。一旦安装了负载分发器,只需添加一台新服务器即可扩展容量。
拓扑
让我们使用以下图表来考虑我刚才讨论的因素。上方条形图中的阴影显示了其标题相对于图表中各层的方向或大小。从客户端到中间层再到数据库,每个单元的成本都会增加。我用“单元”一词来指代处理器或服务器,具体取决于配置。
当用相对值绘制相同结果时,可以轻松进行比较
我没有在图表上标注数字,因为这些数字严重依赖于网络配置、处理器能力以及每个企业独有的其他因素。每项测量也使用不同的计量单位。我在这里展示的是每项测量的大致关系,彼此叠加以显示相互关系。这清楚地表明,中间层具有扩展能力,并且比数据库更便宜、更容易实现。
发展中间层
如果业务逻辑的很大一部分在数据库中实现,您将需要更大的数据库。场景如下:
通过将逻辑迁移到中间层,您可以大幅降低数据库的负载。这里的实际数字仅用于演示目的,会因每个系统而异,但它们应该有助于您理解要点。虽然下图整体上需要更多的硬件,但系统的总成本更低,部署也更容易。发展中间层更容易,也更便宜。
瓶颈
让我们重新审视上一张图中的一个项目
在这个系统中,就容量而言,单一的瓶颈是什么?这些层中哪一层有明显的增长限制?很明显是数据库。所有东西都最终会汇集到数据库中。
因此,通过将处理移到中间层,我们能够离数据库层的边界更远。
障碍
向整合设计迈进存在一些障碍,不仅仅是像编码方式不同这样显而易见的障碍。
习惯
俗话说“旧习难改”。这甚至适用于一个团队。在一个团队中,你不仅需要说服自己,还需要说服团队中的大多数成员。
流程
许多公司都有长期存在的安全策略,规定安全必须在数据库中控制,并且使用存储过程作为视图不能提供足够的控制。改变公司安全策略以适应N层世界通常会很困难,甚至不可能。
随着 .NET 安全和微软未来的产品,中间层级别的企业安全比以往任何时候都受到更多关注,然而许多公司仍然专注于数据库,他们要么不了解这些变化,要么不愿改变。
数据库管理员
这个话题有点敏感。然而,尽管敏感,有些话还是必须说。无论您是 DBA(数据库管理员)还是开发人员,请不要将我接下来要说的解释为刻板印象或适用于所有 DBA。然而,这种情况相当普遍和常见。**如果您是不属于这种类型的 DBA,那么恭喜您!您是数据库总裁,而不是数据库霸主。**
拥有正常运行系统的 DBA 往往不愿进行任何实质性更改,因为这可能会破坏他们的系统。许多企业只有一个 DBA 和许多 DA(数据库助理)。DBA 是其领域中的国王,对所有与数据库相关的事项拥有最终决定权。只有管理层才会否决 DBA,而管理层由于无法判断技术性数据库问题,总是会听从 DBA 的意见。
大多数 DBA 对N层系统不断变化的需求知之甚少,或者根本不关心。对他们来说,任何一层都只是另一个客户端,一切都是客户端服务器模型。他们只关心运行他们的数据库,并会为开发人员做出一些妥协,但前提是这不会给他们带来任何麻烦。
DBA 通常不会像开发人员那样频繁地跳槽,他们中的大多数人已经管理一家企业的数据库长达10年甚至20年。数据库对他们来说非常重要,他们不愿做出任何妥协。他们已经建立了他们的王国,不愿放弃控制权。让这样的 DBA 放弃一定程度的安全性和实现可能是一场主要的斗争,并且需要管理层的介入。
其他DBA没那么挑剔,只要他们觉得合理,都能接受。但在许多企业,特别是大型企业中,有几十甚至几百名开发人员,而DBA只有一两个或几个,并且DBA处于企业指挥链的顶端。
工具
目前可用的大多数工具要么不利于业务逻辑的实现,要么不促进其实现。许多工具只是专注于可伸缩性、连接池和数据库独立性,却未能很好地解决业务逻辑实现的需求。
解决方案
架构师监管
我发现让系统架构师定期进行审查并标记业务逻辑的不当放置非常有用。越早发现,修复起来就越容易,成本也越低。如果您没有指定一名主架构师,那么团队的开发人员可以相互监督。如果发现有不当之处,该开发人员可以提醒团队,然后提醒团队负责人。
DA 教育
对 DA(数据库助理)进行教育非常重要。DA 长期以来一直在实现业务逻辑,因此他们很难将业务逻辑与存储区分开来。DA 通常会按照 DBA 的指示做任何需要他们做的事情。
DA 仍然参与其中。他们仍然执行 JOIN,优化 SQL,并维护数据库。他们还应该监控来自中间层的 SQL 并监控数据库性能。DA 也将继续执行数据库设计。
管理层教育
管理层常常会抵制,尽管这是一种更容易克服的障碍,而不是困难的障碍。管理层不会关心你的工作是否更容易,但他们会关心成本、开发时间、业务收益,并且一定会抛出一些时下流行语。
改变管理层的主要障碍很可能是来自 DBA 的阻力。所以要彻底说服管理层,让他们来处理 DBA。
深入阅读
本文中的思想是我近10年来一直在使用的模式和实践。当然,它们不断地被完善和更新,以利用新技术并适应不断变化的需求。
在我的工作中,我阅读了许多“专家”撰写的论文。不幸的是,这些论文大多是由开发人员撰写的,他们擅长提出理论并告诉人们应该如何做,却从不将自己的实践付诸实践。另一些论文是由经验丰富的开发人员撰写的,他们没有广阔的视野,他们的实践非常领域特定,但他们却自称拥有广泛的知识和适用于所有应用程序的解决方案。当开发人员阅读此类论文时,他们会认为解决每个问题只有一种方法。开发人员需要更加开放,并理解解决问题总是有不止一种方法,并且此类论文是其他用户的经验,应将其用作指导,而不是教条。
我不得不提到所有这些,因为真正优秀并且没有陷入这些陷阱的作品很少见。我近年来读过的最好的论文之一写于2002年8月,是微软模式与实践论文之一。它与我在这里和其他文章中介绍的内容非常吻合,是一篇很好的配套论文。
请参考设计数据层组件并通过层传递数据。
我还写了一篇相关文章
结论
在大型企业中改变方向往往具有很强的政治性和风险。从开发人员的角度来看,低调行事,让其他人去争斗会容易得多。我怀疑许多开发人员会完全停止他们长期使用的做法。通过这篇文章,我希望为您提供一些想法,以修改您现有的流程,或至少更仔细地评估某些否则可能轻易通过的决策。
这里描述的方法最适用于新系统,或在重建整个或部分系统时。对于现有系统,除非有其他压倒性因素导致重新设计,否则通常最好保持原样。