Fork me on GitHub

《领域驱动设计 DDD》核心知识梳理笔记

本书是毕业后读的第二本偏专业书,刚工作读的第一本教的是代码层面的如何让具体实现更优雅, 而这本书旨在提升抽象能力、顶层设计、领域建模能力。看书的过程中,书里解答了之前工作中很多的困惑,比如 团队间,项目模块间如何界定边界?有没有好的方式让项目代码摆脱越来越不可控的结局?另外更重要的是,我们看书的过程中,对内容的思考不要局限在工作本身, 里面的思想完全可以指导对生活中的领域问题如何建模,体会生活的别致与美妙。书里很多共鸣也有很多内容由于工作经验不足体会不深,目前对这座丰碑的精华摄取尚浅,期待在后续的工作和生活中能够对书里的东西持续的印证与揣摩,与大家共勉!

第一部分运用领域模型

1. 模型在领域驱动设计中的作用?

  1. 模型和设计的核心互相影响
  2. 模型是团队所有成员使用的通用语言的中枢: 由于模型与实现之间的关联,开发人员可以使用该语言来讨论程序。
  3. 模型是浓缩的知识

2. 软件的核心?
软件的核心是其为用户解决领域相关的问题的能力。所有其他特性,不管有多么重要,都要
服务于这个基本目的。
3. 模型和实现的绑定

软件的设计如果缺乏概念,那么软件充其量不过是一种机械化的产品——
只实现有用的功能却无法解释操作的原因。

如果整个程序设计或者其核心部分没有与领域模型相对应,那么这个模型就是没有价值的, 软件的正确性也值得怀疑。同时,模型和设计功能之间过于复杂的对应关系也是难于理解的,在
实际项目中,当设计改变时也无法维护这种关系。若分析与和设计之间产生严重分歧,那么在分析和设计活动中所获得的知识就无法彼此共享

软件系统各个部分的设计应该忠实地反映领域模型,以便体现出这二者之间的明确对应关系。我们应该反复检查并修改模型,以便软件可以更加自然地实现模型,即使想让模型反映出更深层次的领域概念时也应如此。我们需要的模型不但应该满足这两种需求,还应该能够支持健壮的UBIQUITOUS LANGUAGE(通用语言)。从模型中获取用于程序设计和基本职责分配的术语。让程序代码成为模型的表达,代码的改变可能会是模型的改变。而其影响势必要波及接下来相应的项目活动。完全依赖模型的实现通常需要支持建模范式的软件开发工具和语言,比如面向对象的编程。

4. 为什么模型对用户至关重要?

如果程序设计基于一个能够反映出用户和领域专家所关心的基本问题的模型,那么与其他设计方式相比,这种设计可以将其主旨更明确地展示给用户。让用户了解模型,将使他们有更多机
会挖掘软件的潜能,也能使软件的行为合乎情理、前后一致。

  1. HANDS-ON MODELER(亲手实践建模者)

如果编写代码的人员认为自己没必要对模型负责,或者不知道如何让模型为应用程序服务, 那么这个模型就和程序没有任何关联。如果开发人员没有意识到改变代码就意味着改变模型,那 么他们对程序的重构不但不会增强模型的作用,反而还会削弱它的效果。同样,如果建模人员不 参与到程序实现的过程中,那么对程序实现的约束就没有切身的感受,即使有,也会很快忘记。 MODEL-DRIVEN DESIGN的两个基本要素(即模型要支持有效的实现并抽象出关键的领域知识)已 经失去了一个,最终模型将变得不再实用。最后一点,如果分工阻断了设计人员与开发人员之间 的协作,使他们无法转达实现MODEL-DRIVEN DESIGN的种种细节,那么经验丰富的设计人员则不 能将自己的知识和技术传递给开发人员。

任何参与建模的技术人员,不管在项目中的主要职责是什么,都必须花时间了解代码。任何 负责修改代码的人员则必须学会用代码来表达模型。每一个开发人员都必须不同程度地参与模型 讨论并且与领域专家保持联系。参与不同工作的人都必须有意识地通过UBIQUITOUS LANGUAGE 与接触代码的人及时交换关于模型的想法。

第二部分模型驱动设计的构造块

image.png

1. 为什么出现分层架构?

在面向对象的程序中,常常会在业务对象中直接写入用户界面、数据库访问等支持代码。而 一些业务逻辑则会被嵌入到用户界面组件和数据库脚本中。这么做是为了以最简单的方式在短期 内完成开发工作。
如果与领域有关的代码分散在大量的其他代码之中,那么查看和分析领域代码就会变得异常 困难。对用户界面的简单修改实际上很可能会改变业务逻辑,而要想调整业务规则也很可能需要 对用户界面代码、数据库操作代码或者其他的程序元素进行仔细的筛查。这样就不太可能实现一 致的、模型驱动的对象了,同时也会给自动化测试带来困难。考虑到程序中各个活动所涉及的大 量逻辑和技术,程序本身必须简单明了,否则就会让人无法理解。

给复杂的应用程序划分层次。在每一层内分别进行设计,使其具有内聚性并且只依赖于 它的下层。采用标准的架构模式,只与上层进行松散的耦合。将所有与领域模型相关的代码 放在一个层中,并把它与用户界面层、应用层以及基础设施层的代码分开。领域对象应该将 重点放在如何表达领域模型上,而不需要考虑自己的显示和存储问题,也无需管理应用任务 等内容。这使得模型的含义足够丰富,结构足够清晰,可以捕捉到基本的业务知识,并有效 地使用这些知识。

2. THE SMART UI“反模式”

如果一个经验并不丰富的项目团队要完成一个简单的项目,却决定使用MODEL-DRIVEN DESIGN以及LAYERED ARCHITECTURE,那么这个项目组将会经历一个艰难的学习过程。团队成员 不得不去掌握复杂的新技术,艰难地学习对象建模。(即使有这本书的帮助,这也依然是一个具 有挑战性的任务!)对基础设施和各层的管理工作使得原本简单的任务却要花费很长的时间来完 成。简单项目的开发周期较短,期望值也不是很高。所以,早在项目团队完成任务之前,该项目 就会被取消,更谈不上去论证有关这种方法的许多种令人激动的可行性了。
即使项目有更充裕的时间,如果没有专家的帮助,团队成员也不太可能掌握这些技术。最后, 假如他们确实能够克服这些困难,恐怕也只会开发出一套简单的系统。因为这个项目本来就不需 要丰富的功能。

因此:

在用户界面中实现所有的业务逻辑。将应用程序分成小的功能模块,分别将它们实现成用户 界面,并在其中嵌入业务规则。用关系数据库作为共享的数据存储库。使用自动化程度最高的用 户界面创建工具和可用的可视化编程工具。

3. 软件中所表示的模型?

  1. 关联: 模型中每个可遍历的关联,软件中都要有同样属性的机制。
  2. ENTITY:一些对象主要不是由它们的属性定义的。它们实际上表示了一条“标识线”(A Thread of Identity),这条线跨越时间,而且常常经历多种不同的表示。有时,这样的对象必须与另一个具 有不同属性的对象相匹配。而有时一个对象必须与具有相同属性的另一个对象区分开。错误的标 识可能会破坏数据。 主要由标识定义的对象被称作ENTITY
  3. VALUE OBJECT: 用于描述领域的某个方面而本身没有概念标识的对象称为VALUE OBJECT(值对象)。VALUEOBJECT被实例化之后用来表示一些设计元素,对于这些设计元素,我们只关心它们是什么,而不
    关心它们是谁。
  4. SERVICE: 有时,对象不是一个事物。在某些情况下,最清楚、最实用的设计会包含一些特殊的操作,这些操作从概念上讲不属于 任何对象。与其把它们强制地归于哪一类,不如顺其自然地在模型中引入一种新的元素,这就是SERVICE(服务)。

当领域中的某个重要的过程或转换操作不是ENTITY或VALUE OBJECT的自然职责时,应该 在模型中添加一个作为独立接口的操作,并将其声明为SERVICE。定义接口时要使用模型语言, 并确保操作名称是UBIQUITOUS LANGUAGE中的术语。此外,应该使SERVICE成为无状态的。

  1. MODULE:: MODULE为人们提供了两种观察模型的方式,一是可以在MODULE中查看细节,而不会被整个模型淹没,二是观察MODULE之间的关系,而不考虑其内部细节。MODULE从更大的角度描述了领域。

每个人都会使用MODULE,但却很少有人把它们当做模型中的一个成熟的组成部分。代码按 照各种各样的类别进行分解,有时是按照技术架构来分割的,有时是按照开发人员的任务分工来 分割的。甚至那些从事大量重构工作的开发人员也倾向于使用项目早期形成的一些MODULE。
众所周知,MODULE之间应该是低耦合的,而在MODULE的内部则是高内聚的。耦合和内聚的 解释使得MODULE听上去像是一种技术指标,仿佛是根据关联和交互的分布情况来机械地判断它 们。然而,MODULE并不仅仅是代码的划分,而且也是概念的划分。一个人一次考虑的事情是有限 的(因此才要低耦合)。不连贯的思想和“一锅粥”似的思想同样难于理解(因此才要高内聚)。

因此:

选择能够描述系统的MODULE,并使之包含一个内聚的概念集合。这通常会实现MODULE之 间的低耦合,但如果效果不理想,则应寻找一种更改模型的方式来消除概念之间的耦合,或者找 到一个可作为MODULE基础的概念(这个概念先前可能被忽视了),基于这个概念组织的MODULE 可以以一种有意义的方式将元素集中到一起。找到一种低耦合的概念组织方式,从而可以相互独 立地理解和分析这些概念。对模型进行精化,直到可以根据高层领域概念对模型进行划分,同时 相应的代码也不会产生耦合。
MODULE的名称应该是UBIQUITOUS LANGUAGE中的术语。MODULE及其名称应反映出领域的 深层知识。

4. 领域对象的生命周期

image.png

1. AGGREGATE
我们需要用一个抽象来封装模型中的引用。AGGREGATE就是一组相关对象的集合,我
们把它作为数据修改的单元。每个AGGREGATE都有一个根(root)和一个边界(boundary)。边界 定义了AGGREGATE的内部都有什么。根则是AGGREGATE所包含的一个特定ENTITY。对AGGREGATE 而言,外部对象只可以引用根,而边界内部的对象之间则可以互相引用。除根以外的其他ENTITY 都有本地标识,但这些标识只在AGGREGATE内部才需要加以区别,因为外部对象除了根ENTITY之 外看不到其他对象。

我们应该将ENTITY和VALUE OBJECT分门别类地聚集到AGGREGATE中,并定义每个 AGGREGATE的边界。在每个AGGREGATE中,选择一个ENTITY作为根,并通过根来控制对边界内 其他对象的所有访问。只允许外部对象保持对根的引用。对内部成员的临时引用可以被传递出去, 但仅在一次操作中有效。由于根控制访问,因此不能绕过它来修改内部对象。这种设计有利于确 保AGGREGATE中的对象满足所有固定规则,也可以确保在任何状态变化时AGGREGATE作为一个 整体满足固定规则。

2. FACTORY

对象的创建本身可以是一个主要操作,但被创建的对象并不适合承担复杂的装配操作。将这 些职责混在一起可能产生难以理解的拙劣设计。让客户直接负责创建对象又会使客户的设计陷入 混乱,并且破坏被装配对象或AGGREGATE的封装,而且导致客户与被创建对象的实现之间产生 过于紧密的耦合。

复杂的对象创建是领域层的职责,然而这项任务并不属于那些用于表示模型的对象。

因此:

应该将创建复杂对象的实例和AGGREGATE的职责转移给单独的对象,这个对象本身可能没有 承担领域模型中的职责,但它仍是领域设计的一部分。提供一个封装所有复杂装配操作的接口, 而且这个接口不需要客户引用要被实例化的对象的具体类。在创建AGGREGATE时要把它作为一 个整体,并确保它满足固定规则

3. REPOSITORY

领域驱动设计的目标是通过关注领域模型(而不是技术)来创建更好的软件。假设开发人 员构造了一个SQL查询,并将它传递给基础设施层中的某个查询服务,然后再根据得到的表行 数据的结果集提取出所需信息,最后将这些信息传递给构造函数或FACTORY。开发人员执行这一 连串操作的时候,早已不再把模型当作重点了。我们很自然地会把对象看作容器来放臵查询出 来的数据,这样整个设计就转向了数据处理风格。虽然具体的技术细节有所不同,但问题仍然 存在——客户处理的是技术,而不是模型概念。

客户需要一种有效的方式来获取对已存在的领域对象的引用。如果基础设施提供了这方面的 便利,那么开发人员可能会增加很多可遍历的关联,这会使模型变得非常混乱。另一方面,开发 人员可能使用查询从数据库中提取他们所需的数据,或是直接提取具体的对象,而不是通过 AGGREGATE的根来得到这些对象。这样就导致领域逻辑进入查询和客户代码中,而ENTITY和 VALUE OBJECT则变成单纯的数据容器。采用大多数处理数据库访问的技术复杂性很快就会使客 户代码变得混乱,这将导致开发人员简化领域层,最终使模型变得无关紧要。

随意的数据库查询会破坏领域对象的封装和AGGREGATE。技术基础设施和数据库访问机制的暴露会增加客户的复杂度,并妨碍模型驱动的
设计。

只为那些确实需要直接访问的AGGREGATE根提供REPOSITORY。 让客户始终聚焦于模型,而将所有对象的存储和访问操作交给REPOSITORY来完成。

5. 如何为哪些不太明显的概念建模?

  • 显示的约束: 即将约束的细节 单独抽象出来,进而显示的表达出 概念
  • 将过程建模为领域对象: 约束和过程是两大类模型概念,对象是用来封装过程的,这样我们只需考虑对象的业务目的或意图就可以了。典型的其实就是我们经常使用的策略模式
  • SPECIFICATION: 封装规则,接口把规则显式地表示出来,同时便于规则的测试

第三部分通过重构来加深理解

image.png

很多过度设计(overengineering)借着灵活性的名义而得到合理的外衣。但是,过多的抽象 层和间接设计常常成为项目的绊脚石。看一下真正为用户带来强大功能的软件设计,你常常会发 现一些简单的东西。简单并不容易做到。

倾听领域专家使用的语言。有没有一些术语能够简洁地表达出复杂的概念?他们有没有纠正 过你的用词(也许是很委婉的提醒)?当你使用某个特定词语时,他们脸上是否已经不再流露出 迷惑的表情?这些都暗示了某个概念也许可以改进模型。

1. INTENTION-REVEALINGINTERFACES

如果开发人员为了使用一个组件而必须要去研究它的实现,那么就失去了封装的价值。当某 个人开发的对象或操作被别人使用时,如果使用这个组件的新的开发者不得不根据其实现来推测 其用途,那么他推测出来的可能并不是那个操作或类的主要用途。如果这不是那个组件的用途, 虽然代码暂时可以工作,但设计的概念基础已经被误用了,两位开发人员的意图也是背道而驰。

2. SIDE-EFFECT-FREE FUNCTION

多个规则的相互作用或计算的组合所产生的结果是很难预测的。开发人员在调用一个操作 时,为了预测操作的结果,必须理解它的实现以及它所调用的其他方法的实现。如果开发人员不 得不“揭开接口的面纱”,那么接口的抽象作用就受到了限制。如果没有了可以安全地预见到结 果的抽象,开发人员就必须限制“组合爆炸”1,这就限制了系统行为的丰富性。

因此:

尽可能把程序的逻辑放到函数中,因为函数是只返回结果而不产生明显副作用的操作。严格 地把命令(引起明显的状态改变的方法)隔离到不返回领域信息的、非常简单的操作中。当发现 了一个非常适合承担复杂逻辑职责的概念时,就可以把这个复杂逻辑移到VALUE OBJECT中,这 样可以进一步控制副作用。

3. ASSERTION

如果操作的副作用仅仅是由它们的实现隐式定义的,那么在一个具有大量相互调用关系的系 统中,起因和结果会变得一团糟。理解程序的唯一方式就是沿着分支路径来跟踪程序的执行。封 装完全失去了价值。跟踪具体的执行也使抽象失去了意义。

因此:

把操作的后置条件和类及AGGREGATE的固定规则表述清楚。如果在你的编程语言中不能直 接编写ASSERTION,那么就把它们编写成自动的单元测试。还可以把它们写到文档或图中(如果 符合项目开发风格的话)。
寻找在概念上内聚的模型,以便使开发人员更容易推断出预期的ASSERTION,从而加快学习 过程并避免代码矛盾。

4. CONCEPTUAL CONTOUR

如果把模型或设计的所有元素都放在一个整体的大结构中,那么它们的功能就会发生重复。 外部接口无法给出客户可能关心的全部信息。由于不同的概念被混合在一起,它们的意义变得很 难理解。
而另一方面,把类和方法分解开也可能是毫无意义的,这会使客户更复杂,迫使客户对象去 理解各个细微部分是如何组合在一起的。更糟的是,有的概念可能会完全丢失。铀原子的一半并 不是铀。而且,粒度的大小并不是唯一要考虑的问题,我们还要考虑粒度是在哪种场合下使用的。

因此:

把设计元素(操作、接口、类和AGGREGATE)分解为内聚的单元,在这个过程中,你对领 域中一切重要划分的直观认识也要考虑在内。在连续的重构过程中观察发生变化和保证稳定的规 律性,并寻找能够解释这些变化模式的底层CONCEPTUAL CONTOUR。使模型与领域中那些一致的 方面(正是这些方面使得领域成为一个有用的知识体系)相匹配

5. STANDALONE CLASS

即使是在MODULE内部,设计也会随着依赖关系的增加而变得越来越难以理解。这加重了
我们的思考负担,从而限制了开发人员能处理的设计复杂度。隐式概念比显式引用增加的负担
更大。

低耦合是对象设计的一个基本要素。尽一切可能保持低耦合。把其他所有无关概念提取到对
象之外。这样类就变得完全独立了,这就使得我们可以单独地研究和理解它。每个这样的独立类
都极大地减轻了因理解MODULE而带来的负担。

尽力把最复杂的计算提取到STANDALONE CLASS(独立的类)中,实现此目的的一种方法是

从存在大量依赖的类中将VALUE OBJECT建模出来。

低耦合是减少概念过载的最基本办法。独立的类是低耦合的极致。

6. CLOSURE OF OPERATION

在适当的情况下,在定义操作时让它的返回类型与其参数的类型相同。如果实现者 (implementer)的状态在计算中会被用到,那么实现者实际上就是操作的一个参数,因此参数和返回值应该与实现者有相同的类型。这样的操作就是在该类型的实例集合中的闭合操作。闭合 操作提供了一个高层接口,同时又不会引入对其他概念的任何依赖。

这种模式更常用于VALUE OBJECT的操作。

7. 声明式设计

通常是指一种编程方式— 把程序或程序的一部分写成一种可执行的规格(specification)。使用声明式设计时,软件实际上是由一些非常精确的属性描述来控制的。是一种设计理念,是一种工作模式,透传出来的是“把方便留给别人,把麻烦留给自己”的哲学

第四部分战略设计

image.png

1. BOUNDEDCONTEXT
任何大型项目都会存在多个模型。而当基于不同模型的代码被组合到一起后,软件就会出现 bug、变得不可靠和难以理解。团队成员之间的沟通变得混乱。人们往往弄不清楚一个模型不应 该在哪个上下文中使用。
因此:

明确地定义模型所应用的上下文。根据团队的组织、软件系统的各个部分的用法以及物理表 现(代码和数据库模式等)来设臵模型的边界。在这些边界中严格保持模型的一致性,而不要受 到边界之外问题的干扰和混淆。
2. CONTINUOUSINTEGRATION

CONTINUOUS INTEGRATION是指把一个上下文中的所有工作足够频繁地合并到一起,并使它们 保持一致,以便当模型发生分裂时,可以迅速发现并纠正问题。像领域驱动设计中的其他方法一 样,CONTINUOUS INTEGRATION也有两个级别的操作:(1) 模型概念的集成;(2) 实现的集成。

因此:

建立一个把所有代码和其他实现工件频繁地合并到一起的过程,并通过自动化测试来快速查 明模型的分裂问题。严格坚持使用UBIQUITOUS LANGUAGE,以便在不同人的头脑中演变出不同的 概念时,使所有人对模型都能达成一个共识。

3.CONTEXTMAP

image.png

只有一个BOUNDED CONTEXT并不能提供全局视图。其他模型的上下文可能仍不清楚而且还 在不断变化。

其他团队中的人员并不是十分清楚CONTEXT的边界,他们会不知不觉地做出一些更改,从而 使边界变得模糊或者使互连变得复杂。当不同的上下文必须互相连接时,它们可能会互相重叠。

因此:

识别在项目中起作用的每个模型,并定义其BOUNDED CONTEXT。这包括非面向对象子系统 的隐含模型。为每个BOUNDED CONTEXT命名,并把名称添加到UBIQUITOUS LANGUAGE中。

描述模型之间的联系点,明确所有通信需要的转换,并突出任何共享的内容。 先将当前的情况描绘出来。以后再做改变。**

正如里根总统在裁减核武器谈判时所说 的名言**“信任,但要确认”**。这句话一语道破了双边事务的核心——这是连接上下文的又一个隐喻。

4. 模式:SHAREDKERNEL

从领域模型中选出两个团队都同意共享的一个子集。当然,除了这个模型子集以外,还包括 与该模型部分相关的代码子集,或数据库设计的子集。这部分明确共享的内容具有特殊的地位, 一个团队在没与另一个团队商量的情况下不应擅自更改它。 功能系统要经常进行集成,但集成的频率应该比团队中CONTINUOUS INTEGRATION的频率低
一些。在进行这些集成的时候,两个团队都要运行测试。

5. CUSTOMER/SUPPLIERDEVELOPMENTTEAM

游团队依赖于上游团队,但上游团队却不负责下游团队的产品交付。要琢磨拿什么来影响 对方团队,是人性呢,还是时间压力,亦或其他诸如此类的,这需要耗费大量额外的精力。因此, 正式规定团队之间的关系会使所有人工作起来更容易。这样,就可以对开发过程进行组织,均衡 地处理两个用户群的需求,并根据下游所需的特性来安排工作。

因此:

两个团队之间建立一种明确的客户/供应商关系。在计划会议中,下游团队相当于上游团 队的客户。根据下游团队的需求来协商需要执行的任务并为这些任务做预算,以便每个人都知道 双方的约定和进度。
两个团队共同开发自动化验收测试,用来验证预期的接口。把这些测试添加到上游团队的测 试套件中,以便作为其持续集成的一部分来运行。这些测试使上游团队在做出修改时不必担心对 下游团队产生副作用。

6. CONFORMIST

当两个开发团队具有上/下游关系时,如果上游团队没有动力来满足下游团队的需求,那么 下游团队将无能为力。出于利他主义的考虑,上游开发人员可能会做出承诺,但他们可能不会履 行承诺。下游团队出于良好的意愿会相信这些承诺,从而根据一些永远不会实现的特性来制定计 361 划。下游项目只能被搁臵,直到团队最终学会利用现有条件自力更生为止。下游团队不会得到根 据他们的需求而量身定做的接口。

如果上游设计的质量不是很差,而且风格也能兼容的话,那么最好不要再开发一 个独立的模型。这种情况下可以使用CONFORMIST(跟随者)模式。

因此 :

通过严格遵从上游团队的模型,可以消除在BOUNDED CONTEXT之间进行转换的复杂性。尽 管这会限制下游设计人员的风格,而且可能不会得到理想的应用程序模型,但选择CONFORMITY 模式可以极大地简化集成。此外,这样还可以与供应商团队共享UBIQUITOUS LANGUAGE。供应商 处于统治地位,因此最好使沟通变容易。他们从利他主义的角度出发,会与你分享信息。

7. ANTICORRUPTIONLAYER

image.png

创建一个隔离层,以便根据客户自己的领域模型来为客户提供相关功能。这个层通过另一个 系统现有接口与其进行对话,而只需对那个系统作出很少的修改,甚至无需修改。在内部,这个 层在两个模型之间进行必要的双向转换。

8. SEPARATEWAY

集成总是代价高昂,而有时获益却很小。

因此:

声明一个与其他上下文毫无关联的BOUNDED CONTEXT,使开发人员能够在这个小范围内找 到简单、专用的解决方案

9. OPENHOSTSERVICE

当一个子系统必须与大量其他系统进行集成时,为每个集成都定制一个转换层可能会减慢团队分析瘫痪,analysis paralysis,指一个项目在大量的分析工作面前陷入困境 队的工作速度。需要维护的东西会越来越多,而且进行修改的时候担心的事情也会越来越多

因此:

定义一个协议,把你的子系统作为一组SERVICE供其他系统访问。开放这个协议,以便所有 需要与你的子系统集成的人都可以使用它。当有新的集成需求时,就增强并扩展这个协议,但个 别团队的特殊需求除外。满足这种特殊需求的方法是使用一次性的转换器来扩充协议,以便使共 享协议简单且内聚。

10. PUBLISHEDLANGUAGE


本文地址:https://www.6aiq.com/article/1604021305358
本文版权归作者和AIQ共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出