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

發表於 2021-01-21

這篇文章使用一個簡單的示例說明了域建模過程。第一步,確定實際問題。接下來,找到一種解決方法。接下來是建立初始域模型。之後,提供第一實施方式。然後,討論並解決了技術和邏輯上的挑戰。此外,還將解釋域模型及其實現之間的差異。該帖子最後建議即使對於小型專案,也應使用以問題為中心和模型驅動的方法。

 

問題識別

領域驅動設計強調解決的問題及其涉及的知識領域。在我的書中,領域模型被定義為“專注於[..]解決特定問題的知識抽象集”。這意味著,為了建立有用的模型,首先需要確定問題。作為一個具體示例,請考慮當在讀書時遇到的以下問題:我想知道文字中單個單詞的出現情況。

開始這問題提得似乎很有用,但是,它並不是真正描述問題,而是已經暗示了特定的解決方案。相關的問題是:我試圖通過計算單個單詞的出現來解決什麼問題?由於我不是英語母語,因此我通常不確定自己的詞彙多樣性。我想以某種方式衡量。因此,要解決的真正問題是確定文字的詞彙多樣性。

 

解決方法

發現問題後,就可以確定特定的解決方案。我的想法是,我可以通過檢視單個單詞的出現情況來確定詞彙的多樣性。但是,這種方法暗示僅軟體部分還不是完整的解決方案。相反,它將僅生成有助於推導詞彙使用指示的資料。確定實際的多樣性將由我作為使用者來完成。

 

初始域模型

如前所述,域模型是一組知識抽象。因此,它不必具有特定的表現形式或表示形式。更重要的是,雖然通常以某種方式表達模型,但各個工件通常只是資訊的子集。對於詞彙示例,可以通過純文字傳達知識抽象。請注意,這種方法沒有對問題或域模型的複雜性做出任何宣告。

目的是確定文字中詞彙多樣性的程度。“文字”代表單詞和標點符號的集合。“詞彙”可以定義為單個單詞的集合。術語“多樣性”包含了不同單詞的出現及其出現。表述“度”表示邊界和邊界之間的離散步驟。例如,詞彙多樣性被認為是不能通過軟體計算的主觀指標。

總而言之,可以將域模型方面總結為以下幾點:

  • 給定文字,應確定詞彙多樣性
  • 文字是單詞和標點符號的集合
  • 詞彙是一組單個單詞
  • 詞彙多樣性是手動確定的主觀指標

 

第一次實施

在定義域模型之後,可以開始實施。對於該示例,假定迭代軟體開發過程。結果,在域模型的完整性和正確性方面有較低的要求。相反,以上定義可以看作是進一步發展的初始草案。理解它的另一種方式是,以下迭代是實驗階段的一部分,而無需構建生產軟體。

以下程式碼是計算文字中單個單詞的出現次數的第一個實現:

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小問題

這篇文章說明的另一方面是所謂的簡單問題可能會給他們帶來很多複雜性。對於具有豐富和複雜域的大型軟體專案,通常建議使用域驅動設計及其單獨的模式。但是,從問題空間開始並在實施之前建立有用的概念模型總是有益的。即使對於諸如確定詞彙多樣性之類的小功能,以問題為中心和模型驅動的方法也很有價值。

相關文章