烂翻译系列之学习领域驱动设计——第五章:实现简单的业务逻辑

Business logic is the most important part of software. It’s the reason the software is being implemented in the first place. A system’s user interface can be sexy and its database can be blazing fast and scalable. But if the software is not useful for the business, it’s nothing but an expensive technology demo.

业务逻辑是软件最重要的部分。这就是软件被实现的首要原因。一个系统的用户界面可以是非常吸引人的,其数据库可以是非常快速和可扩展的。但是如果软件对业务没有用处,那么它就只不过是一个昂贵的技术演示。

As we saw in Chapter 2, not all business subdomains are created equal. Different subdomains have different levels of strategic importance and complexity. This chapter begins our exploration of the different ways to model and implement business logic code. We will start with two patterns suited for rather simple business logic: transaction script and active record.

正如我们在第2章中看到的,并非所有的业务子域都是平等的。不同的子域具有不同的战略重要性和复杂性级别。从本章开始,我们将探索建模和实现业务逻辑代码的不同方法。我们将从两种适用于相对简单的业务逻辑的模式开始:事务脚本和活动记录。

Transaction Script

事务脚本

Organizes business logic by procedures where each procedure handles a single request from the presentation. —Martin Fowler

事务脚本:通过过程(方法)组织业务逻辑,每个过程(方法)处理来自展示(层)的一个单一请求。—Martin Fowler

A system’s public interface can be seen as a collection of business transactions that consumers can execute, as shown in Figure 5-1. These transactions can retrieve information managed by the system, modify it, or both. The pattern organizes the system’s business logic based on procedures, where each procedure implements an operation that is executed by the system’s consumer via its public interface. In effect, the system’s public operations are used as encapsulation boundaries.

系统的公共接口可以看作是消费者可以执行的一系列业务事务的集合,如图5-1所示。这些事务可以检索由系统管理的信息,也可以修改它,或者两者兼而有之。该模式基于过程(方法)组织系统的业务逻辑,其中每个过程(方法)实现一个操作,该操作由系统的消费者通过其公共接口执行。实际上,系统的公共操作被用作封装边界。

 

Figure 5-1. Transaction script interface

图5-1.  事务脚本接口

Implementation

实现

Each procedure is implemented as a simple, straightforward procedural script. It can use a thin abstraction layer for integrating with storage mechanisms, but it is also free to access the databases directly.

每个过程(方法)都被实现为一个简单、直接的过程脚本。它可以使用一个很薄的抽象层来与存储机制集成,但也可以直接访问数据库。

The only requirement procedures have to fulfill is transactional behavior. Each operation should either succeed or fail but can never result in an invalid state. Even if execution of a transaction script fails at the most inconvenient moment, the system should remain consistent—either by rolling back any changes it has made up until the failure or by executing compensating actions. The transactional behavior is reflected in the pattern’s name: transaction script.

过程(方法)必须满足的唯一要求是事务行为。每个操作要么成功,要么失败,但绝不能导致无效状态。即使事务脚本的执行在最困难的时候失败,系统也应该保持一致——要么回滚在失败之前所做的任何更改,要么执行补偿操作。这种事务行为体现在模式的名称中: 事务脚本。

Here is an example of a transaction script that converts batches of JSON files into XML files:

下面是一个将批量的JSON文件转换为XML文件的事务脚本的示例:

1 DB.StartTransaction();
2 var job = DB.LoadNextJob();
3 var json = LoadFile(job.Source);
4 var xml = ConvertJsonToXml(json);
5 WriteFile(job.Destination, xml.ToString();
6 DB.MarkJobAsCompleted(job);
7 DB.Commit()

It’s Not That Easy!

没那么简单!

When I introduce the transaction script pattern in my domain-driven design classes, my students often raise their eyebrows, and some even ask, “Is it worth our time? Aren’t we here for the more advanced patterns and techniques?”

当我在我的领域驱动设计课程中介绍事务脚本模式时,我的学生经常会皱起眉头,甚至有些会问:“这值得我们花时间吗?我们不是来学习更高级的模式和技术吗?”

The thing is, the transaction script pattern is a foundation for the more advanced business logic implementation patterns you will learn in the forthcoming chapters. Furthermore, despite its apparent simplicity, it is the easiest pattern to get wrong. A considerable number of production issues I have helped to debug and fix, in one way or another, often boiled down to a misimplementation of the transactional behavior of the system’s business logic.

事实上,事务脚本模式是你在接下来章节中将学习的更高级业务逻辑实现模式的基础。此外,尽管它看起来很简单,但也是最容易被误用的模式。我曾以多种方式帮助调试和修复了大量生产问题,这些问题通常都归结为系统业务逻辑的事务行为的错误实现。

Let’s take a look at three common, real-life examples of data corruption that results from failing to correctly implement a transaction script.

让我们来看一下三个常见的、真实的数据损坏示例,这些数据损坏是由于未能正确实现事务脚本而导致的。

Lack of transactional behavior

缺乏事务行为

A trivial example of failing to implement transactional behavior is to issue multiple updates without an overarching transaction. Consider the following method that updates a record in the Users table and inserts a record into the VisitsLog table:

未能实现事务行为的一个简单例子是,在没有全局事务的情况下发出多个更新。思考以下方法,该方法更新Users表中的记录并向VisitsLog表中插入记录:

01 public class LogVisit
02 {
03 ...
04
05 public void Execute(Guid userId, DataTime visitedOn)
06 {
07 _db.Execute("UPDATE Users SET last_visit=@p1 WHERE user_id=@p2",
08 visitedOn, userId);
09 _db.Execute(@"INSERT INTO VisitsLog(user_id, visit_date)
10 VALUES(@p1, @p2)", userId, visitedOn);
11 }
12 }

If any issue occurs after the record in the Users table was updated (line 7) but before appending the log record on line 9 succeeds, the system will end up in an inconsistent state. The Users table will be updated but no corresponding record will be written to the VisitsLog table. The issue can be due to anything from a network outage to a database timeout or deadlock, or even a crash of the server executing the process.

如果在更新Users表中的记录(第7行)后但在成功追加日志记录(第9行)之前发生任何问题,系统最终将处于不一致状态。Users表将被更新,但VisitsLog表中不会写入相应的记录。这个问题可能是由于网络中断、数据库超时或死锁,甚至执行该过程(方法)的服务器崩溃等原因造成的。

This can be fixed by introducing a proper transaction encompassing both data changes:

可以通过引入包含两个数据更改的适当事务来解决这个问题:

 1 public class LogVisit
 2 {
 3  ...
 4  public void Execute(Guid userId, DataTime visitedOn)
 5  {
 6  try
 7  {
 8  _db.StartTransaction();
 9  _db.Execute(@"UPDATE Users SET last_visit=@p1
10  WHERE user_id=@p2",
11  visitedOn, userId);
12  _db.Execute(@"INSERT INTO VisitsLog(user_id,
13 visit_date)
14  VALUES(@p1, @p2)",
15  userId, visitedOn);
16  _db.Commit();
17  } catch {
18  _db.Rollback();
19  throw;
20  }
21  }
22 }

The fix is easy to implement due to relational databases’ native support of transactions spanning multiple records. Things get more complicated when you have to issue multiple updates in a database that doesn’t support multirecord transactions, or when you are working with multiple storage mechanisms that are impossible to unite in a distributed transaction. Let’s see an example of the latter case.

由于关系型数据库本身支持跨多个记录的事务,因此这个修复很容易实现。但是,当您必须在不支持多记录事务的数据库中发出多个更新时,或者当您正在使用多个存储机制且这些机制无法在分布式事务中统一时,情况就会变得更加复杂。让我们来看一个后一种情况的例子。

Distributed transactions

分布式事务

In modern distributed systems, it’s a common practice to make changes to the data in a database and then notify other components of the system about the changes by publishing messages into a message bus. Consider that in the previous example, instead of logging a visit in a table, we have to publish it to a message bus:

在现代分布式系统中,一种常见做法是在数据库中更改数据,然后通过向消息总线发布消息来通知系统的其他组件有关更改的信息。考虑在前面的例子中,我们不是将访问记录记录在表中,而是将其发布到消息总线上:

01 public class LogVisit
02 {
03 ...
04
05 public void Execute(Guid userId, DataTime visitedOn)
06 {
07 _db.Execute("UPDATE Users SET last_visit=@p1 WHERE user_id=@p2",
08 visitedOn,userId);
09 _messageBus.Publish("VISITS_TOPIC",
10 new { UserId = userId, VisitDate = visitedOn });
11 }
12 }

As in the previous example, any failure occurring after line 7 but before line 9 succeeds will corrupt the system’s state. The Users table will be updated but the other components won’t be notified as publishing to the message bus has failed.

与前面的例子一样,如果在第7行之后但在第9行成功之前发生任何失败,都将破坏系统的状态。Users表将被更新,但其他组件不会收到通知,因为向消息总线的消息发布已经失败。

Unfortunately, fixing the issue is not as easy as in the previous example. Distributed transactions spanning multiple storage mechanisms are complex, hard to scale, error prone, and therefore are usually avoided. In Chapter 8, you will learn how to use the CQRS architectural pattern to populate multiple storage mechanisms. In addition, Chapter 9 will introduce the outbox pattern, which enables reliable publishing of messages after committing changes to another database.

不幸的是,解决此问题并不像前一个例子那样简单。跨越多个存储机制的分布式事务是复杂的、难以扩展的、容易出错的,因此通常避免使用分布式事务。在第8章中,您将学习如何使用CQRS架构模式来填充多个存储机制。此外,第9章将介绍outbox(发件箱)模式,该模式允许在将更改提交到另一个数据库后可靠地发布消息。

Let’s see a more intricate example of improper implementation of transactional behavior.

让我们来看一个事务行为不正确实现的更复杂的例子。

Implicit distributed transactions

隐式分布式事务

Consider the following deceptively simple method:

思考下面这个看似简单的方法:

 1 public class LogVisit
 2 {
 3  ...
 4  public void Execute(Guid userId)
 5  {
 6  _db.Execute("UPDATE Users SET visits=visits+1 WHERE
 7 user_id=@p1",
 8  userId);
 9  }
10 }

Instead of tracking the last visit date as in the previous examples, this method maintains a counter of visits for each user. Calling the method increases the corresponding counter’s value by 1. All the method does is update one value, in one table, residing in one database. Yet this is still a distributed transaction that can potentially lead to inconsistent state.

与前面的例子不同,这个方法不是跟踪最后访问日期,而是为每个用户维护一个访问计数器。调用该方法会将相应的计数器的值增加1。这个方法所做的只是在单个数据库的单个表中更新一个值。然而,这仍然是一个分布式事务,可能会导致不一致的状态。

This example constitutes a distributed transaction because it communicates information to the databases and the external process that called the method, as demonstrated in Figure 5-2.

这个例子构成一个分布式事务,因为它将信息传递给数据库和调用该方法的外部进程,如图5-2所示。

Figure 5-2. The LogVisit operation updating the data and notifying the caller of the operation’s success or failure

图5-2. “LogVisit”操作更新数据并通知调用者操作成功或失败

Although the execute method is of type void, that is, it doesn’t return any data, it still communicates whether the operation has succeeded or failed: if it failed, the caller will get an exception. What if the method succeeds, but the communication of the result to the caller fails? For example:

尽管“execute”方法的类型是void,即它不返回任何数据,但它仍然会通知调用者操作是否成功或失败:如果失败,调用者将收到一个异常。但是,如果方法成功,但返回结果时与调用方的通信失败了怎么办?例如:

  • If LogVisit is part of a REST service and there is a network outage; or  如果“LogVisit”是REST服务的一部分,并且发生了网络中断;或者
  • If both LogVisit and the caller are running in the same process, but the process fails before the caller gets to track successful execution of the LogVisit action?  如果“LogVisit”和调用者都在同一个进程中运行,但在调用者能够跟踪“LogVisit”操作成功执行之前,该进程失败了怎么办?

In both cases, the consumer will assume failure and try calling LogVisit again. Executing the LogVisit logic again will result in an incorrect increase of the counter’s value. Overall, it will be increased by 2 instead of 1. As in the previous two examples, the code fails to implement the transaction script pattern correctly, and inadvertently leads to corrupting the system’s state.

在这两种情况下,消费者都会假设操作失败并尝试再次调用“LogVisit”。再次执行“LogVisit”逻辑将导致计数器的值错误地增加。总的来说,计数器的值将增加2而不是1。与前面的两个例子一样,代码未能正确实现事务脚本模式,并无意中导致系统状态损坏。

As in the previous example, there is no simple fix for this issue. It all depends on the business domain and its needs. In this specific example, one way to ensure transactional behavior is to make the operation idempotent: that is, leading to the same result even if the operation repeated multiple times.

与前面的例子一样,这个问题没有简单的修复方法。一切都取决于业务领域及其需求。在这个特定的例子中,确保事务行为的一种方法是使操作具有幂等性:即,即使操作被重复多次,也能得到相同的结果。

For example, we can ask the consumer to pass the value of the counter. To supply the counter’s value, the caller will have to read the current value first, increase it locally, and then provide the updated value as a parameter. Even if the operation will be executed multiple times, it won’t change the end result:

例如,我们可以要求消费者传递计数器的值。为了提供计数器的值,调用者将首先读取当前值,在本地增加它,然后将更新后的值作为参数提供。即使这个操作被多次执行,它也不会改变最终结果:

 1 public class LogVisit
 2 {
 3  ...
 4  
 5  public void Execute(Guid userId, long visits)
 6  {
 7  _db.Execute("UPDATE Users SET visits = @p1 WHERE
 8 user_id=@p2",
 9  visits, userId);
10  }
11 }

Another way to address such an issue is to use optimistic concurrency control: prior to calling the LogVisit operation, the caller has read the counter’s current value and passed it to LogVisit as a parameter. LogVisit will update the counter’s value only if it equals the one initially read by the caller:

解决此类问题的另一种方法是使用乐观并发控制:在调用“LogVisit”操作之前,调用者会读取计数器的当前值并将其作为参数传递给“LogVisit”。只有当计数器的值等于调用者最初读取的值时,“LogVisit”才会更新计数器的值:

 1 public class LogVisit
 2 {
 3 ...
 4  
 5  public void Execute(Guid userId, long expectedVisits)
 6  {
 7  _db.Execute(@"UPDATE Users SET visits=visits+1
 8  WHERE user_id=@p1 and visits = @p2",
 9  userId, visits);
10  }
11 }

Subsequent executions of LogVisit with the same input parameters won’t change the data, as the WHERE...visits = @prm2 condition won’t be fulfilled.

使用相同的输入参数再次执行“LogVisit”将不会更改数据,因为“WHERE...visits = @prm2”条件将不会满足。

When to Use Transaction Script

何时使用事务脚本

The transaction script pattern is well adapted to the most straightforward problem domains in which the business logic resembles simple procedural operations. For example, in extract-transform-load (ETL) operations, each operation extracts data from a source, applies transformation logic to convert it into another form, and loads the result into the destination store. This process is shown in Figure 5-3.

事务脚本模式非常适用于业务逻辑类似于简单过程操作的最直接的问题域。例如,在提取-转换-加载(ETL)操作中,每个操作都从源中提取数据,应用转换逻辑将其转换为另一种形式,然后将结果加载到目标存储中。这个过程如图5-3所示。

Figure 5-3. Extract-transform-load data flow

图5-3。提取-转换-装载数据流

The transaction script pattern naturally fits supporting subdomains where, by definition, the business logic is simple. It can also be used as an adapter for integration with external systems—for example, generic subdomains, or as a part of an anticorruption layer (more on that in Chapter 9).

事务脚本模式天然适合支撑子域,根据支撑子域的定义,其中的业务逻辑是简单的。它还可以用作与外部系统集成的适配器——例如,通用子域,或者作为防腐层的一部分(在第9章中有更多介绍)。

The main advantage of the transaction script pattern is its simplicity. It introduces minimal abstractions and minimizes the overhead both in runtime performance and in understanding the business logic. That said, this simplicity is also the pattern’s disadvantage. The more complex the business logic gets, the more it’s prone to duplicate business logic across transactions, and consequently, to result in inconsistent behavior—when the duplicated code goes out of sync. As a result, transaction script should never be used for core subdomains, as this pattern won’t cope with the high complexity of a core subdomain’s business logic.

事务脚本模式的主要优势在于其简单性。它引入了最小的抽象,并最小化了运行时性能和理解业务逻辑的开销。然而,这种简单性也是该模式的缺点。随着业务逻辑变得更加复杂,它在不同事务之间越容易重复业务逻辑,从而导致不一致的行为——当重复的代码不同步时。因此,事务脚本模式永远不应用于核心子域,因为这种模式无法应对核心子域业务逻辑的高度复杂性。

This simplicity earned the transaction script a dubious reputation. Sometimes the pattern is even treated as an antipattern. After all, if complex business logic is implemented as a transaction script, sooner rather than later it’s going to turn into an unmaintainable, big ball of mud. It should be noted, however, that despite the simplicity, the transaction script pattern is ubiquitous in software development. All the business logic implementation patterns that we will discuss in this and the following chapters, in one way or another, are based on the transaction script pattern.

这种简单性使事务脚本模式获得了一个令人怀疑的名声。有时,这种模式甚至被视为一种反模式。毕竟,如果复杂的业务逻辑使用事务脚本实现,那么它迟早会变成一个难以维护的、一团糟的大泥球。然而,值得注意的是,尽管事务脚本模式很简单,但是它在软件开发中是普遍存在的。我们将在本章和后续章节中讨论的所有业务逻辑实现模式,都以某种方式基于事务脚本模式。

Active Record

活动记录

An object that wraps a row in a database table or view, encapsulates the database access, and adds domain logic on that data. —Martin Fowler

活动记录:一个封装了数据库表或视图中的行的数据对象,封装了对数据库的访问,并在该数据对象上添加了领域逻辑。——Martin Fowler

Like the transaction script pattern, active record supports cases where the business logic is simple. Here, however, the business logic may operate on more complex data structures. For example, instead of flat records, we can have more complicated object trees and hierarchies, as shown in Figure 5-4.

与事务脚本模式类似,活动记录(Active Record)模式也支持业务逻辑简单的情况。然而在这里(活动记录模式下),业务逻辑可能操作更复杂的数据结构。例如,我们可以使用更复杂的对象树和层次结构来代替平面记录,如图5-4所示。

Figure 5-4. A more complicated data model with one-to-many and many-to-many relationships

图5-4.  具有一对多和多对多关系的更复杂的数据模型

Operating on such data structures via a simple transaction script would result in lots of repetitive code. The mapping of the data to an in-memory representation would be duplicated all over.

通过简单的事务脚本来操作这样的数据结构会导致大量重复的代码。数据到内存表示的映射将在所有被重复。

Implementation

实现

Consequently, this pattern uses dedicated objects, known as active records, to represent complicated data structures. Apart from the data structure, these objects also implement data access methods for creating, reading, updating, and deleting records—the so-called CRUD operations. As a result, the active record objects are coupled to an object-relational mapping (ORM) or some other data access framework. The pattern’s name is derived from the fact that each data structure is “active”; that is, it implements data access logic.

因此,这种模式使用称为活动记录(Active Record)的专用对象来表示复杂的数据结构。除了数据结构之外,这些对象还实现了用于创建、读取、更新和删除记录的数据访问方法——即所谓的CRUD操作。因此,活动记录对象与对象关系映射(ORM)或其他数据访问框架耦合。该模式的名称来源于每个数据结构都是“活动的”这一事实,即它实现了数据访问逻辑。

As in the previous pattern, the system’s business logic is organized in a transaction script. The difference between the two patterns is that in this case, instead of accessing the database directly, the transaction script manipulates active record objects. When it completes, the operation has to either complete or fail as an atomic transaction:

与前面的事务脚本模式一样,系统的业务逻辑被组织在一个事务脚本中。这两种模式之间的区别在于,在这种情况下(活动记录模式下),事务脚本不是直接访问数据库,而是操作活动记录对象。当事务完成时,操作必须作为一个原子事务完成或失败:

 1 public class CreateUser
 2 {
 3  ...
 4  public void Execute(userDetails)
 5  {
 6  try
 7  {
 8  _db.StartTransaction();
 9  var user = new User();
10  user.Name = userDetails.Name;
11  user.Email = userDetails.Email;
12  user.Save();
13  _db.Commit();
14  } catch {
15  _db.Rollback();
16  throw;
17  }
18  }
19 }

The pattern’s goal is to encapsulate the complexity of mapping the inmemory object to the database’s schema. In addition to being responsible for persistence, the active record objects can contain business logic; for example, validating new values assigned to the fields, or even implementing business-related procedures that manipulate an object’s data. That said, the distinctive feature of an active record object is the separation of data structures and behavior (business logic). Usually, an active record’s fields have public getters and setters that allow external procedures to modify its state.

该模式的目标是封装内存对象到数据库模式的映射的复杂性。除了负责持久化之外,活动记录对象还可以包含业务逻辑;例如,验证分配给字段的新值,或者甚至实现与业务相关的、操纵对象数据的过程。不过,活动记录对象的显著特征是数据结构和行为(业务逻辑)的分离。通常,活动记录的字段具有公共的getter和setter方法,允许外部程序修改其状态。

When to Use Active Record

何时使用活动记录

Because an active record is essentially a transaction script that optimizes access to databases, this pattern can only support relatively simple business logic, such as CRUD operations, which, at most, validate the user’s input.

因为活动记录本质上是一个优化数据库访问的事务脚本,所以这个模式只能支持相对简单的业务逻辑,如CRUD操作,这些操作最多只能验证用户的输入。

Accordingly, as in the case of the transaction script pattern, the active record pattern lends itself to supporting subdomains, integration of external solutions for generic subdomains, or model transformation tasks. The difference between the patterns is that active record addresses the complexity of mapping complicated data structures to a database’s schema.

因此,与事务脚本模式的情况一样,活动记录模式适用于支撑子域、集成通用子域的外部解决方案或模型转换任务。这两种模式之间的区别在于,活动记录解决了将复杂数据结构映射到数据库模式的复杂性。

The active record pattern is also known as an anemic domain model antipattern; in other words, an improperly designed domain model. I prefer to restrain from the negative connotation of the words anemic and antipattern. This pattern is a tool. Like any tool, it can solve problems, but it can potentially introduce more harm than good when applied in the wrong context. There is nothing wrong with using active records when the business logic is simple. Furthermore, using a more elaborate pattern when implementing simple business logic will also result in harm by introducing accidental complexity. In the next chapter, you will learn what a domain model is and how it differs from an active record pattern.

活动记录模式也被称为贫血领域模型反模式;换句话说,是一个设计不当的领域模型。我更喜欢避免使用“贫血”和“反模式”这些带有负面含义的词汇。这个模式是一个工具。像任何工具一样,它可以解决问题,但是如果没用应用在对的地方,它可能会带来弊大于利的后果。当业务逻辑简单时,使用活动记录是没有问题的。此外,在实现简单的业务逻辑时使用更复杂的模式也会导致引入不必要的复杂性而造成危害。在下一章中,您将了解什么是领域模型以及它与活动记录模式的区别。


NOTE  备注

It’s important to stress that in this context, active record refers to the design pattern, not the Active Record framework. The pattern name was coined in Patterns of Enterprise Application Architecture by Martin Fowler. The framework came later as one way to implement the pattern. In our context, we are talking about the design pattern and the concepts behind it, not a specific implementation.

需要强调的是,在这里,活动记录指的是设计模式,而不是活动记录框架。该模式名称是由Martin Fowler在《企业应用架构模式》一书中提出的。活动记录框架是后来出现的一种实现该模式的方式。在我们的语境中,我们讨论的是该设计模式及其背后的概念,而不是具体的实现。


Be Pragmatic

务实

Although business data is important and the code we design and build should protect its integrity, there are cases in which a pragmatic approach is more desirable.

虽然业务数据很重要,我们设计和编写的代码应该保护其完整性,但有些时候,务实的方法更为可取。

Especially at high levels of scale, there are cases when data consistency guarantees can be relaxed. Check whether corrupting the state of one record out of 1 million is really a showstopper for the business and whether it can negatively affect the performance and profitability of the business. For example, let’s assume you are building a system that ingests billions of events per day from IoT devices. Is it a big deal if 0.001% of the events will be duplicated or lost?

特别是在大规模的情况下,有时可以放宽数据一致性的保证。检查在100万个记录中破坏一个记录的状态是否真的会阻碍业务,以及它是否会对业务的表现和盈利能力产生负面影响。例如,假设你正在构建一个系统,该系统每天从物联网设备中接收数十亿个事件。如果0.001%的事件被重复或丢失,这真的是一个大问题吗?

As always, there are no universal laws. It all depends on the business domain you are working in. It’s OK to “cut corners” where possible; just make sure you evaluate the risks and business implications.

一如既往,没有普遍适用的法则。一切都取决于你所在的业务领域。在可能的情况下“走捷径”是可以的;只要确保你评估了风险和业务影响。

Conclusion

总结

In this chapter, we covered two patterns for implementing business logic:

在本章中,我们介绍了实现业务逻辑的两种模式:

Transaction script  事务脚本

       This pattern organizes the system’s operations as simple, straightforward procedural scripts. The procedures ensure that each operation is transactional—either it succeeds or it fails. The transaction script pattern lends itself to supporting subdomains, with business logic resembling simple, ETL-like operations.      该模式将系统的操作组织成简单、直接的过程脚本。这些过程确保每个操作都是事务性的——要么成功,要么失败。事务脚本模式适用于支撑子域,其业务逻辑类似于简单的ETL(Extract, Transform, Load)操作。

Active record  活动记录

       When the business logic is simple but operates on complicated data structures, you can implement those data structures as active records. An active record object is a data structure that provides simple CRUD data access methods.     当业务逻辑简单但处理复杂的数据结构时,你可以将这些数据结构实现为活动记录。活动记录对象是一个数据结构,提供简单的CRUD(创建、读取、更新、删除)数据访问方法。

The two patterns discussed in this chapter are oriented toward cases of rather simple business logic. In the next chapter, we will turn to more complex business logic and discuss how to tackle the complexity using the domain model pattern.

本章讨论的两种模式适用于相对简单的业务逻辑的情况。在下一章中,我们将转向更复杂的业务逻辑,并讨论如何使用领域模型模式来应对这种复杂性。

Exercises

练习

1. Which of the discussed patterns should be used for implementing a core subdomain’s business logic?  应该使用哪种讨论过的模式来实现核心子域的业务逻辑?

a. Transaction script.  事务脚本

b. Active record.  活动记录

c. Neither of these patterns can be used to implement a core subdomain.  这两种模式都不能用于实现核心子域。

d. Both can be used to implement a core subdomain.  两者都可以用来实现核心子域。

答案:c。Both transaction script and active record lend themselves to the case of simple business logic, whereas core subdomains involve more complex business logic.    事务脚本和活动记录都适用于简单的业务逻辑的情况,而核心子域则涉及更复杂的业务逻辑。

2. Consider the following code:  思考以下代码:

 1 public void CreateTicket(TicketData data)
 2 {
 3  var agent = FindLeastBusyAgent();
 4  agent.ActiveTickets = agent.ActiveTickets + 1;
 5  agent.Save();
 6  var ticket = new Ticket();
 7  ticket.Id = Guid.New();
 8  ticket.Data = data;
 9  ticket.AssignedAgent = agent;
10  ticket.Save();
11  _alerts.Send(agent, "You have a new ticket!");
12 }

Assuming there is no high-level transaction mechanism, what potential data consistency issues can you spot here?

假设没有高级事务机制,你能发现这里存在哪些潜在的数据一致性问题?

a. On receiving a new ticket, the assigned agent’s counter of active tickets can be increased by more than 1.  当收到一张新ticket(工单)时,指定客服的Active Tickets(活动工单)计数器可能增加超过1。

b. An agent’s counter of active tickets can be increased by 1 but the agent won’t get assigned any new tickets.  一个客服的活动工单计数器可以增加1,但该客服不会收到新分配的工单。

c. An agent can get a new ticket but won’t be notified about it.  客服可以分配到一张新工单,但不会收到通知。

d. All of the above issues are possible.  以上所有问题都是可能的。

答案:d。

a. If the execution fails after line 6, the caller retries the operation, and the same agent is chosen by the FindLeastBusyAgent method, the agent’s ActiveTickets counter will be increased by more than 1.    如果在第6行之后执行失败,调用者重试该操作,并且FindLeastBusyAgent方法选择了相同的客服,那么该客服的活动工单计数器将增加超过1。

b. If the execution fails after line 6 but the caller doesn’t retry the operation, the counter will be increased, while the ticket itself won’t be created.    如果在第6行之后执行失败,但是调用者没有重试操作,计数器将增加,而工单本身将不会创建。

c. If the execution fails after line 12, the ticket is created and assigned, but the notification on line 14 won’t be sent.    如果在第12行之后执行失败,将创建并分配工单,但第14行的通知将不会被发送。

3. In the preceding code, there is at least one more possible edge case that can corrupt the system’s state. Can you spot it?  在前面的代码中,至少还有一种可能的边缘情况会破坏系统的状态。你能找到它吗?

答案:If the execution fails after line 12 and the caller retries the operation and it succeeds, the same ticket will be persisted and assigned twice.    如果在第12行之后执行失败,并且调用方重试操作并成功,则同一工单将被持久化并分配两次。

4. Going back to the example of WolfDesk in the book’s Preface, what parts of the system could potentially be implemented as a transaction script or an active record?  回到本书前言中的 WolfDesk 的例子,系统的哪些部分可以作为事务脚本或活动记录实现?

答案:All of WolfDesk’s supporting subdomains are good candidates for implementation as transaction script or active record as their business logic is relatively straightforward:

WolfDesk 的所有支撑子域都很适合作为事务脚本或活动记录实现,因为它们的业务逻辑相对简单:

a. Management of a tenant’s ticket categories    管理租户的工单种类

b. Management of a tenant’s products, regarding which the customers can open support tickets    管理租户的产品(关于客户可以打开的支持工单)

c. Entry of a tenant’s support agents’ work schedules    输入租户的支持客服的工作时间表

posted @ 2022-12-26 17:57  菜鸟吊思  阅读(77)  评论(0)    收藏  举报