贫血的领域模型对于函数式编程影响分析

36 阅读5分钟

维基百科说贫血的领域模型"是指使用一个软件领域模型,其中的领域对象几乎不包含任何业务逻辑(验证、计算、业务规则等)"。

如果你不熟悉领域建模,很多项目都是这样开始的。

贫血的领域模型主要是缺乏封装和隔离的原因。

看看当我们有一个CreateUserUseCase 和一个EditUserUseCase ,验证逻辑会发生什么。

class CreateUserUseCase extends BaseUseCase<ICreateUserRequestDTO, ICreateUserResponseDTO> {
  constructor () {
    super();
  }

  exec (request: ICreateUserRequestDTO): ICreateUserResponseDTO {
    const { name, email } = request.user;
    const isValidName = UserValidator.validateName(name);
    const isValidEmail = UserValidator.validateEmail(email);
    if (isValidName && isValidEmail) {
      const user: User = User.create(name, email);
      // continue
    } else {
      // error
    }
  }
}

class EditUserUseCase extends BaseUseCase<IEditUserRequestDTO, IEditUserResponseDTO> {
  constructor () {
    super();
  }

  exec (request: IEditUserRequestDTO): IEditUserResponseDTO {
    const { name, email } = request.user;
    const isValidName = UserValidator.validateName(name);
    const isValidEmail = UserValidator.validateEmail(email);
    if (isValidName && isValidEmail) {
      const user: User = User.create(name, email);
      // continue
    } else {
      // error
    }
  }
}

你觉得这看起来很DRY吗?

我们必须对来自API的用户输入编写两次用户验证逻辑,在两个用例中都要重复一遍。

虽然这看起来不是什么大问题,但当我们添加n 更多在User 模型上操作的用例时,这就会失去控制。

不仅如此,如果我们向User 模型添加更多的属性,我们也必须为这些属性编写验证器。这就意味着我们必须回溯我们曾经写过的每一个用例,确保我们已经添加了新的验证规则。

如果我的计算是正确的,这意味着我们必须在头脑中跟踪n x m 不同的地方来更新验证规则,如果我们这样假设。

要维护的规则 = # 模型上的属性 x # 使用模型的服务

一个更好的方法是重构Validator,使其消耗整个原始User 对象,并在那里维护所有的验证规则。

class UserValidator extends BaseValidator<IUser> {
  constructor () {
    super();
  }

  private validateName (name: string) : boolean {
    // should be longer than 2 chars, less than 100
  }

  private validateEmail (email: string) : boolean {
    // regex to check string
  }

  public validate (user: IUser) : boolean {
    const isValidName = this.validateName(name);
    const isValidEmail = this.validateEmail(email);

    if (isValidName && isValidEmail) {
      const user: User = User.create(name, email);
      // continue
    } else {
      // error
    }
  }
}

在领域驱动设计中,我们的目标是将不变量/领域逻辑封装在靠近实际模型本身的地方,所以在这个例子中,在User.create(name: string, email: string) 工厂函数中。

像这样的验证器类在理论上是可以存在的,但我们希望在创建对象时在靠近模型的地方定义和运行这个逻辑,而不是在控制器、交互器或用例等更高级别的技术构件中。

在JavaScript或TypeScript中,我喜欢使用Joi进行验证。你可以创建一个漂亮的BaseValidator 抽象类,然后用像这样的可读验证代码实现validate() 方法。

Joi.string().min(3).max(100).required()

通过封装进行不变的验证只是丰富领域模型的好处之一

丰富领域模型的好处

更好的可发现性

如果我们确切地知道代码的归属,这就减少了我们寻找新代码应该被添加到哪里的时间。

我们的自然本能是看我们的模型,看它能做什么。当我们的模型准确地描述了它所能做的事情时,我们就会少花很多时间去寻找它在其他地方能做什么。

服务中的任何业务逻辑,如果能被确定为某个实体的唯一责任,就应该被转移到该实体。

任何不完全属于一个实体的逻辑都应该留在领域服务中。

任何在外部资源上执行操作的逻辑(比如使用Google Places API来获取地址的地理位置坐标)都应该属于应用服务

两个相关的软件设计原则协助提出了这个指标。

缺少重复性

建立在上一点的基础上。

如果我们知道代码应该放在哪里,就会减少重复的可能性。

重复的代码是精心设计的软件的敌人。如果我们必须保持冗余,写代码来描述一块业务逻辑的两个或更多的地方,我们就需要依靠我们的记忆来记住所有要更新的地方,如果它被改变的话。

人类通常不是最善于记住东西的人。

封闭性

封装是保护数据完整性的行为。

这就是我们通过用例方案解决的问题。

通过数据的完整性,我们的意思是 "这个数据被允许采取什么形式?","什么方法可以被调用,在什么时候?","为了创建这个对象,需要什么参数和前提条件?"?

贫血的领域模型的好处

因为使用DDD对一个领域进行建模需要时间,所以有时使用贫血领域模型确实更好。

根据各种不同的原因,它可能在 "短期内 "对你更有利。

例如,如果你是一个尚未验证产品的初创公司,或者你正在开发一个概念验证或一次性应用,我建议走贫血路线,因为你可以快速迭代。

我怎么知道我什么时候应该使用富域模型?

有一个甜蜜点,你会知道。好的是,你总是可以朝着这个方向重构;如果你已经写了SOLID和设计良好的代码,那就更容易了。

image.png


旁白:事实证明,贫血的领域模型对于函数式编程真的很有用,原因有几个。

在函数式编程中,对象是不可变的。这意味着没有办法让不变性变得不满足。在这种情况下,将模型和服务明确分开是非常有意义的。

CanShootLazers 如果你想在游戏引擎中的各种模型上添加CanRunCanFly ,这是有意义的,而且可以节省很多时间。