TypeScript如何实现DDD的值对象?


值对象是领域驱动设计的主要组件之一。这是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它来确定相等性。这是一种完成结构相等的方法。