DDD+Javascript领域建模示例 -Alex Lawrence


这篇文章使用一个简单的示例说明了域建模过程。第一步,确定实际问题。接下来,找到一种解决方法。接下来是创建初始域模型。之后,提供第一实施方式。然后,讨论并解决了技术和逻辑上的挑战。此外,还将解释域模型及其实现之间的差异。该帖子最后建议即使对于小型项目,也应使用以问题为中心和模型驱动的方法。
 
问题识别
领域驱动设计强调解决的问题及其涉及的知识领域。在我的书中,领域模型被定义为“专注于[..]解决特定问题的知识抽象集”。这意味着,为了创建有用的模型,首先需要确定问题。作为一个具体示例,请考虑当在读书时遇到的以下问题:我想知道文本中单个单词的出现情况。
开始这问题提得似乎很有用,但是,它并不是真正描述问题,而是已经暗示了特定的解决方案。相关的问题是:我试图通过计算单个单词的出现来解决什么问题?由于我不是英语母语,因此我通常不确定自己的词汇多样性。我想以某种方式衡量。因此,要解决的真正问题是确定文本的词汇多样性。
 
解决方法
发现问题后,就可以确定特定的解决方案。我的想法是,我可以通过查看单个单词的出现情况来确定词汇的多样性。但是,这种方法暗示仅软件部分还不是完整的解决方案。相反,它将仅生成有助于推导词汇使用指示的数据。确定实际的多样性将由我作为用户来完成。
 
初始域模型
如前所述,域模型是一组知识抽象。因此,它不必具有特定的表现形式或表示形式。更重要的是,虽然通常以某种方式表达模型,但各个工件通常只是信息的子集。对于词汇示例,可以通过纯文本传达知识抽象。请注意,这种方法没有对问题或域模型的复杂性做出任何声明。
目的是确定文本中词汇多样性的程度。“文本”代表单词和标点符号的集合。“词汇”可以定义为单个单词的集合。术语“多样性”包含了不同单词的出现及其出现。表述“度”表示边界和边界之间的离散步骤。例如,词汇多样性被认为是不能通过软件计算的主观指标。
总而言之,可以将域模型方面总结为以下几点:

  • 给定文本,应确定词汇多样性
  • 文本是单词和标点符号的集合
  • 词汇是一组单个单词
  • 词汇多样性是手动确定的主观指标

 
第一次实施
在定义域模型之后,可以开始实施。对于该示例,假定迭代软件开发过程。结果,在域模型的完整性和正确性方面有较低的要求。相反,以上定义可以看作是进一步发展的初始草案。理解它的另一种方式是,以下迭代是实验阶段的一部分,而无需构建生产软件。
以下代码是计算文本中单个单词的出现次数的第一个实现:

const countWordOccurrences = text => {
  wordOccurrences = {};
  text.split(' ').forEach(word => {
    if (wordOccurrences[word] == null) wordOccurrences[word] = 0;
    wordOccurrences[word]++;
  });
  return wordOccurrences;
};

const wordOccurrences = countWordOccurrences(`This is a basic example.
  Also, this is only one of many possible examples.`);

console.log(wordOccurrences);
/* output: {
  This: 1, is: 2, a: 1, basic: 1, 'example.\n': 1, '': 1, 'Also,': 1,
  this: 1, only: 1, one: 1, of: 1, many: 1, possible: 1, 'examples.': 1
} */

该示例用法及其输出演示了初始解决方案的功能。

 
技术问题
第一次实施存在一些技术问题。这些方面不是由于模型中的缺陷所致,而是与将隐式需求正确集成到代码中有关:一个问题是标点符号被错误地视为单词的一部分。对于换行符也是如此。另一个问题是,多个空格导致创建空单词条目。这些方面不适合作为模型的显式部分,因为它们应被视为常识。
以下代码提供了经过重做的实现,克服了上述问题:

const wordRegex = /[a-z0-9]{1}[a-z0-9-]*/gi;

const countWordOccurrences = text => {
  wordOccurrences = {};
  Array.from(text.matchAll(wordRegex), match => match[0]).forEach(word => {
    if (wordOccurrences[word] == null) wordOccurrences[word] = 0;
    wordOccurrences[word]++;
  });
  return wordOccurrences;
};

const wordOccurrences = countWordOccurrences(`This is a basic example.
  Also, this is only one of many possible examples.`);

console.log(wordOccurrences);

第二段代码通过使用正则表达式解决了上述技术问题。此表达式定义两个规则。首先,每个单词都必须以字母数字字符开头。其次,第一个字符后面可以是字母数字字符和破折号的任意组合。
 
模型改进
重新设计的实现是一种改进,但仍然面临问题。有一些问题提示域模型中存在Bug缺陷:

  1. 一个Bug是该实现是区分大小写的,这导致具有不同大小写的相同单词的多个条目。
  2. 另外一个是:一个单词的单数和复数形式被认为是不同的事物。

可以如下更新域模型定义:
  • 给定文本,应确定词汇多样性
  • 文本是单词和标点符号的集合
  • 词汇集是单个单词
  • 词汇多样性是指示语言质量的指标
  • 一个单词的不同大小写被认为是相同的
  • 一个单词的单数和复数被认为是相同的

最后一个示例提供了一个反映最新域模型的实现:
const wordRegex = /[a-z0-9]{1}[a-z0-9-]*/gi;

const countWordOccurrences = (text, {asSingular}) => {
  wordOccurrences = {};
  text = text.toLowerCase();
  Array.from(text.matchAll(wordRegex), match => match[0]).forEach(word => {
    word = asSingular(word);
    if (wordOccurrences[word] == null) wordOccurrences[word] = 0;
    wordOccurrences[word]++;
  });
  return wordOccurrences;
};

const pluralize = require('pluralize');

const wordOccurrences = countWordOccurrences(`This is a basic example.
  Also, this is only one of many possible examples.`,
  {asSingular: pluralize.singular});

console.log(wordOccurrences);
/* output: {
  this: 2, is: 2, a: 1, basic: 1, example: 2,
  also: 1, only: 1, one: 1, of: 1, many: 1, possible: 1
} */


通过使输入文本为小写字母来减轻大小写敏感性。对于单数和复数形式的合并,实现中引入了dependency asSingular。此参数必须分配有一个单词并返回单数形式的操作。例如,将pluralize加载npm模块,并将其功能singular()作为依赖关系传入。这种方法可确保正确表达模型行为,同时又不受具体依赖。
 
模型与代码
域模型和实现所表达的知识之间存在差异。考虑一下我的书中的以下摘录:“实际的实现可能只反映基础抽象的一个子集,并最终处理无关的技术方面。” 词汇多样性示例说明了这两种说法。一方面,该实现没有表达完整的模型,因为它仅对每个单词的出现次数进行计数。其次,它还处理纯粹的技术问题,例如多个空格或换行符。
 
DDD小问题
这篇文章说明的另一方面是所谓的简单问题可能会给他们带来很多复杂性。对于具有丰富和复杂域的大型软件项目,通常建议使用域驱动设计及其单独的模式。但是,从问题空间开始并在实施之前创建有用的概念模型总是有益的。即使对于诸如确定词汇多样性之类的小功能,以问题为中心和模型驱动的方法也很有价值。