烂翻译系列之学习领域驱动设计——应用 DDD: 一个案例研究
In this appendix, I will share how my domain-driven design journey started: the story of a start-up company that, for the purposes of this example, we’ll refer to as “Marketnovus.” At Marketnovus, we had been employing DDD methodology since the day the company was founded. Over the years, not only had we committed every possible DDD mistake, but we also had the opportunity to learn from those mistakes and fix them. I will use this story and the mistakes we made to demonstrate the role that DDD patterns and practices play in the success of a software project.
在本附录中,我将分享我的领域驱动设计(Domain-Driven Design,简称DDD)之旅是如何开始的:这是一个初创公司的故事,为了本例的说明,我们将它称为“Marketnovus”。在Marketnovus,自公司成立之日起,我们就一直采用DDD方法。多年来,我们不仅犯下了所有可能的DDD错误,还有机会从这些错误中吸取教训并加以改正。我将通过这个故事和我们所犯的错误来展示DDD模式和实践在软件项目成功中所扮演的角色。
This case study consists of two parts. In the Part I, I’ll walk you through the stories of five of Marketnovus’s bounded contexts, what design decisions were made, and what the outcomes were. In the second part, I will discuss how these stories reflect the material you learned in this book.
这个案例研究分为两部分。在第一部分中,我将带您了解Marketnovus的五个有界上下文(Bounded Contexts)的故事,包括所做的设计决策以及结果如何。在第二部分中,我将讨论这些故事如何反映您在这本书中所学的内容。
Before we begin, I need to stress that Marketnovus doesn’t exist anymore. As a result, this appendix is in no way promotional. Furthermore, since this is a defunct company, I’m free to speak honestly about our experiences.
在我们开始之前,我需要强调Marketnovus已经不复存在了。因此,这个附录绝不是为了宣传。此外,由于这是一家倒闭的公司,我可以自由地讲述我们的经历。
Five Bounded Contexts
五个有界上下文
Before we delve into the bounded contexts and how they were designed, as well-behaved DDD practitioners we have to start by defining Marketnovus’s business domain.
在我们深入探讨有界上下文及其设计方式之前,作为遵循良好实践领域的DDD从业者,我们首先需要定义Marketnovus的业务领域。
Business Domain
业务领域
Imagine you are producing a product or a service. Marketnovus allowed you to outsource all of your marketing-related chores. Marketnovus’s experts would come up with a marketing strategy for your product. Its copywriters and graphic designers would produce tons of creative material, such as banners and landing pages, that would be used to run advertising campaigns promoting your product. All the leads generated by these campaigns would be handled by Marketnovus’s sales agents, who would make the calls and sell your product. This process is depicted in Figure A-1.
想象一下,你正在生产一种产品或提供一种服务。Marketnovus可以让你将所有与营销相关的工作外包出去。Marketnovus的专家会为你的产品制定营销策略。他们的文案人员和图形设计师会制作大量的创意素材,如横幅和登录页,用来开展广告宣传活动(推广你的产品)。这些活动产生的所有潜在客户都将由Marketnovus的销售代理处理,他们会打电话并推销你的产品。这个过程如图A-1所示。
Figure A-1. Marketing process
图 A-1. 市场营销过程
Most importantly, this marketing process provided many opportunities for optimization, and that’s exactly what the analysis department was in charge of. They analyzed all the data to make sure Marketnovus and its clients were getting the biggest bang for their buck, whether by pinpointing the most successful campaigns, celebrating the most effective creatives, or ensuring that the sales agents were working on the most promising leads.
最重要的是,这个营销过程提供了许多优化的机会,这正是分析部门所负责的。他们分析所有数据,以确保Marketnovus及其客户能够以最少的投入获得最大的回报,无论是通过挑选最成功的活动,还是表彰最有效的创意,或是确保销售代表正在处理最有前景(最有希望)的潜在客户。
Since we were a self-funded company, we had to get rolling as fast as possible. As a result, right after the company was founded, the first version of our software system had to implement the first one-third of our value chain:
由于我们是一家自筹资金的公司,我们必须尽快启动。因此,在公司成立后不久,我们的软件系统的第一个版本就必须实现我们价值链的前三分之一:
- A system for managing contracts and integrations with external publishers 一个管理合同和与外部出版商集成的系统
- A catalog for our designers to manage creative materials 一个供我们的设计师管理创意素材的目录
- A campaign management solution to run advertising campaigns 一个开展广告活动的广告活动管理解决方案
I was overwhelmed and had to find a way to wrap my head around all the complexities of the business domain. Fortunately, not long before we started working, I read a book that promised just that. Of course, I’m talking about Eric Evans’s seminal work, Domain-Driven Design: Tackling Complexity at the Heart of Software.
我感到不知所措,必须找到一种方法来处理业务领域的所有复杂性。幸运的是,在我们开始工作之前不久,我读了一本书,它正好解决了这个问题。当然,我说的是埃里克·埃文斯(Eric Evans)的开创性著作《领域驱动设计:软件核心复杂性应对之道》。
If you have read this book’s Preface, you know Evans’s book provided the answers I’d been seeking for quite a while: how to design and implement business logic. That said, for me it wasn’t an easy book to comprehend on the first read. Nevertheless, I felt like I’d already gotten a strong grasp of DDD just by reading the tactical design chapters.
如果你读过这本书的序言,你就会知道埃文斯的书提供了我一直在寻找的答案:如何设计和实现业务逻辑。不过,对我来说,这本书并不是一读就懂的。然而,仅仅通过阅读战术设计章节,我就感觉自己已经对DDD有了深刻的理解。
Guess how the system was initially designed? It would definitely make a certain prominent individual from the DDD community very proud.
猜猜系统最初是如何设计的?这肯定会让DDD社区中的某位杰出人士感到非常自豪。
Bounded Context #1: Marketing
有界上下文 # 1:Marketing(营销)
The architectural style of our first solution could be neatly summarized as “aggregates everywhere.” Agency, campaign, placement, funnel, publisher: each and every noun in the requirements was proclaimed as an aggregate.
我们第一个解决方案的架构风格可以简洁地概括为“聚合无处不在”。广告商、广告推广活动、广告位(网站放置广告的位置。广告位的信息通常会被追踪并且记录,以便于我们对广告活动进行优化)、广告活动漏斗(一个用户从广告、着陆页、到国外网赚项目任务和或联盟任务页面的完整体验)、媒体(流量主,拥有流量需要销售的人,比如网站站长、APP所有者等。流量主加入流量平台从而把广告位置卖给营销人员)需求中的每一个名词都被声明为一个聚合。
All of those so-called aggregates resided in a huge, lone, bounded context. Yes, a big, scary monolith, the kind everyone warns you about nowadays.
所有这些所谓的聚合都驻留在一个庞大而孤立的有界上下文中。是的,一个庞大而可怕的单体,正是如今每个人都警告你要避免的那种。
And of course, those were no aggregates. They didn’t provide any transactional boundaries, and they had almost no behavior in them. All the business logic was implemented in an enormous service layer.
当然,那些都不是真正的聚合。它们没有提供任何事务边界,而且几乎没有任何行为。所有的业务逻辑都实现在一个庞大的服务层中。
When you aim to implement a domain model but end up with the active record pattern, it is often termed an “anemic domain model” antipattern. In hindsight, this design was a by-the-book example of how not to implement a domain model. However, things looked quite different from a business standpoint.
当你试图实现一个领域模型,但最终却得到了活动记录模式时,这通常被称为“贫血领域模型”反模式。回想起来,这个设计是一个典型的反面教材,展示了如何不实现领域模型。然而,从商业角度来看,情况却大不相同。
From the business’s point of view, this project was considered a huge success! Despite the flawed architecture, we were able to deliver working software in a very aggressive time to market. How did we do it?
从商业角度来看,这个项目被认为是一个巨大的成功!尽管架构存在缺陷,但我们还是能够在非常紧迫的时间内将可工作软件推向市场。我们是怎么做到的?
A kind of magic
一种魔力
We somehow managed to come up with a robust ubiquitous language. None of us had any prior experience in online marketing, but we could still hold a conversation with domain experts. We understood them, they understood us, and to our astonishment, domain experts turned out to be very nice people! They genuinely appreciated the fact that we were willing to learn from them and their experience.
我们不知怎么地就形成了一种强大的通用语言。我们之中没有一个人有在线营销方面的经验,但我们仍然能够与领域专家进行对话。我们理解他们,他们也理解我们,令我们惊讶的是,这些领域专家竟然都是非常好的人!他们由衷地欣赏我们愿意向他们请教,学习他们的经验。
The smooth communication with the domain experts allowed us to grasp the business domain in no time and implement its business logic. Yes, it was a pretty big monolith, but for two developers in a garage, it was just good enough. Again, we produced working software in a very aggressive time to market.
与领域专家的顺畅沟通使我们能够迅速掌握业务领域并实现其业务逻辑。是的,它确实是一个相当大的单体,但对于车库里的两名开发人员来说,它已经足够好了。再次强调,我们在非常紧迫的时间内生产出了可以工作的软件。
Our early understanding of domain-driven design
我们对领域驱动设计的早期认识
Our understanding of domain-driven design at this stage could be represented with the simple diagram shown in Figure A-2.
我们在这个阶段对领域驱动设计的理解可以用图A-2中的简单图表来表示。
Figure A-2. Our early understanding of domain-driven design
图 A-2. 我们早期对领域驱动设计的理解
Bounded Context #2: CRM
有界上下文#2:CRM(客户关系管理)
Soon after we deployed the campaign management solution, leads started flowing in, and we were in a rush. Our sales agents needed a robust customer relationship management (CRM) system to manage the leads and their lifecycles.
在我们部署了广告活动管理解决方案后不久,潜在客户开始涌入,我们忙得不可开交。我们的销售代表需要一个强大的客户关系管理(CRM)系统来管理潜在客户及其生命周期。
The CRM had to aggregate all incoming leads, group them based on different parameters, and distribute them across multiple sales desks around the globe. It also had to integrate with our clients’ internal systems, both to notify the clients about changes in the leads’ lifecycles and to complement our leads with additional information. And, of course, the CRM had to provide as many optimization opportunities as possible. For example, we needed to be able to make sure the agents were working on the most promising leads, assign leads to agents based on their qualifications and past performance, and allow a very flexible solution for calculating agents’ commissions.
CRM系统必须汇总所有进入的潜在客户,根据不同的参数将它们分组,并将它们分配给全球各地的多个销售部门。它还必须与客户的内部系统集成,以便通知客户潜在客户生命周期中的变化,并补充我们的潜在客户信息。当然,CRM系统还必须提供尽可能多的优化机会。例如,我们需要能够确保销售代表正在处理最有希望的潜在客户,根据销售代表的资质和过去的表现分配潜在客户给销售代表,并允许非常灵活地计算销售代表的佣金。
Since no off-the-shelf product fit our requirements, we decided to roll out our own CRM system.
由于没有任何现成的产品符合我们的要求,我们决定推出自己的CRM系统。
More “aggregates”!
更多的“聚合”!
The initial implementation approach was to continue focusing on the tactical patterns. Again, we pronounced every noun as an aggregate and shoehorned them into the same monolith. This time, however, something felt wrong right from the start.
最初的实现方法是持续专注于战术模式。同样,我们把每个名词都声明成一个聚合,并把它们硬塞在一起。然而,这一次,从一开始就感觉有些不对劲。
We noticed that, all too often, we were adding awkward prefixes to those “aggregates” names: for example, CRMLead and MarketingLead, MarketingCampaign and CRMCampaign. Interestingly, we never used those prefixes in our conversations with the domain experts. Somehow, they always understood the meaning from the context.
我们注意到,我们经常在那些“聚合”名称前加上笨拙的前缀,例如CRMLead和MarketingLead,MarketingCampaign和CRMCampaign。有趣的是,在与领域专家的对话中,我们从未使用过这些前缀。不知怎的,他们总是能从上下文中理解其含义。
Then I recalled that domain-driven design has a notion of bounded contexts that we had been ignoring so far. After revisiting the relevant chapters of Evans’s book, I learned that bounded contexts solve exactly the same issue we were experiencing: they protect the consistency of the ubiquitous language. Furthermore, by that time, Vaughn Vernon had published his “Effective Aggregate Design” paper. The paper made explicit all the mistakes we were making when designing the aggregates. We were treating aggregates as data structures, but they play a much larger role by protecting the consistency of the system’s data.
然后我想起领域驱动设计有一个我们迄今为止一直忽略的概念,那就是有界上下文。在重新阅读了埃文斯(Evans)书中的相关章节后,我了解到有界上下文正是解决我们所遇到问题的关键:它们保护通用语言的一致性。此外,到那时,沃恩·弗农(Vaughn Vernon)已经发表了他的《有效的聚合设计》论文。该论文明确指出了我们在设计聚合时犯下的所有错误。我们一直在将聚合视为数据结构,但它们在保护系统数据一致性方面发挥着更大的作用。
We took a step back and redesigned the CRM solution to reflect these revelations.
我们退后一步,重新设计了CRM解决方案以反映这些启示。
Solution design: Take two
解决方案设计: 第二次
We started by dividing our monolith into two distinct bounded contexts: marketing and CRM. Of course, we didn’t go all the way to microservices here; we just did the bare minimum to protect the ubiquitous language.
我们首先将单体划分为两个不同的有界上下文:营销和CRM。当然,我们并没有完全采用微服务的方式;我们只是做了最基本的保护通用语言的工作。
However, in the new bounded context, the CRM, we were not going to repeat the same mistakes we made in the marketing system. No more anemic domain models! Here we would implement a real domain model with real, by-the-book aggregates. In particular, we vowed that:
然而,在新的限界上下文CRM中,我们不会重复在营销系统中犯下的同样错误。不再会有贫血的领域模型!在这里,我们将实现一个真正的领域模型,其中包含真正的、按书中所写的聚合。特别是,我们发誓要:
- Each transaction would affect only one instance of an aggregate. 每个事务只会影响一个聚合实例。
- Instead of an ORM, each aggregate itself would define the transactional scope. 每个聚合本身将定义事务范围,而不是使用ORM。
- The service layer would go on a very strict diet, and all the business logic would be refactored into the corresponding aggregates. 服务层将进行严格限制,所有业务逻辑都将重构到相应的聚合中。
We were so enthusiastic about doing things the right way. But, soon enough, it became apparent that modeling a proper domain model is hard!
我们非常热衷于用正确的方式做事。但是,很快就发现,建模一个恰当的领域模型是困难的!
Relative to the marketing system, everything took much more time! It was almost impossible to get the transactional boundaries right the first time. We had to evaluate at least a few models and test them, only to figure out later that the one we hadn’t thought about was the correct one. The price of doing things the “right” way was very high: lots of time.
与营销系统相比,一切都需要花费更多的时间!几乎不可能第一次就正确设置事务边界。我们不得不评估至少几个模型并进行测试,后来才发现我们没想到的那个模型才是正确的。以“正确”的方式做事的代价是非常高的:需要花费大量时间。
Soon it became obvious to everyone that there was no chance in hell we would meet the deadlines! To help us out, management decided to offload implementation of some of the features to…the database administrators team.
很快,每个人都清楚地意识到,我们绝对不可能按时完成工作!为了帮助我们,管理层决定将一些功能的实现工作交给...数据库管理员团队。
Yes, to implement the business logic in stored procedures.
是的,在存储过程中实现业务逻辑。
This one decision resulted in much damage down the line. Not because SQL is not the best language for describing business logic. No, the real issue was a bit more subtle and fundamental.
这一决定在后续造成了很大的损害。这并不是因为SQL不是描述业务逻辑的最佳语言。不,真正的问题更加微妙和本质。
Tower of Babel 2.0
巴比伦通天塔 2.0
This situation produced an implicit bounded context whose boundary dissected one of our most complex business entities: the Lead.
这种情况产生了一个隐式的有界上下文,其边界划分了我们最复杂的业务实体之一:潜在客户。
The result was two teams working on the same business component and implementing closely related features, but with minimal interaction between them. Ubiquitous language? Give me a break! Literally, each team had its own vocabulary to describe the business domain and its rules.
结果是,两个团队在同一个业务组件上工作,实现密切相关的功能,但彼此之间的交流却很少。通用语言?饶了我吧!从字面上看,每个团队都有自己的一套词汇来描述业务领域及其规则。
The models were inconsistent. There was no shared understanding. Knowledge was duplicated, and the same rules were implemented twice. Rest assured, when the logic had to change, the implementations went out of sync immediately.
模型不一致。没有共同的理解。知识被重复,同样的规则被实现了两次。请放心,当逻辑需要改变时,实现会立即变得不同步。
Needless to say, the project wasn’t delivered anywhere near on time, and it was full of bugs. Nasty production issues that had flown under the radar for years corrupted our most precious asset: our data.
不用说,项目根本没有按时完成,而且到处都是漏洞。多年来一直未被发现的严重生产问题破坏了我们最宝贵的资产:我们的数据。
The only way out of this mess was to completely rewrite the Lead aggregate, this time with proper boundaries, which we did a couple of years later. It wasn’t easy, but the mess was so bad there was no other way around it.
摆脱这种困境的唯一方法就是彻底重写潜在客户聚合,这一次我们设定了正确的边界,这在几年后才得以实现。这并不容易,但混乱到如此程度,我们别无选择。
A broader understanding of domain-driven design
对领域驱动设计有更广泛的了解
Even though this project failed pretty miserably by business standards, our understanding of domain-driven design evolved a bit: build a ubiquitous language, protect its integrity using bounded contexts, and instead of implementing an anemic domain model everywhere, implement a proper domain model everywhere. This model is shown in Figure A-3.
尽管这个项目在商业标准上彻底失败了,但我们对领域驱动设计的理解却有所进步:构建一种通用语言,使用有界上下文来保护其完整性,并在各处实现一个恰当的领域模型,而不是到处都实现一个贫血的领域模型。这个模型如图A-3所示。
Figure A-3. Introduction of strategic design concepts into our understanding of domain-driven design
图 A-3. 将战略设计概念引入我们对领域驱动设计的理解中
Of course, a crucial part of domain-driven design was missing here: subdomains, their types, and how they affect a system’s design.
当然,这里缺少了领域驱动设计的一个关键部分: 子域,它们的类型,以及它们如何影响系统的设计。
Initially we wanted to do the best job possible, but we ended up wasting time and effort on building domain models for supporting subdomains. As Eric Evans put it, not all of a large system will be well designed. We learned that the hard way, and we wanted to use the acquired knowledge in our next project.
最初,我们想要尽可能做好工作,但最终却在为支撑子域构建领域模型上浪费了时间和精力。正如埃里克·埃文斯(Eric Evans)所说,大型系统并非所有部分都会设计得很好。我们经历了艰难的过程才学到这一点,并希望将所学知识应用到下一个项目中。
Bounded Context #3: Event Crunchers
有界上下文 #3:Event Crunchers(事件处理器)
After the CRM system was rolled out, we suspected that an implicit subdomain was spread across marketing and CRM. Whenever the process of handling incoming customer events had to be modified, we had to introduce changes both in the marketing and CRM bounded contexts.
在CRM系统推出后,我们怀疑在营销和CRM之间存在一个隐性的子域。每当需要修改处理传入客户事件的流程时,我们都必须在营销和CRM的有界上下文中进行更改。
Since conceptually this process didn’t belong to any of them, we decided to extract this logic into a dedicated bounded context called “event crunchers,” shown in Figure A-4.
由于从概念上讲,这个过程不属于它们中的任何一个,因此我们决定将这个逻辑提取到一个专用的有界上下文中,称为“事件处理器”,如图A-4所示。
Figure A-4. The event crunchers bounded context handling the incoming customer events
图 A-4。处理传入客户事件的事件处理器有界上下文
Since we didn’t make any money out of the way we move data around, and there weren’t any off-the-shelf solutions that could have been used, event crunchers resembled a supporting subdomain. We designed it as such.
由于我们没有从数据传输中获利,也没有任何现成的解决方案可以使用,事件处理器就像一个支撑子域。我们就是这么设计的。
Nothing fancy this time: just layered architecture and some simple transaction scripts. This solution worked great, but only for a while.
这次没什么特别的:只是分层架构和一些简单的事务脚本。这个解决方案效果很好,但只是暂时的。
As our business evolved, we implemented more and more features in the event crunchers. It started with business intelligence (BI) people asking for some flags: a flag to mark a new contact, another one to mark various first-time events, some more flags to indicate some business invariants, and so on.
随着我们业务的发展,我们在事件处理器中实现了越来越多的功能。最初,商业智能(BI)人员要求添加一些标志:一个标志用于标记新联系人,另一个标志用于标记各种首次发生的事件,还有一些其他标志用于指示一些业务不变性等。
Eventually, those simple flags evolved into a real business logic, with complex rules and invariants. What started out as transaction scripts evolved into a full-fledged core business subdomain.
最终,这些简单的标志演变成了真正的业务逻辑,包含了复杂的规则和不变性。最初作为事务脚本开始的内容逐渐演变为一个完整的核心业务子域。
Unfortunately, nothing good happens when you implement complex business logic as transaction scripts. Since we didn’t adapt our design to cope with the complex business logic, we ended up with a very big ball of mud. Each modification to the codebase became more and more expensive, quality went downhill, and we were forced to rethink the event crunchers design. We did that a year later. By that time, the business logic had become so complex that it could only be tackled with event sourcing. We refactored the event crunchers’ logic into an event-sourced domain model, with other bounded contexts subscribing to its events.
不幸的是,当你将复杂的业务逻辑作为事务脚本来实现时,结果往往并不理想。由于我们没有调整设计以适应复杂的业务逻辑,最终我们得到了一个非常大的泥球。每次对代码库进行修改都变得越来越成本高昂,质量每况愈下,我们不得不重新考虑事件处理器的设计。我们在一年后这样做了。到那时,业务逻辑已经变得如此复杂,以至于只能通过事件溯源来解决。我们将事件处理器的逻辑重构为事件溯源领域模型,其他有界上下文订阅其事件。
Bounded Context #4: Bonuses
有界上下文 #4:Bonuses(奖金)
One day, the sales desk managers asked us to automate a simple yet tedious procedure they had been doing manually: calculate the commissions for the sales agents.
有一天,销售部经理让我们自动化一个简单但乏味的程序,之前他们一直在手工执行:计算销售代理的佣金。
Again, it started out simple: once a month, just calculate a percentage of each agent’s sales and send the report to the managers. As before, we contemplated whether this was a core subdomain. The answer was no. We weren’t inventing anything new, weren’t making money out of this process, and if it was possible to buy an existing implementation, we definitely would. Not core, not generic, but another supporting subdomain.
同样,这个任务起初也很简单:每月只需计算每位销售代理销售额的百分比,并将报告发送给管理人员。和之前一样,我们思考这是否是一个核心子域。答案是否定的。我们并没有发明任何新东西,也没有从这个过程中营利,如果有可能购买现有的实现方案,我们肯定会这么做。因此,这既不是一个核心子域,也不是一个通用子域,而是另一个支撑子域。
We designed the solution accordingly: active record objects, orchestrated by a “smart” service layer, as shown in Figure A-5.
我们相应地设计了解决方案:由“智能”服务层编排的活动记录对象,如图 A-5所示。
Figure A-5. The bonuses bounded context implemented using the active record and layered architecture patterns
图 A-5。使用活动记录和分层架构模式实现的bonuses(奖金)有界上下文
Once the process became automated, boy, did everyone in the company become creative about it. Our analysts wanted to optimize the heck out of this process. They wanted to try out different percentages, tie percentages to sales amounts and prices, unlock additional commissions for achieving different goals, and on and on. Guess when the initial design broke down?
一旦这个过程实现了自动化,公司里的每个人都变得非常有创造力。我们的分析师想要彻底优化这个过程。他们想要尝试不同的百分比,将百分比与销售金额和价格挂钩,为达到不同目标而解锁额外的佣金,等等。猜猜看,最初的设计是在什么时候崩溃的?
Again, the codebase started turning into an unmanageable ball of mud. Adding new features became more and more expensive, bugs started to appear—and when you’re dealing with money, even the smallest bug can have BIG consequences.
再一次,代码库开始变成一个难以管理的泥球。添加新功能的成本越来越高,开始出现错误——而当你处理金钱时,即使是最小的错误也可能产生严重的后果。
Design: Take two
设计: 第二次
As with the event crunchers project, at some point we couldn’t bear it anymore. We had to throw away the old code and rewrite the solution from the ground up, this time as an event-sourced domain model.
就像事件处理器项目一样,在某个时候,我们再也无法忍受了。我们不得不扔掉旧代码,从头开始重写解决方案,这次是一个事件溯源领域模型。
And just as in the event crunchers project, the business domain was initially categorized as a supporting one. As the system evolved, it gradually mutated into a core subdomain: we found ways to make money out of these processes. However, there is a striking difference between these two bounded contexts.
就像在事件处理器项目中一样,业务领域最初被归类为支撑子域。随着系统的演变,它逐渐演变为一个核心子域:我们找到了从这些过程中获利的方法。然而,这两个有界上下文之间存在显著差异。
Ubiquitous language
通用语言
For the bonuses project, we had a ubiquitous language. Even though the initial implementation was based on active records, we could still have a ubiquitous language.
在奖金项目中,我们有一种通用语言。尽管最初的实现是基于活动记录的,但我们仍然可以有一种通用语言。
As the domain’s complexity grew, the language used by the domain experts got more and more complicated as well. At some point, it could no longer be modeled using active records! This realization allowed us to notice the need for a change in the design much earlier than we did in the event crunchers project. We saved a lot of time and effort by not trying to fit a square peg into a round hole, thanks to the ubiquitous language.
随着领域复杂性的增加,领域专家所使用的语言也变得越来越复杂。在某个时候,它已经无法再使用活动记录进行建模了!这一认识使我们比在处理事件处理器项目时更早地注意到了设计更改的必要性。由于有了通用语言,我们避免了将方榫头插入圆孔中的尝试,从而节省了大量时间和精力。
A classic understanding of domain-driven design
对领域驱动设计的典型理解
At this point, our understanding of domain-driven design had finally evolved into a classic one: ubiquitous language, bounded contexts, and different types of subdomains, each designed according to its needs, as shown in Figure A-6.
此时,我们对领域驱动设计的理解终于演变成了经典的理解:通用语言、有界上下文以及不同类型的子域,每个子域都根据其需求进行设计,如图A-6所示。
Figure A-6. A classic model of domain-driven design
图A-6. 经典的领域驱动设计模型
However, things took quite an unexpected turn for our next project.
然而,我们的下一个项目发生了意想不到的变化。
Bounded Context #5: The Marketing Hub
有界上下文 #5:The Marketing Hub(营销中心)
Our management was looking for a profitable new vertical. They decided to try using our ability to generate a massive number of leads and sell them to smaller clients, ones we hadn’t worked with before. This project was called “marketing hub.”
我们的管理层正在寻找一个有利可图的新领域。他们决定尝试利用我们产生大量潜在客户的能力,并将这些潜在客户出售给之前未曾合作过的小客户。这个项目被称为“营销中心”。
Since management had defined this business domain as a new profit opportunity, it was clearly a core business domain. Hence, designwise, we pulled out the heavy artillery: event-sourced domain model and CQRS. Also, back then, a new buzzword, microservices, started gaining lots of traction. We decided to give it a try.
由于管理层已将此业务领域定义为新的盈利机会,因此它显然是一个核心业务领域。因此,在设计方面,我们动用了重型武器:事件溯源领域模型和CQRS。此外,当时,一个新流行语“微服务”开始受到广泛关注。我们决定尝试一下。
Our solution looked like the implementation shown in Figure A-7.
我们的解决方案看起来如图A-7所示的实现。
Figure A-7. A microservices-based implementation of the marketing hub bounded context
图A-7. 基于微服务的营销中心有界上下文实现
Small services, each having its own database, with both synchronous and asynchronous communication between them: on paper, it looked like a perfect solution design. In practice, not so much.
小型服务,每个服务都有自己的数据库,它们之间既有同步通信也有异步通信:从纸面上看,这似乎是一个完美的解决方案设计。但在实践中,并非如此。
Micro what?
微什么?
We näively approached microservices thinking that the smaller the service was, the better. So we drew service boundaries around the aggregates. In DDD lingo, each aggregate became a bounded context on its own.
我们天真地看待微服务,认为服务越小越好。因此,我们在围绕聚合划分了服务边界。在DDD术语中,每个聚合都成为了自己的有界上下文。
Again, initially this design looked great. It allowed us to implement each service according to its specific needs. Only one would be using event sourcing, and the rest would be state-based aggregates. Moreover, all of them could be maintained and evolved independently.
同样,最初这个设计看起来很棒。它允许我们根据每个服务的特定需求来实现它们。只有一个会使用事件溯源,其余的将是基于状态的聚合。此外,它们都可以独立维护和演进。
However, as the system grew, those services became more and more chatty. Eventually, almost each service required data from all the other services to complete some of its operations. The result? What was intended to be a decoupled system ended up being a distributed monolith: an absolute nightmare to maintain.
然而,随着系统的增长,这些服务之间的通信变得越来越频繁。最终,几乎每个服务都需要来自其他所有服务的数据才能完成某些操作。结果如何?一个本应是解耦的系统最终变成了一个分布式单体:维护起来绝对是一场噩梦。
Unfortunately, there was another, much more fundamental issue we had with this architecture. To implement the marketing hub, we used the most complex patterns for modeling the business domain: domain model and event-sourced domain model. We carefully crafted those services. But it all was in vain.
不幸的是,我们在这个架构上遇到了另一个更为根本的问题。为了实现营销中心,我们使用了最复杂的业务领域建模模式:领域模型和事件溯源领域模型。我们精心设计了这些服务。但这一切都是徒劳的。
The real problem
真正的问题
Despite the fact that the business considered the marketing hub to be a core subdomain, it had no technical complexity. Behind that complex architecture stood a very simple business logic, one so simple that it could have been implemented using plain active records.
尽管管理层认为营销中心是一个核心子域,但它并没有技术复杂性。在这个复杂的架构背后,隐藏着非常简单的业务逻辑,简单到可以使用普通的活动记录来实现。
As it turned out, the businesspeople were looking to profit by leveraging our existing relationships with other companies, and not through the use of clever algorithms.
事实证明,这些商人希望通过利用我们与其他公司的现有关系来获利,而不是通过使用巧妙的算法。
The technical complexity ended up being much higher than the business complexity. To describe such discrepancies in complexities, we use the term accidental complexity, and our initial design ended up being exactly that. The system was overengineered.
技术复杂性最终远高于业务复杂性。为了描述这种复杂性之间的差异,我们使用了“偶然复杂性”这个术语,而我们最初的设计正是如此。系统被过度设计了。
Discussion
论述
Those were the five bounded contexts I wanted to tell you about: marketing, CRM, event crunchers, bonuses, and marketing hub. Of course, such a wide business domain as Marketnovus entailed many more bounded contexts, but I wanted to share the bounded contexts we learned from the most.
这就是我想要告诉你的五个有界上下文: marketing(市场营销)、CRM(客户关系管理)、event crunchers(事件处理)、bonuses(奖金)和marketing hub(营销中心)。当然,像Marketnovus这样广泛的业务领域涉及许多其他有界上下文,但我想分享我们从中学到最多东西的有界上下文。
Now that we’ve walked through the five bounded contexts, let’s look at this from a different perspective. How did application or misapplication of core elements of domain-driven design influence our outcomes? Let’s take a look.
既然我们已经了解了这五个有界上下文,那么让我们从另一个角度来看待这个问题。领域驱动设计的核心元素的应用或误用是如何影响我们的结果呢?让我们来看一下。
Ubiquitous Language
通用语言
In my experience, ubiquitous language is the “core subdomain” of domain-driven design. The ability to speak the same language with our domain experts has been indispensable to us. It turned out to be a much more effective way to share knowledge than tests or documents.
根据我的经验,通用语言是领域驱动设计的“核心子域”。与领域专家使用相同的语言对我们来说至关重要。事实证明,这比测试或文档更加有效地分享知识。
Moreover, the presence of a ubiquitous language has been a major predictor of a project’s success for us:
此外,通用语言的存在一直是我们预测项目成功的重要指标:
- When we started, our implementation of the marketing system was far from perfect. However, the robust ubiquitous language compensated for the architectural shortcomings and allowed us to deliver the project’s goals. 当我们开始时,我们的营销系统实现远非完美。然而,强大的通用语言弥补了架构上的不足,使我们能够完成项目目标。
- In the CRM context, we screwed it up. Unintentionally, we had two languages describing the same business domain. We strived to have a proper design, but because of the communication issues we ended up with a huge mess. 在CRM上下文中,我们搞砸了。无意中,我们使用了两种语言来描述同一个业务领域。我们努力追求一个适当的设计,但由于沟通问题,最终陷入了一片混乱。
- The event crunchers project started as a simple supporting subdomain, and we didn’t invest in the ubiquitous language. We regretted this decision big time when the complexity started growing. It would have taken us much less time if we initially started with a ubiquitous language. event crunchers(事件处理器)事件处理器项目最初是作为一个简单的支撑子域开始的,我们没有在通用语言上进行投资。当复杂性开始增加时,我们非常后悔这个决定。如果我们最初就开始使用通用语言,那么将节省我们大量时间。
- In the bonuses project, the business logic became more complex by orders of magnitude, but the ubiquitous language allowed us to notice the need for a change in the implementation strategy much earlier. 在bonuses(奖金)项目中,业务逻辑的复杂性增加了几个数量级,但通用语言使我们能够更早地注意到需要改变实现策略。
Hence, ubiquitous language is not optional, regardless of whether you’re working on a core, supporting, or generic subdomain.
因此,无论您是在处理核心子域、支撑子域还是通用子域,通用语言都是必要的。
We learned the importance of investing in the ubiquitous language as early as possible. It requires immense effort and patience to “fix” a language if it has been spoken for a while in a company (as was the case with our CRM system). We were able to fix the implementation. It wasn’t easy, but eventually we did it. That’s not the case, however, for the language. For years, some people were still using the conflicting terms defined in the initial implementation.
我们尽可能早地认识到为通用语言投入(投入时间和精力)的重要性。如果一种语言在一家公司已经使用了一段时间(我们的 CRM 系统就是这种情况),那么“修正”这种语言就需要巨大的努力和耐心。我们修正了实现。这并不容易,但最终我们做到了。然而,对于语言来说,情况并非如此。多年来,一些人仍在使用最初实现中定义的相互冲突的术语。
Subdomains
子域
As you learned in Chapter 1, there are three types of subdomains— core, supporting, and generic—and it’s important to identify the subdomains at play when designing the solution.
正如您在第1章中了解到的,有三种类型的子域——核心子域,支撑子域和通用子域——在设计解决方案时,识别这些子域是非常重要的。
It can be challenging to identify a subdomain’s type. As we discussed in Chapter 1, it’s important to identify the subdomains at the granularity level that is relevant to the software system you are building. For example, our marketing hub initiative was intended to be the company’s additional profit source. However, the software aspect of this functionality was a supporting subdomain, while leveraging the relationships and contracts with other companies was the actual competitive advantage, the real core subdomain.
识别子域的类型可能具有挑战性。正如我们在第1章中讨论的那样,重要的是要在与您正在构建的软件系统相关的粒度级别上识别子域。例如,我们的营销中心计划旨在成为公司的额外利润来源。然而,该功能的软件方面是一个支撑子域,而利用与其他公司的关系和合同才是真正的竞争优势,是真正的核心子域。
Furthermore, as you learned in Chapter 11, it’s not enough to identify a subdomain’s type. You also have to be aware of the possible evolutions of the subdomain into another type. At Marketnovus, we witnessed almost all the possible combinations of changes in subdomain types:
此外,正如你在第11章中学到的那样,仅仅识别子域的类型是不够的。你还必须意识到子域可能演变为另一种类型的可能性。在Marketnovus,我们见证了子域类型变化中几乎所有可能的组合:
- Both the event crunchers and bonuses started as supporting subdomains, but once we discovered ways to monetize these processes, they became our core subdomains. 事件处理器和奖金最初都是作为支撑子域开始的,但一旦我们发现了从这些过程中获利的方法,它们就变成了我们的核心子域。
- In the marketing context, we implemented our own creative catalog. There was nothing really special or complex about it. However, a few years later, an open source project came out that offered even more features than we originally had. Once we replaced our implementation with this product, the supporting subdomain became a generic one. 在marketing(营销)上下文中,我们实现了自己的创意目录。它并没有什么特别或复杂的地方。然而,几年后,一个开源项目出现了,它提供了比我们最初拥有的更多的功能。一旦我们用该产品替换了我们的实现,支撑子域就变成了通用子域。
- In the CRM context, we had an algorithm that identified the most promising leads. We refined it over time and tried different implementations, but eventually it was replaced with a machine learning model running in a cloud vendor’s managed service. Technically, a core subdomain became generic. 在CRM上下文中,我们有一个算法来识别最有希望的潜在客户。我们随着时间的推移对其进行了优化,并尝试了不同的实现方式,但最终它被云供应商托管服务中运行的机器学习模型所取代。从技术上讲,一个核心子域变成了通用子域。
- As we’ve seen, our marketing hub system started as a core, but ended up being a supporting subdomain, since the competitive edge resided in a completely different dimension. 正如我们所见,我们的营销中心系统最初是一个核心子域,但最终成为了一个支撑子域,因为竞争优势存在于一个完全不同的维度上。
As you’ve learned throughout this book, the subdomain types affect a wide range of design decisions. Failing to properly identify a subdomain can be a costly mistake as, for example, in the case of the event crunchers and the marketing hub.
正如您在本书中了解到的,子域类型对设计决策影响深远。未能正确识别子域可能是一个代价高昂的错误,例如,就event crunchers(事件处理器)和marketing hub(营销中心)来说。
Mapping design decisions to subdomains
将设计决策映射到子域
Here is a trick I came up with at Marketnovus to foolproof the identification of subdomains: reverse the relationship between subdomains and tactical design decisions. Choose the business logic implementation pattern. No speculation or gold plating; simply choose the pattern that fits the requirements at hand. Next, map the chosen pattern to a suitable subdomain type. Finally, verify the identified subdomain type with the business vision.
在Marketnovus,我想出了一个确保子域识别万无一失的技巧:颠倒子域和战术设计决策之间的关系。选择业务逻辑实现模式。不要投机取巧或过度美化;只需选择适合当前需求的模式。接下来,将所选模式映射到合适的子域类型。最后,用业务愿景来验证已识别的子域类型。
Reversing the relationship between subdomains and tactical design decisions creates an additional dialogue between you and the business. Sometimes businesspeople need us as much as we need them.
颠倒子域和战术设计决策之间的关系,会在你和业务之间产生更多的对话。有时,业务人员需要我们的程度与我们需要他们的程度是一样的。
If they think something is a core business, but you can hack it in a day, then it is either a sign that you need to look for finer-grained subdomains or that questions should be raised about the viability of that business.
如果他们认为某些东西是核心业务,但你可以在一天之内搞定它,那么这要么是一个迹象,表明你需要寻找更细粒度的子域,要么是应该对该业务的可行性提出质疑。
On the other hand, things get interesting if a subdomain is considered a supporting one by the business but can only be implemented using the advanced modeling techniques: domain model or event-sourced domain model.
另一方面,如果业务认为某个子域是支撑子域,但只能使用高级建模技术(如领域模型或基于事件的领域模型)来实现,那么事情就变得有趣了。
First, the businesspeople may have gotten overly creative with their requirements and ended up with accidental business complexity. It happens. In such a case, the requirements can, and probably should, be simplified. Second, it might be that the businesspeople don’t yet realize they employ this subdomain to gain an additional competitive edge. This happened in the case of the bonuses project. By uncovering this mismatch, you’re helping the business identify new profit sources faster.
首先,业务人员可能对他们的要求过于创新,最终导致了意外的业务复杂性。这种情况确实会发生。在这种情况下,可以而且很可能应该简化要求。其次,可能是业务人员还没有意识到他们正在利用这个子域来获得额外的竞争优势。这在奖金项目中就发生了。通过发现这种不匹配,你正在帮助业务更快地发现新的利润来源。
Don’t ignore pain
不要忽视痛苦
Most importantly, never ignore “pain” when implementing the system’s business logic. It is a crucial signal to evolve and improve either the model of the business domain or the tactical design decisions. In the latter case, it means the subdomain has evolved, and it’s time to go back and rethink its type and implementation strategy. If the type has changed, talk with the domain experts to understand the business context. If you need to redesign the implementation to meet new business realities, don’t be afraid of this kind of change. Once the decision of how to model the business logic is made consciously and you’re aware of all the possible options, it becomes much easier to react to such a change and refactor the implementation to a more elaborate pattern.
最重要的是,在实现系统业务逻辑时,永远不要忽视“痛点”。这是发展和改进业务领域模型或战术设计决策的关键信号。在后一种情况下,这意味着子域已经发展(即变化),是时候回去重新思考它的类型和实现策略了。如果类型已经改变,与领域专家交谈以了解业务背景。如果你需要重新设计实现以满足新的业务事实,不要害怕这种改变。一旦你有意识地决定如何建模业务逻辑,并且了解所有可能的选项,那么对这种变化做出反应并将实现重构为更复杂的模式就变得容易多了。
Boundaries of Bounded Contexts
有界上下文的边界
At Marketnovus, we tried quite a few strategies for setting the boundaries of bounded contexts:
在 Marketnovus,我们尝试了很多策略来设定有界上下文的边界:
- Linguistic boundaries: We split our initial monolith into marketing and CRM contexts to protect their ubiquitous languages. 语言边界:我们将最初的单体应用拆分为营销和CRM上下文,以保护它们的通用语言。
- Subdomain-based boundaries: Many of our subdomains were implemented in their own bounded contexts; for example, event crunchers and bonuses. 基于子域的边界:我们的许多子域是在它们自己的有界上下文中实现的; 例如,event crunchers(事件处理器)和bonuses(奖金)。
- Entity-based boundaries: As we discussed earlier, this approach had limited success in the marketing hub project, but it worked in others. 基于实体的边界:正如我们前面讨论的,这种方法在marketing hub(营销中心)项目中的成效有限,但是在其他项目中却有效。
- Suicidal boundaries: As you may remember, in the initial implementation of the CRM we dissected an aggregate into two different bounded contexts. Never try this at home, okay? 自杀式边界:你可能还记得,在CRM的初步实现中,我们将一个聚合拆分为两个不同的限界上下文。不要在家里尝试这个,好吗?
Which of these strategies is the recommended one? None of them fits in all cases. In our experience, it was much safer to extract a service out of a bigger one than to start with services that are too small. Hence, we preferred to start with bigger boundaries and decompose them later, as more knowledge was acquired about the business. How wide are those initial boundaries? As we discussed in Chapter 11, it all goes back to the business domain: the less you know about the business domain, the wider the initial boundaries.
这些策略中哪一个是被推荐的?它们都不是万能的。根据我们的经验,从较大的服务中提取服务比从太小的服务开始要安全得多。因此,我们倾向于先设定较大的边界,然后在获得更多业务知识后再进行分解。那么,这些初始边界有多宽?正如我们在第11章中所讨论的,这一切都取决于业务领域:你对业务领域了解得越少,初始边界就越宽。
This heuristic served us well. For example, in the cases of the marketing and CRM bounded contexts, each encompassed multiple subdomains. As time passed, we gradually decomposed the initially wide boundaries into microservices. As we defined in Chapter 14, throughout the evolution of the bounded contexts, we stayed in the range of the safe boundaries. We were able to avoid going past the safe boundaries by doing the refactoring only after gaining enough knowledge of the business domain.
这个启发对我们很有用。例如,在marketing(市场营销)和 CRM 有界上下文中,每个上下文都包含多个子域。随着时间的推移,我们逐渐将最初宽泛的边界分解为微服务。正如我们在第14章中所定义的,在整个限界上下文的演变过程中,我们都保持在安全边界的范围内。我们能够在获得足够的业务领域知识后再进行重构,从而避免超出安全边界。
Conclusion
总结
In the stories of Marketnovus’s bounded contexts I showed how our understanding of domain-driven design evolved through time (refer to Figure A-6 for a refresher):
在Marketnovus的有界上下文故事中,我展示了我们对领域驱动设计的理解是如何随着时间的推移而演变的(请参考图A-6进行回顾):
- We always started by building a ubiquitous language with the domain experts to learn as much as possible about the business domain. 我们总是从与领域专家建立通用语言开始,以尽可能多地了解业务领域。
- In the case of conflicting models, we decomposed the solution into bounded contexts, following the linguistic boundaries of the ubiquitous language. 在模型冲突的情况下,我们将解决方案分解为有界上下文,遵循通用语言的语言边界。
- We identified the subdomains’ boundaries and their types in each bounded context. 我们在每个有界上下文中确定了子域的边界和类型。
- For each subdomain we chose an implementation strategy by using tactical design heuristics. 对于每个子域,我们通过使用战术设计启发法选择一种实现策略。
- We verified the initial subdomain types with those resulting from the tactical design. In cases of mismatching types, we discussed them with the business. Sometimes this dialogue led to changes in the requirements, because we were able to provide a new perspective on the project to the product owners. 我们用战术设计的结果验证了最初的子域类型。在类型不匹配的情况下,我们与业务部门进行了讨论。有时,这种对话会导致需求的变化,因为我们能够为产品所有者提供了一个关于项目的新视角。
- As more domain knowledge was acquired, and if it was needed, we decomposed the bounded contexts further into contexts with narrower boundaries. 随着获得的领域知识越来越多,如果需要的话,我们将有界上下文进一步分解为边界更窄的上下文。
If we compare this vision of domain-driven design with the one we started with, I’d say the main difference is that we went from “aggregates everywhere” to “ubiquitous language everywhere.”
如果我们将这种领域驱动设计视角与我们最初的视角进行比较,我会说主要的区别在于,我们从“无处不在的聚合”转变为“无处不在的通用语言”。
In parting, since I’ve told you the story of how Marketnovus started, I want to share how it ended.
在结束之前,由于我已经告诉了你Marketnovus是如何开始的,所以我想分享一下它是如何结束的。
The company became profitable very quickly, and eventually it was acquired by its biggest client. Of course, I cannot attribute its success solely to domain-driven design. However, during all those years, we were constantly in “start-up mode.”
该公司很快实现了盈利,并最终被其最大的客户收购。当然,我不能将其成功完全归功于领域驱动设计。然而,在那些年里,我们一直处于“创业模式”。
What we term “start-up mode” in Israel is called “chaos” in the rest of the world: constantly changing business requirements and priorities, aggressive time frames, and a tiny R&D team. DDD allowed us to tackle all of these complexities and keep delivering working software. Hence, when I look back, the bet we placed on domain-driven design paid off in full.
在以色列,我们所说的“创业模式”在世界其他地方被称为“混乱”:不断变化的业务需求和优先级、紧迫的时间以及规模很小的研发团队。领域驱动设计使我们能够应对所有这些复杂性,并持续交付可运行的软件。因此,当我回顾过去时,我认为我们在领域驱动设计上下的赌注得到了充分的回报。
1 @DDDBorat is a parody Twitter account known for sharing bad advice on domain-driven design. @ DDdborat 是一个恶搞推特账号,以分享领域驱动设计上的糟糕建议而闻名。
