如何用TypeScript进行领域驱动设计

299 阅读5分钟

在领域驱动设计中,价值对象是两个原始概念之一,帮助我们创建丰富和封装的领域模型。

这些概念是实体价值对象

要理解价值对象,最好先了解它与实体的区别。它们的主要区别在于我们如何确定两个Value Objects之间的身份,以及我们如何确定两个Entity之间的身份

实体身份

当我们关心模型的身份并能够将该身份与模型的其他实例区分开来时,我们使用一个实体来为一个领域概念建模。

我们确定该身份的方式有助于我们确定它是一个实体还是一个价值对象。

一个常见的例子是为一个用户建模。

在这个例子中,我们会说一个User 是一个实体,因为我们确定一个User 的两个不同实例之间区别的方式是通过它的唯一标识符

我们在这里使用的唯一标识符要么是随机生成的UUID,要么是一个自动递增的SQL ID,它成为一个主键,我们可以用它来从一些持久化技术中进行查询。


价值对象

对于价值对象,我们通过两个实例的结构平等来建立身份。

结构平等

结构平等意味着两个对象有相同的内容。这与指代平等/身份不同,指代平等/身份意味着两个对象是相同的

为了将两个价值对象相互识别出来,我们看一下对象的实际内容,并在此基础上进行比较。

例如,在User 实体上可能有一个Name 属性。

我们如何判断两个Name是否相同?

这很像比较两个字符串,对吗?

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

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

这很简单。

我们的User 实体可以看起来像这样。

domain/user.ts

interface UserProps {
  name: string
}

class User extends Entity<UserProps> {
  
  get name (): string {
    return this.props.name;
  }

  constructor (props: UserProps) {
    super(props);
  }
}

这是好的,但它可以更好。让我问一个问题。

如果我们想限制一个用户的名字的长度。比方说,它不能超过100个字符,而且必须至少是2个字符。

一个天真的方法是在我们创建这个用户的实例之前写一些验证逻辑,也许在一个服务中。

services/createUserService.ts

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)
    }
  }
}

这并不理想。如果我们想处理编辑一个用户的名字呢?

services/editUserService.ts

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. 我们只是重复了同样的验证逻辑。

实际上,很多项目都是这样开始超出范围的。我们最终把太多的领域逻辑和验证放到了服务中,而模型本身并没有准确地封装领域逻辑。

我们把这称为[贫血的领域模型]。

我们引入价值对象类来严格代表一个类型,并封装该类型的验证规则。

价值对象

我们以前就有过这样的例子,我们的User 实体的基本类,有一个string-ly typedname 属性。

domain/user.ts

interface UserProps {
  name: string
}

class User extends Entity<UserProps> {

  get name (): string {
    return this.props.name;
  }

  constructor (props: UserProps) {
    super(props);
  }
}

如果我们为name 属性创建一个类,我们可以把所有的验证逻辑都放在该类中,name

上限(最大长度),下限(最小长度),以及我们想要实现的任何算法,以去除空白,删除坏字符,等等,都可以在这里实现。

使用一个静态工厂方法和一个私有构造函数,我们可以确保为了创建一个有效的name ,必须满足的前提条件。

domain/name.ts

interface NameProps {
  value: string
}

class Name extends ValueObject<NameProps> {

  get value (): string {
    return this.props.value;
  }
  
  // Can't use the `new` keyword from outside the scope of the class.
  private constuctor (props: NameProps) {
    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 Name({ value: name })
    }
  }
}

然后,在User 类中,我们将更新UserProps 中的name 属性,使其成为Name 类型,而不是string

域名/用户.ts

interface UserProps {
  name: Name;
}

class User extends Entity<UserProps> {

  get name (): Name {
    return this.props.name;
  }

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

  public static create (props: IUser) {
    if (props.name === null || props.name === undefined) {
      throw new Error('Must provide a name for the user');
    } else {
      return new User(props);
    }
  }
}

价值对象类

下面是一个价值对象类的例子。

shared/domain/valueObject.ts

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 ,以确定平等。这是完成structural equality 的一种方法。

当有意义的时候,这个价值对象基类的子类也可以被扩展到包括像greaterThan(vo?: ValueObject<T>)lessThan(vo?: ValueObject<T>) 这样的方便方法。在这个例子中没有意义,但如果我们谈论的是像LoggingLevels 或BusinessRatings 这样的东西,它就有意义了。


在以后的文章中,我们将讨论。

  • 实体设计
  • 创建对象时更好的错误处理技术
  • 将贫血的代码从服务中转移到领域模型中去
  • 编写DTO来创建数据契约

这是用TypeScript进行领域驱动设计系列的一部分。如果这对你有用,请在评论中告诉我,并订阅新闻通讯,以便在新文章出来时得到通知。干杯!