TypeScript如何實現DDD的值物件?

banq發表於2019-04-09

值物件是領域驅動設計的主要元件之一。這是TypeScript中的一個簡單的Value Object類。
在領域驅動設計中,值物件是幫助我們建立豐富且封裝的域模型的兩個原始概念之一。
實體和價值物件這兩個概念。
透過了解它與實體的不同之處,可以最好地理解值物件。它們的主要區別在於我們如何確定兩個值物件之間的身份標識以及我們如何確定兩個實體之間的身份標識。

實體身份標識
當我們關心模型的身份標識並能夠將該身份與模型的其他例項區分開來時,我們使用實體來建模域概念。
在我們確定身份的方式幫助我們確定它是否是一個實體或值物件。
一個常見的例子是為使用者建模。
在這個例子中,我們假設一個User是一個實體,因為我們確定兩個不同例項之間差異的方式是透過User的唯一識別符號辨別的。
我們在這裡使用的唯一識別符號是隨機生成的UUID或自動遞增的SQL ID,它們成為我們可以用來從某些永續性技術查詢的主鍵。

值物件
使用Value Objects,我們透過兩個例項的結構相等來建立身份標識。

意味著兩個物件具有相同的內容。這與引用相等/同一性不同,這意味著兩個物件是相同的。
為了識別彼此的兩個值物件,我們檢視物件的實際內容並基於此進行比較。
例如,實體User上可能存在屬性Name。我們如何判斷兩個Name是否相同?
這非常類似於比較兩個字串,對嗎?

"Nick Cave" === "Nick Cave" // true

"Kim Gordon" === "Nick Cave" // false


這很簡單。
我們User可能看起來像這樣:

interface IUser {
  readonly name: string
}

class User extends Entity<IUser> {
  public readonly name: string;

  constructor (props: IUser) {
    super(props);
    this.name = props.name;
  }
}


如果我們想限制使用者名稱的長度怎麼辦?假設它不能超過100個字元,並且必須至少為2個字元。
一種天真的方法是在建立此使用者的例項之前編寫一些驗證邏輯,可能在服務中:

class CreateUserService {
  public static createUser (name: string) : User{
    if (name === undefined || name === null || name.length <= 2 || name.length > 100) {
      throw new Error('User must be greater than 2 chars and less than 100.')
    } else {
      return new User(name)
    }
  }
}


這不太理想。如果我們想要處理編輯使用者的名字怎麼辦?

class EditUserService {
  public static editUserName (user: User, name: string) : void {
    if (name === undefined || name === null || name.length <= 2 || name.length > 100) {
      throw new Error('User must be greater than 2 chars and less than 100.')
    } else {
      user.name = name;
      // save
    }
  }
}

  1. 這不是真正適合這樣做的地方。
  2. 我們剛剛重複了相同的驗證邏輯。

我們最終會將過多的域邏輯和驗證放入服務中,而模型本身並沒有準確地封裝域邏輯。
我們稱之為貧血領域模型。
我們引入了值物件類來封裝應該進行驗證的位置,並滿足模型的不變數(驗證和域規則)。

如果我們要為name屬性建立一個類,我們可以共同定位name該類本身的所有驗證邏輯。
我們還將使constuctor私有,並使用一個靜態工廠方法來執行必須滿足的前提條件,以便使用建立有效name的構造器。

interface IName {
  value: string
}

class Name extends ValueObject<IName> {
  private constuctor (props: IName) {
    super(props);
  }

  public static create (name: string) : Name {
    if (name === undefined || name === null || name.length <= 2 || name.length > 100) {
      throw new Error('User must be greater than 2 chars and less than 100.')
    } else {
      return new User(name)
    }
  }
}


值物件類
這是一個Value Object類的示例。

import { shallowEqual } from "shallow-equal-object";

interface ValueObjectProps {
  [index: string]: any;
}

/**
 * @desc ValueObjects are objects that we determine their
 * equality through their structrual property.
 */

export abstract class ValueObject<T extends ValueObjectProps> {
  public readonly props: T;

  constructor (props: T) {
    this.props = Object.freeze(props);
  }

  public equals (vo?: ValueObject<T>) : boolean {
    if (vo === null || vo === undefined) {
      return false;
    }
    if (vo.props === undefined) {
      return false;
    }
    return shallowEqual(this.props, vo.props)
  }
}

看看equals方法。請注意,我們使用shallowEquals它來確定相等性。這是一種完成結構相等的方法。

相關文章