领域驱动架构设计_不断发展的架构中的领域驱动设计

领域驱动架构设计

域驱动的设计可以最容易地应用于稳定的域,在该域​​中,关键活动是开发人员捕获并建模用户头脑中的内容。 但是,当领域本身处于不断变化和发展的状态时,这将变得更具挑战性。 这在敏捷项目中很常见,并且在业务本身试图发展时也会发生。 本文研究了我们如何在为期两年的工作计划中使用DDD来重新思考和重建guardian.co.uk。 我们展示了如何确保对最终用户不断发展的看法反映在软件体系结构中,以及如何实现该体系结构以确保将来的变化。 我们在模型中提供了重要的项目流程和特定的演化步骤的详细信息。

顶级标题:

  1. 程序背景
  2. 从DDD开始
  3. 不断发展的计划中的DDD流程
  4. 不断发展的领域模型
  5. 代码级的演进
  6. 不断发展的体系结构中DDD的一些最后教训
  7. 附录:具体示例

1.程序背景

Guardian.co.uk具有悠久的新闻,评论和功能历史,目前每月接收1800万独立用户和1.8亿页面印象。 在此期间的大部分时间里,该网站都使用其原始的Java之前的技术,但在2006年2月,该工作开始了一项主要工作计划,以将其移至更现代的平台上,最早的阶段是在2006年11月, -look Travel网站上线,并于2007年5月继续提供新的头版,随后出现了更多内容。 尽管只有少数人在二月份开始工作,但团队后来达到了顶峰104。

但是,该程序背后的原因不仅仅是想要重新外观。 最重要的是,多年的经验告诉我们,有更好的方法来构造内容,将商品商业化的更好方法以及开发其背后的软件的更好方法。

从本质上讲,我们对工作的思考方式已经超出了软件可以处理的范围。 这就是DDD对我们如此宝贵的原因。

我们将简要介绍一下遗留软件的概念不匹配使我们受阻的两个方面,首先是内部用户的问题,然后是开发人员的问题。 这些都是DDD可以帮助我们解决的问题。

1.1。 内部用户的问题

新闻业是一个古老的行业,已经建立了培训,资格和职业结构,但是,即使经过几个月的培训,新的,接受过新闻培训的编辑人员也无法加入我们并使用我们的网络工具有效地工作。 要成为一个非常有效的用户,仅了解我们的CMS和网站的关键概念以及如何实施它们是不够的。

例如,缓存内容的概念(严格来说应该是系统内部的技术优化)已暴露给编辑人员。 他们需要将内容放入缓存中以确保已启动,并且需要了解缓存工作流程以诊断和解决CMS工具的问题。 显然,这是不合理的要求。

1.2。 开发人员的问题

概念上的不匹配也在技术方面表现出来。 例如,CMS的概念之一就是“工件”,它足够核心,所有开发人员每天都在使用它们。 一个团队曾经承认,距离他整整九个月才意识到这些“人工制品”实际上只是网页。 隐秘的语言和在其周围长大的软件掩盖了他作品的真实本质。

再举一个例子,从我们的内容生成RSS提要特别耗时。 即使每个部分的首页上都包含明显的主要内容列表以及随附的家具,但底层软件并未区分这两者。 因此,从页面提取RSS提要的逻辑是模糊逻辑,即“获取页面上的每个项目,如果其几何形状大约跨过一半,并且如果其长度大于平均长度,则可能是主要内容,因此我们可以提取链接并将其制作为供稿。”

我们已经清楚地知道,人们对工作的看法(发布,网页,RSS提要)与实施方式(缓存工作流,“工件”,模糊逻辑)之间的分歧正在对我们产生切实而昂贵的影响效力。

2.从DDD开始

本节为我们使用DDD奠定了基础:我们选择DDD的原因,它在系统体系结构中的位置以及我们的初始域模型。 在后面的部分中,我们将研究如何将初始领域的知识扩展到扩展的团队,如何演化模型以及如何围绕这一基础演化编码技术。

2.1。 选择DDD

DDD的第一个吸引人的方面是无处不在的语言,并将用户自己的概念直接嵌入到代码中。 这清楚地解决了我们上面描述的概念不匹配的问题。 仅凭这一点就可以得到宝贵的见解,但仅凭其本身就是“正确完成面向对象”。

DDD带来的技术语言和概念更进一步:实体,价值对象,服务,存储库等。 这确保了在进行大型项目时,我们庞大的开发团队有机会持续发展,这对于长期保持质量至关重要。 即使我们的低级代码萎缩了,我们将在后面展示,通用的技术语言也使我们能够使每个人重新团结起来并提高代码质量。

2.2。 将领域模型嵌入系统

本节显示DDD在整个系统体系结构中的位置。

我们的系统由三个主要组件组成:面向用户的网站呈现应用程序; 我们面向编辑人员的工具应用程序,用于创建和管理内容; 以及我们的Feed应用程序,它可以将数据路由进出系统。 这些应用程序都是使用Spring和Hibernate构造的Java Web应用程序,其中Velocity是我们的模板语言。

我们可以将这些应用程序视为具有以下布局:

Hibernate层使用EHCache作为Hibernate的第二级缓存来提供数据访问。 “模型”层包含域对象和存储库,服务位于上面的下一层。 在此之上,我们有Velocity模板层,它提供页面渲染逻辑。 最后,最顶层包含充当应用程序入口点的控制器。

纵观应用程序的这种分层体系结构,很容易想到模型只是我们应用程序的一个独立层。 这个想法大体上是正确的,但是在模型层和其他层之间有一些细微的差异:当我们使用领域驱动的设计时,我们需要一种无处不在的语言,不仅让我们在谈论领域时作为人使用,而且在应用程序中的任何地方都可以使用。 模型层的存在不仅可以将业务逻辑与呈现逻辑隔离开来,而且还可以提供词汇表供其他层使用。

而且,模型层可以构建为独立的代码单元,并可以作为JAR导出到依赖于它的许多应用程序中。 其他任何层都不是这样。 这对构建和发布应用程序有影响:在基础架构的模型层中更改代码必须是所有应用程序的全局更改。 我们可以在前端网站中更改Velocity模板,而只需部署前端应用程序,而无需部署管理应用程序或feed。 如果我们在域模型中更改对象的逻辑,则必须推出所有依赖该模型的应用程序,因为我们只有(并希望)一个域视图。

如果业务域非常大,则这种域建模方法可能会导致整体模型的更改成本很高,这是有危险的。 我们意识到这一点,并且随着我们领域的不断发展,我们需要确保这一层不会变得太笨拙。 当前,这不会给我们造成问题,并且域层非常大且复杂。 无论如何,我们希望在敏捷环境中工作,每两周对所有应用程序进行完整的生产部署。 但是,我们一直在关注这一层代码更改的成本。 如果它开始上升到不可接受的水平,我们可能必须考虑将单个模型分成多个较小的模型,并在每个子模型之间提供适应层。 但是,我们在项目开始时并未这样做,而是相对于需要使用多个模型解决的更复杂的依赖管理问题,它更倾向于使用单个模型的简单性。

2.3。 早期领域建模

在项目的早期,很久没有人拿起键盘并开始编写代码之前,我们决定在项目进行期间将开发人员,QA,BA和业务人员安置在同一房间。 在此阶段,我们只有一小组业务和技术人员,并且只需要适度的第一版即可。 这确保了我们的模型和过程都非常简单。

我们的首要目标是清楚地了解我们的编辑(业务代表的重要组成部分)对项目前几个迭代的期望。 我们与编辑人员组成一个整体团队,讨论他们的想法,使各种职能的代表可以质疑和澄清这些想法,直到我们认为我们对英语有一个合理的了解,即我们对编辑的需求。

我们的编辑认为,在项目的早期阶段,他们最优先考虑的是该系统能够生成可以显示文章的网页和文章分类系统。

他们的最初要求可以总结如下。

  • 我们应该只能将一篇文章与任何给定的URL相关联。
  • 我们将需要能够通过选择其他模板来更改呈现结果页面的方式。
  • 我们需要将内容分为广泛的部分进行管理,例如新闻,体育,旅行
  • 系统必须能够显示一个页面,其中包含指向给定类别的所有文章的链接。

我们的编辑需要一种非常灵活的方法来对文章进行分类。 他们设计的方法基于关键字的使用。 每个关键字定义了内容可能涉及的主题。 每篇文章可能与许多关键字相关联,因为一篇文章可能涉及许多主题。

我们的网站有许多编辑器,每个编辑器拥有内容的不同部分。 每个部分都需要自己的导航和自己的关键字的所有权。

从编辑者使用的语言看来,我们正在向我们的领域引入一些关键实体:

  • 页面 URL的所有者。 负责选择模板以呈现内容。
  • 模板页面的布局,可以随时更改。 技术人员将模板实现为光盘上的Velocity文件。
  • A 节是非常广泛的页面分类。 每个部分都有一个编辑器,并且其中的页面具有相同的外观。 这些部分的示例是新闻,旅游和商务。
  • 关键字描述部分中存在的主题的一种方式。 关键字用于分类文章,可驱动自动导航。 它们将与页面相关联,以便可以自动生成有关给定主题的所有文章的页面。
  • 文章我们可以交付给用户的一段文本内容。

提取这些信息后,我们开始对领域建模。 在项目的早期做出的决定是,编辑拥有域模型,并在技术团队的帮助下负责设计模型。 对于不习惯这种形式的技术设计的编辑来说,这是一个很大的转变。 我们发现通过举办由编辑,一些开发人员和技术架构师组成的研讨会,我们能够使用简单的低技术方法勾勒出并发展领域模型。 将讨论模型的一个区域,并使用毡笔,文件卡和Blu-Tak绘制候选解决方案。 每个候选模型都会进行讨论,技术团队会向编辑人员建议设计中每项改进的含义。

尽管起初速度很慢,但过程很有趣。 编辑们发现它非常实用。 他们能够草拟事物并四处移动对象,然后立即获得开发人员有关最终模型是否满足其要求的反馈。 技术人员都非常惊讶和高兴,每个人很快就能精通该过程,并且所有人都对所产生的系统能够满足他们的客户充满信心。

观察领域语言的发展也很有趣。 有时将要成为“ 文章”的对象称为“故事”。 显然,我们不能使用在同一实体中具有多个名称的通用语言,所以这是一个问题。 是我们的编辑发现他们没有使用通用语言来描述事物,因此决定将该对象称为文章 。 在那之后,任何时候任何人说“故事”时都会有人说“你不是在说文章吗?”这种持续不断的社区改正过程在设计通用语言时是强大的力量。

我们的编辑最初设计的结果模型如下所示:

[页面及其部分之间的关​​系是通过应用到作为页面核心内容的文章的关键字得出的。 页面的部分由应用于文章的第一个关键字的部分给出

由于并非所有团队都参与了过程的所有阶段,因此我们向他们介绍了模型,并将其表示形式挂在墙上。 然后,开发人员开始进行敏捷开发,并且随着编辑人员与开发人员,BA和QA一起位于同一地点,有关模型及其意图的任何问题都可以在开发过程中的任何时候“从马口中直接拿出来”。

经过几次迭代,系统开始成型,我们已经建立了用于创建和管理关键字文章页面的工具 。 编辑人员可以在构建时使用它们并提出更改建议。 人们普遍认为,这种简单的核心模型正在发挥作用,并且可以继续构成该网站首次发布的基础。

3.不断增长的计划中的DDD流程

初始版本发布后,我们的项目团队与技术人员和业务代表一起成长,并且我们希望领域模型能够不断发展。 显然,我们需要一种有组织的方法来将新用户引入域模型并管理系统的发展。

3.1。 新员工入职

DDD是归纳过程的核心部分。 非技术人员会在项目的整个生命周期内加入该项目,因为工作计划依次遍及各个编辑领域,并且我们会在适当时机引入部门编辑。 技术人员一生都加入了该项目,这仅仅是因为我们一直在招聘新员工。

我们的入门过程包括针对这两个受众的DDD会议,尽管细节有所不同,但高级议程涵盖了相同的两个领域:DDD是什么以及为什么DDD很重要; 以及领域模型本身的特定区域。

我们在描述DDD时要强调的重要事项是:

  • 领域模型非常归业务代表所有。 这是关于从业务代表的负责人中提取概念并将其嵌入到软件中,而不是从软件中获取想法并尝试培训业务代表。
  • 技术团队是关键利益相关者。 我们仍将围绕具体细节进行激烈辩论。

覆盖域模型本身的特定区域很重要,因为这可以使应征者真正了解项目中的特定问题。 我们的领域模型中有几十个对象,因此我们只关注一些高级和较明显的对象,在我们的案例中是本文讨论的各种内容和关键字概念。 我们在这里做三件事:

  • 我们在白板上画出了概念和关系,因此我们可以提供到目前为止系统如何工作的切实展示。
  • 我们确保有一名编辑人员来解释很多领域模型,以强调它不是技术团队所有的事实。
  • 我们解释了为达到这一点所做的一些历史变化,因此应征者可以理解(a)这不是一成不变的,而是可以改变的,以及(b)他们在即将进行的计划对话中可以扮演什么样的角色进一步开发模型。

3.2。 计划中的DDD

归纳法是必不可少的,但是只有当我们开始计划每次迭代时,知识才会被实际应用。

3.2.1。 使用无所不在的语言

通用语言DDD部队允许业务人员,技术人员和设计师在同一张桌子上开会,以计划和确定特定任务的优先级。 这意味着更多的会议与业务人员相关,他们与技术人员越来越近,对技术过程的了解也更多。 一位同事从该项目的编辑助理发展到关键决策者; 她评论说,通过参加迭代启动会议,她亲眼目睹了技术人员如何估计和(通常是热情地)评估任务,并且她变得更加欣赏功能和努力之间的平衡。 如果她不与技术团队共享语言,那么她将不会参加该会议,也不会获得这种了解。

在计划阶段使用DDD时,我们使用两个重要原则:

  1. 领域模型归企业所有; 和
  2. 领域模型需要权威的业务资源。

域模型的业务所有权在归纳过程中进行了解释,但仅在此处起作用。 这意味着技术团队的关键角色是倾听和理解,而不是解释什么是可能的和不可能的。 需求提取需要将概念性领域模型映射到具体的功能需求,以及在不匹配的地方可以挑战和查询业务代表。 如果存在不匹配,则要么需要更改域模型,要么需要在更高级别解决功能要求(“您希望使用此功能实现什么?”)。

我们组织的性质明确需要领域模型的权威业务来源。 我们正在构建一个单一的软件平台,该平台需要满足许多不一定以相同方式看待世界的编辑团队的需求。 Guardian不像许多公司那样采用“命令与控制”结构。 相反,编辑部被赋予了很大的自由度,并获得了以他们认为合适的方式发展自己的网站版块和受众的许可。 因此,不同的编辑者对领域模型的理解和观点略有不同,这有可能破坏一种普遍存在的语言。 我们的解决方案是确定并嵌入负责所有编辑部门的业务代表。 对于我们来说,这是生产团队,他们负责处理建筑断面的日常工作,指定布局等。他们是超级用户桌面编辑人员所依赖的专家工具建议,因此他们是技术团队通常用作域模型的拥有者,并确保整个大型软件系统的一致性。 当然,他们不是唯一的业务代表,而是与技术人员保持一致的人。

3.2.2。 用DDD规划问题

不幸的是,我们发现在计划流程中应用DDD尤其面临挑战,特别是在持续进行计划的敏捷环境中。 这些问题是:

  1. 为新的不确定的业务模型编写软件的性质;
  2. 被束缚于旧模式;
  3. 商界人士走向本土。

我们依次讨论这些...

当埃里克·埃文斯(Eric Evans)撰写有关创建域模型的文章时,其观点是业务代表头脑中有一个模型,需要将其提取出来。 即使他们的模型不是很明确,他们也确实了解核心概念,并且可以向技术人员大体上解释这些核心概念。 但是,在我们的情况下,我们正在更改模型-实际上是在更改业务-却不知道我们要做什么的确切细节。 (我们将在短期内看到具体的示例。)某些想法很明显或很早就建立了(例如,我们将有文章和关键字),但许多想法却没有((对引入页面的想法有些抵触;关键字如何关联)彼此完全可以争夺)。 我们的教科书没有提供解决这些问题的指南。 但是,敏捷开发的原则是:

  • 构建最简单的东西。 尽管我们无法尽早解决所有细节,但通常我们已经足够了解,可以构建下一个有用的功能。
  • 经常释放。 通过发布此功能,我们可以看到它在现场的工作方式。 进一步的调整和进化步骤由此变得最为明显(不可避免地,它们并不总是我们所期望的)。
  • 最小化变更成本。 随着这些调整和进化步骤的不可避免,降低变更成本至关重要。 对我们来说,这包括自动化的构建过程,自动化的测试等等。
  • 经常重构。 经过几步发展,我们将看到技术债务不断积累,这需要解决。

与此相关的是第二个问题:与我们的旧模型有太多的精神联系。 在我们的示例中,我们的旧系统要求编辑人员和生产人员分别布置页面,而新系统的愿景是提供基于关键字的自动页面生成。 在这个新世界中,关塔那摩湾的页面将在没有任何人为干预的情况下出现,这仅仅是因为关塔那摩湾关键字将具有很多内容。 但是,事实证明,这只是技术团队所拥有的过分机械化的愿景,他们希望减少体力劳动并不断精简所有页面。 相比之下,编辑人员高度重视他们在不仅报道而且还提供新闻的过程中所带来的人文见解。 对他们而言,个人布局对于突出最重要的故事(而不仅仅是最新的故事)以及以不同的方法和敏感性(例如9/11与Web 2.0报道)对待不同的主题至关重要。

对于这种问题,没有一个万能的解决方案,但我们发现了成功的两个关键:专注于业务问题,而不是技术问题; 并注意“创意冲突”一词。 在此示例中,存在意见分歧,但是当事双方通过商业表达其动机时,我们正在同一领域中开展工作。 解决方案是一种创造性的解决方案,它源于了解每个人的动机,因此可以解决每个人的担忧。 在这种情况下,我们构建了许多模板,编辑者可以从模板中进行选择和切换,每种模板都有不同的感觉,影响等。此外,每个模板的关键区域都允许显示手动选择的故事,其余页面是自动内容(请注意不要重复内容),并且如果策划变得繁重,则可以随时关闭此手动区域,因此使页面完全自动化。

我们发现的第三个挑战是业务人员本土化,这意味着他们可能会深深地嵌入到技术中,并陷入技术的细微差别中,以至于他们忘记了新来的内部用户的感受。系统。 当业务代表发现很难与同事交流工作方式或指定价值有限的功能时,就会出现危险迹象。 在第一版《 极限编程说明》中,肯特·贝克说,可以通过强调与技术团队的互动绝不会花费他们一天中的大部分时间来确保现场客户的安全。 但是,在与拥有数十名开发人员,BA和QA的团队合作时,我们发现有时甚至三个专职业务代表也不够。 随着商务人士与技术人员花费大量时间,与同行失去联系可能是一个真正的问题。 这些是人类解决方案的人类问题。 解决方案是提供个人备份和支持,将新的业务人员转入团队(可能开始提供协助,建立关键的决策角色),使代表有时间回到一周的一天的核心工作中。 , 等等。 实际上,这还有一个额外的优势,那就是它使更多的业务代表能够接触软件开发,从而传播技能和经验。

4.演进领域模型

在本节中,我们将研究模型在程序的后续阶段如何演变

4.1。 进化步骤1:超越文章

最初发行后不久,编辑人员要求系统能够处理除Article之外的多种类型的内容。 尽管这不足为奇,但我们在构建模型的第一个版本时明确决定不要考虑太多。

这是一个关键点:我们不是在尝试预先构建整个系统,而是将重点放在整个团队上,这些团队对模型和建模过程有一个很好的理解,并且管理的对象很小。 随着理解的增加或改变,以后必须更改模型并不是错误的。 这种方法与YAGNI(您将不需要它)的编码原理兼容,因为它阻止了开发人员引入额外的复杂性,从而避免了漏洞的蔓延。它还使整个团队有时​​间共同了解系统。大小的块。 我们认为,今天生产一个运行良好的无错误系统比明天生产一个美观,全面的模型更为重要。

编辑器在下一个迭代中需要的新型内容是“ 音频视频” 。 我们的技术团队与我们的编辑坐在一起,再次进行了领域建模过程。 通过与我们的编辑交谈,很明显, 音频视频类似于文章 :应该可以在页面上放置视频音频 。 每页只允许一个内容。 视频音频可以按关键字进行分类。 关键字可以属于Sections 。 编辑们还指出,在将来的迭代中,他们将添加更多类型的内容,并认为这是时候了解我们将如何逐步发展内容类型模型。

对于我们的开发人员而言,很明显,我们的编辑人员想在我们的语言中明确引入两个新术语: AudioVideo 。 同样清楚的是, 音频视频文章都有一些共同点:它们都是Content的类型。 我们的编辑人员不熟悉继承的概念,因此技术团队能够教给编辑人员有关继承的知识,以便他们可以按照自己的意愿正确地表达模型。

这里有一个明确的教训:通过使用敏捷开发技术将软件开发过程分解成小块,我们还能够为我们的业务人员简化学习曲线。 随着时间的推移,他们能够加深对领域建模过程的了解,而不必花大量时间在前期学习面向对象设计的所有组件。

这是我们的编辑器设计的新模型,其中添加了新的内容类型。

对模型的这种单一演变是对我们普遍存在的语言进行的许多较小演变的结果。 现在,我们有了三个额外的单词: 音频视频内容 ; 我们的编辑人员已经了解了继承,并可以在模型的未来迭代中使用它; 而且我们有一个未来的扩展策略,可以添加新的内容类型,这对于我们的编辑人员来说很简单。 如果编辑者需要一种新的内容类型,并且该新的内容类型与PagesKeywords的关系与我们现有的内容类型具有大致相同的关系,则编辑者可以要求开发团队生成一种新的Content类型。 通过逐渐生成模型,我们可以提高团队效率,因为编辑人员不再需要冗长的域建模过程来添加新的内容类型。

4.2。 进化步骤2:

随着我们的模型扩展到包括更多类型的内容 ,需要对其进行更灵活的分类。 我们开始将更多类型的元数据添加到我们的域模型中,但是尚不清楚编辑者的最终意图在哪里。 但是,这并不使我们担心太多,因为我们使用与内容处理相同的方法对元数据进行建模,将需求分解为可管理的块并将每个块添加到我们的域中。

我们的编辑人员想要添加的第一个元数据类型是系列的概念 系列是具有隐式基于日期的顺序的相关内容的分组。 我们报纸上有许多Series系列的例子,需要在网络上翻译这个概念。

我们最初的想法很简单。 我们将Series添加为可以与ContentPage关联的域对象。 该对象将用于汇总与系列相关联的内容。 如果读者参观了一块内容 ,并且内容是在一个系列中 ,我们将能够从页面系列内的前面和后面的内容链接。 我们还可以链接到并生成系列索引页面 ,该页面将显示系列中的所有内容

这是我们的编辑人员设计的系列模型:

同时,在森林的另一部分,我们的编辑人员正在考虑要与Content关联的更多元数据。 目前, 关键字描述了内容的含义。 编辑人员还要求系统根据内容的音调来处理内容 。 不同色调的示例包括评论,ob告,读者要约和信件。 通过介绍Tone ,我们可以向读者展示此内容,并允许他们找到相似的内容(其他ob告,评论等)。 这感觉与关键字系列的关系不同。 像“ 系列”一样,“ 色调”可以附加到一条内容上,并与Page有关系。

这是我们的编辑器设计的Tone模型:

开发完成后,我们有了一个可以按关键字,系列音调对内容进行分类的系统。 但是,编辑对于达到这一点所需的技术工作量有些担心。 当我们接下来发展模型时,他们向技术团队提出了这些担忧,并能够提出解决方案。

4.3。 进化步骤3:重构元数据

我们的编辑想要介绍的模型的下一个演变与添加SeriesTone类似。 我们的编辑希望添加具有贡献者Content的概念。 贡献者是创造内容的人,可以是文章的作者或视频的制作人。 像Series一样, 贡献者可以在系统上拥有一个页面 ,该页面将自动聚合他们产生的所有内容

编辑们还看到了另一个问题。 他们认为,随着SeriesTone的引入,他们必须指定大量与开发人员非常相似的细节。 他们不得不要求创建一个工具来创建系列,并要求另一个工具来创建音调 。 他们必须指定这些对象如何与ContentPage相关 。 每次他们发现为两种类型的域对象指定非常相似的开发任务时,都将使用它们。 这既耗时又重复。 编辑人员更加担心将Contributor添加到组合中,并且可能会跟随更多的元数据类型。 看起来他们又不得不指定和管理大量昂贵的开发工作,而所有这些工作都非常相似。

这显然成为一个问题。 看来我们的编辑发现了我们的模型有问题,而开发人员没有发现。 为什么添加新的元数据对象如此昂贵? 他们为什么要一遍又一遍地指定相同的工作? 我们的编辑提出的一个问题是“这仅仅是'软件开发的工作方式',还是该模型有问题?” 技术团队认为编辑在做某事,因为他们显然没有以与编辑相同的方式看待领域。 我们与编辑人员举行了另一场领域建模会议,以尝试找出问题所在。

在会议上,我们的编辑建议,所有现有的元数据类型实际上都可以从相同的基本思想中得出。 所有元数据对象( 关键字,系列,音调贡献者 )都可以与Content建立多对多关系,并且都需要它们自己的Page 。 (在模型的先前版本中,我们必须推导对象与Page之间的关系。)我们重构了模型,以引入一个称为Tag的新超类,并对其他元数据进行子类化。 编辑们喜欢使用技术术语“超类”,并宣称整个重构过程被称为“超级标签”,尽管最终落到实处。

随着标签的引入,添加贡献者和其他预期的新元数据类型变得非常简单,因为我们将能够利用现有工具的功能和框架。

我们修改后的模型现在看起来像这样:

找到我们的业务代表以这种方式看待开发过程和领域模型,这真是令人着迷,这是领域驱动设计促进双向双向理解的能力的一个很好的例子:我们发现我们的技术团队对我们要解决的业务问题有很好的一致的理解,而且,作为意外的收获,业务代表能够“洞悉”开发过程并对其进行更改以更好地满足他们的需求。 现在,编辑者不仅能够将其需求转换为领域模型,而且还可以设计和监督领域模型的重构,以确保它与我们当前对业务问题的最新了解保持一致。

编辑者计划领域模型重构并成功执行它们的能力,是我们在guardian.co.uk上通过领域驱动设计取得成功的关键之一。

5.代码级的演变

以前,我们研究了领域模型的演化方面。 但是DDD也会对代码级别产生影响,不断发展的业务需求也意味着在那里也要进行更改。 我们现在来看其中一些。

5.1。 构建模型

构建域模型时,首先要确定的是域内发生的聚合。 可以将聚合视为相互之间具有引用的相关对象的集合。 这些对象不应直接引用其他集合中的对象; 那就是聚合根的工作。

查看上面定义的模型示例,我们可以开始看到物体形成的味道。 我们有PageTemplate对象,它们共同作用来为我们的网页提供URL和外观。 由于页面是我们系统的入口点,因此很明显, 页面是此处的聚合根。

我们还有一个聚合,其中Content是聚合根。 我们已经看到, Content具有Article,Video,Audio等子类型并且我们将它们视为内容的子集合,而核心Content对象为其集合根。

我们还可以看到另一种聚集形成。 这是元数据对象的集合: Tag,Series,Tone 这些以Tag作为其聚合根形成标签聚合。

Java编程语言提供了一种建模这些聚合的理想方法。 我们可以使用Java包对每个集合建模,而标准POJO可以对每个域对象建模。 不是聚合根目录且仅在聚合内使用的域对象可以具有包范围的构造函数,以便不能在聚合外构造它们。

以上模型的包结构如下所示(“ r2”是我们的应用程序套件的名称):

com.gu.r2.model.page
com.gu.r2.model.tag
com.gu.r2.model.content
com.gu.r2.model.content.article
com.gu.r2.model.content.video
com.gu.r2.model.content.audio

我们将内容聚合细分为子包,因为内容对象倾向于具有许多聚合特定的支持类(此处未在我们的简化图中显示)。 所有基于标签的对象往往要简单得多,因此我们将它们保留在同一程序包中,而不是引入额外的复杂性。

但是,我们已经意识到上述包结构可能在以后给我们带来问题,并打算对其进行更改。 可以通过查看前端应用程序中的包结构示例来了解问题,以了解我们如何构造控制器:

com.gu.r2.frontend.controller.page
com.gu.r2.frontend.controller.articl

在这里,我们看到我们的代码库开始碎片化。 我们已经将所有聚合都提取到包中,但是没有一个包含与该聚合相关的每个对象的包。 这意味着,如果由于域变得太大而无法作为一个单元来管理,如果将来希望拆分应用程序,则可能很难解决依赖关系。 这一点并没有真正给我们造成问题,但是我们将重构应用程序,以使我们没有那么多的交叉包依赖项。 改进的结构将是:

com.gu.r2.page.model   (domain objects in the page aggregate)
com.gu.r2.page.controller (controllers providing access to aggregate)
com.gu.r2.content.article.model
com.gu.r2.content.article.controller
...
etc

除了约定外,我们的代码库中没有其他任何领域驱动设计原则的实施。 可以创建注释或标记接口来标记聚合根,从而真正锁定模型包中的开发,从而减少开发人员在建模中犯错误的机会。 但是,除了这些机械强制措施之外,我们还依赖于更人为的技术,例如结对编程和测试驱动的开发,以确保整个代码库均遵循标准约定。 如果我们发现确实存在违反我们的设计原则的事情(这种情况很少见),那么我们将与开发人员交谈,并请他们完善设计。 我们非常喜欢这种轻量级的方法,因为它可以减少代码库中的混乱情况,并提高代码的简洁性和可读性。 这也意味着我们的开发人员可以更好地了解为什么某些事物按其原样构造,而不是仅仅因为它们被迫而做。

5.2。 DDD核心概念的演变

遵循域驱动设计原理构建的应用程序将具有四种广泛类型的对象:实体,值对象,存储库和服务。 在本节中,我们将从应用程序中查看这些示例。

5.2.1。 实体

实体是存在于集合中并具有标识的对象。 并非所有实体都是聚合根,而只有实体可以是聚合根。

实体的概念是开发人员(尤其是使用关系数据库的开发人员)非常熟悉的概念。 但是,我们发现这个看似很好理解的概念可能会引起一些混乱。

混淆似乎部分与我们使用Hibernate保留实体有关。 在使用Hibernate时,我们通常将实体建模为简单的POJO。 每个实体都有可以用setter和getter方法访问的属性。 每个属性都映射到一个XML文件中,该文件定义了应如何将其持久保存在数据库中。 为了创建一个新的持久化实体,开发人员需要创建一个用于存储的数据库表,创建适当的Hibernate映射文件并创建具有相关属性的域对象。 当开发人员花一些时间研究持久性机制时,他们有时似乎觉得实体对象的目的仅仅是数据的持久性,而不是业务逻辑的执行。 然后,当他们开始实现业务逻辑时,他们倾向于在服务对象而不是实体对象本身中实现它。

在(简化的)代码片段中可以看到这种类型的错误的示例。 我们有一个简单的实体对象来代表足球比赛:

public class FootballMatch extends IdBasedDomainObject
{
private final FootballTeam homeTeam;
private final FootballTeam awayTeam;
private int homeTeamGoalsScored;
private int awayTeamGoalsScored;

FootballMatch(FootballTeam homeTeam, FootballTeam awayTeam) {
this.homeTeam = homeTeam;
this.awayTeam = awayTeam;
}

public FootballTeam getHomeTeam() {
return homeTeam;
}

public FootballTeam getAwayTeam() {
return awayTeam;
}
public int getHomeTeamScore() {
return homeTeamScore;
}

public void setHomeTeamScore(int score) {
this.homeTeamScore = score;
}

public void setAwayTeamScore(int score) {
this.awayTeamScore = score;
}
}

这个实体对象使用FootballTeam实体为团队建模,并且看起来像使用Hibernate的任何Java开发人员都会熟悉的对象类型。 该实体的每个属性都保存在数据库中,尽管从域驱动设计的角度来看,该细节并不是很重要,但是我们的开发人员将持久化的属性提升到了应有的地位。 当我们尝试从赢得比赛的FootballTeam对象中锻炼时,可以显示出这一点。 我们的开发人员正在做的事情是创建另一个所谓的域对象,如下所示:

public class FootballMatchSummary {

public FootballTeam getWinningTeam(FootballMatch footballMatch) {
if(footballMatch.getHomeTeamScore() > footballMatch.getAwayTeamScore()) {
return footballMatch.getHomeTeam();
}
return footballMatch.getAwayTeam();
}
}

片刻的想法应该表明出了点问题。 我们创建了一个名为FootballMatchSummary的新类,该类存在于我们的域模型中,但对企业没有任何意义。 它似乎充当FootballMatch对象的服务,提供确实应该在FootballMatch域对象上的功能。 似乎引起混乱的是,开发人员正在查看FootballMatch实体对象的目的只是为了反映数据库中保留的信息,而不是回答所有业务问题。 我们的开发人员将实体视为传统ORM意义上的实体,而不是企业拥有的和业务定义的域对象。

如果不加以检查,这种不愿在域对象中放置业务逻辑的行为可能会导致领域模型变得贫乏,并导致混乱的服务对象泛滥(正如我们稍后将看到的那样)。 现在,作为一个团队,我们将认真研究所创建的任何服务对象,以查看它们是否实际上包含业务逻辑。 我们还有一个严格的规则,即开发人员不能在模型中创建对业务没有任何意义的新对象类型。

作为一个团队,在项目开始时,我们也进一步受到实体对象的困惑,而这种困惑又与持久性有关。 在我们的应用程序中,我们的大多数实体与内容相关,并且大多数实体都是持久性的。 但是,有时实体不是持久性的,而是由工厂或存储库在运行时创建的。

一个很好的例子是“标签组合页面”。 我们将数据库中由编辑者创建的所有页面的表示形式保留下来,但是我们可以自动生成页面,这些页面通过诸如美国+经济或技术+中国之类的标记组合来聚合内容。 因为所有可能的标签组合的总数都是天文数字,所以我们不可能持久保留所有这些页面,但是系统仍必须能够生成它们。 呈现标记组合器页面时,我们必须在运行时实例化Page类的新的非持久实例。 在项目的早期,我们倾向于将这些非持久对象视为与“真实”持久域对象不同的东西,并且在对它们的建模中不够完善。 实际上,从业务的角度来看,因此这些自动生成的实体与持久实体没有什么区别,因此从域驱动设计的角度来看也是如此。 无论它们是否持久化,它们对业务都具有同等定义的含义,因此仅存在域对象。 没有“真实”或“非真实”领域对象的概念。

5.2.2。 价值对象

值对象是实体的属性,这些实体不具有在域内具有任何含义的自然标识,但表示具有在域内具有含义的概念。 这些对象很重要,因为它们可以使通用语言更加清晰。

通过更详细地查看我们的Page类,可以看到值对象澄清能力的示例。 我们系统上的任何页面都有两个可能的URL。 一个URL是面向公众的URL,读者可以在其Web浏览器中键入该URL以访问内容。 另一个URL是直接从我们的应用程序服务器提供服务时内容存在的内部URL。 我们的Web服务器会查看用户请求的任何传入URL,并将其转换为适当的后端CMS服务器上的内部URL。

这两个可能的URL的简单视图是将它们都建模为Page类上的字符串对象:

public String getUrl();
public String getCmsUrl();

但是,这不是特别具有表现力。 通过查看它们的签名而不是返回字符串的事实,很难确切地知道这些方法将返回什么。 同样,想象一下我们要基于数据访问对象的URL从页面访问页面的情况。 我们可能有一个如下所示的方法签名:

public Page loadPage(String url);

这里需要哪个URL? 面向公众还是CMS网址? 不检查该方法的代码就无法分辨。 在谈论页面的URL时,也很难与企业进行对话。 我们是指哪一个? 我们的模型中没有代表每种URL的对象,因此我们的词汇表中没有术语。

在这里酿造还有更多麻烦。 对于内部和外部URL,我们可能有不同的验证规则,并希望对它们执行不同的操作。 如果我们无处可放,如何正确地封装此逻辑? 操纵URL的逻辑当然不属于Page,并且我们也不想引入更多不必要的服务对象。

域驱动设计的发展趋势是,我们明确地对这些价值对象进行建模。 我们应该创建代表值对象的简单包装器类来键入它们。 如果这样做,那么我们在Page上的签名现在看起来像这样:

public Url getUrl();
public CmsPath getCmsPath();

现在,我们可以安全地在应用程序中传递CmsPathUrl对象,并以他们能理解的语言与我们的业务代表就此代码进行对话。

5.2.3。 储存库

存储库是存在于聚合中的对象,用于提供对该聚合根对象实例的访问,同时抽象出任何持久性机制。 这些对象被问业务问题,并以域对象作为响应。

试图将存储库视为类似于具有数据持久性功能的数据访问对象的技术对象,而不是域中存在的业务对象。 但是存储库是领域对象:它们回答业务问题。 存储库也始终与聚合关联,并返回其聚合根的实例。 如果我们需要一个Page对象,我们将转到PageRepository 。 如果我们需要能够回答特定业务问题的Page对象列表,我们还将转到PageRepostory

我们发现,思考存储库的一种好方法是将它们视为数据访问对象集合上的外观。 然后,它们成为需要询问特定聚合的业务问题与提供底层功能的数据传输对象之间的集成点。

在这里,我们可以从页面存储库中获取一小段代码来查看实际情况:

private final PageDAO<Page> pageDAO;
private final PagesRelatedBySectionDAO pagesRelatedBySectionDAO;

public PageRepository(PageDAO<Page> pageDAO,
EditorialPagesInThisSectionDAO pagesInThisSectionDAO,
PagesRelatedBySectionDAO pagesRelatedBySectionDAO) {
this.pageDAO = pageDAO;
this.pagesRelatedBySectionDAO = pagesRelatedBySectionDAO;
}

public List<Page> getAudioPagesForPodcastSeriesOrderedByPublicationDate(Series series, int maxNumberOfPages) {
return pageDAO.getAudioPagesForPodcastSeriesOrderedByPublicationDate(series, maxNumberOfPages);
}

public List<Page> getLatestPagesForSection(Section section, int maxResults) {
return pagesRelatedBySectionDAO.getLatestPagesForSection(section, maxResults);
}

我们的存储库包含业务问题:获取PublicationDate订购的特定Podcast 系列页面 。 获取特定部分的最新页面 我们可以在这里看到正在使用的业务领域语言。 这不仅仅是一个数据访问对象,它本身就是域对象,就像页面文章是域对象一样。

我们花了一段时间才意识到,将存储库视为域对象可以帮助我们克服实现域模型的技术问题。 在我们的模型中,我们可以看到标签内容具有双向的多对多关系。 我们将Hibernate用作ORM工具,因此我们对此进行了映射,以使Tag具有以下方法:

public List<Content> getContent();

内容具有以下方法:

public List<Tag>  getTags();

尽管这种实现方式是模型的正确表达,正如我们的编辑看到的那样,但我们却为自己制造了一个问题。 开发人员可以编写如下代码:

if(someTag.getContent().size() == 0){
... do some stuff
}

这里的问题是,如果所讨论的标签是具有大量内容的标签(例如,“新闻”),我们可能最终将数十万个内容项加载到内存中只是为了查看标签是否包含任何内容。 显然,这在站点上造成了巨大的性能和稳定性问题。

随着我们对模型的发展和对域驱动设计的理解,我们意识到有时我们必须务实:模型的某些遍历可能很危险,应该避免。 在这种情况下,我们使用存储库以安全的方式回答问题,从而牺牲了模型的某些小部分清晰度和纯度,以提高系统的性能和稳定性。

5.2.4。 服务

服务是通过协调域对象的交互来管理业务问题执行的对象。 随着流程的发展,我们对服务的了解发展最快。

主要问题是,开发人员创建确实不应该存在的服务非常容易。 它们要么最终包含应该存在于域对象中的域逻辑,要么实际上代表了尚未创建为模型一部分的缺失域对象。

在项目的早期,我们开始发现出现了诸如ArticleService名称的服务。 这是什么? 我们有一个称为Article的领域对象; 文章服务的目的是什么? 通过检查代码,我们发现该类似乎遵循与上面讨论的FootballMatchSummary对象类似的模式,其中包含确实属于核心域对象的域逻辑。

为了解决此问题,我们对应用程序中的所有服务进行了代码审查,并执行了重构以将逻辑移到适当的域对象中。 我们还提出了一条新规则:任何服务对象的名称中都必须有一个动词。 这个简单的规则阻止开发人员创建类似ArticleService的类。 相反,我们可以创建诸如ArticlePublishingServiceArticleDeletionService类的类。 转向这种简单的命名约定无疑可以帮助我们将域逻辑转移到正确的位置,但是仍然需要定期对服务进行代码审查,以确保我们在可行的范围内跟踪和建模我们的域,并尽可能接近业务的观点。 。

6.不断发展的体系结构中DDD的一些最后教训

尽管存在挑战,但我们已经发现在不断发展的敏捷环境中使用DDD的显着优势,并且我们还吸取了以下教训:

  • 您无需了解整个领域即可增加业务价值。 您甚至不需要全面了解域驱动设计。 团队中的所有成员可以随时根据需要达到对模型的共同理解。
  • 随着我们共同的理解的提高, 可能(甚至是必不可少的)随着时间发展模型过程并纠正先前的错误。

我们系统的完整域模型比此处描述的简化版本大得多,并且随着我们业务的扩展而不断发展。 在一个充满活力的大型网站世界中,创新不断发生。 我们一直想保持领先地位并开拓新的市场,有时我们很难在第一次就获得正确的模型。 确实,我们的业务代表经常希望尝试新的想法和方法。 有些会取得成果,有些则不会成功。 企业能够逐步扩展现有的领域模型,甚至在不再满足其需求时对其进行重构,这也为开发Guardian.co.uk时发生的许多创新奠定了基础。

7.附录:一个具体示例

为了了解我们的领域模型如何产生真实的结果,这是一个示例,从单个内容开始...

翻译自: https://www.infoq.com/articles/ddd-evolving-architecture/?topicPageSponsorship=c1246725-b0a7-43a6-9ef9-68102c8d48e1

领域驱动架构设计

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值