TypeScript中的DDD实践(十):规范

图片

一种提供许多应用的模式,比如在验证、创建和查询中 — 规范。

我小时候住的地方,互联网还不是那么普及,在接下来的15年里也没有,想象有一天只要坐在椅子上就能买到工具,简直是亵渎神明。

后来,我们有了一种叫做“远程商店”的东西。你知道,有两个家伙,一个表现得好像他不是这个星球的人,另一个向他解释如何用这种新工具制造一切。

我喜欢看这些。我的意思是,我是一个十岁的孩子——你能期望什么呢?看到这些工具可以做任何事情的片段特别有趣。吸尘器也可以做早餐。

你拿着一样东西,就可以把它应用到任何你想要的地方,这正是我第一次联想到领域驱动设计中的规格说明模式——你可以把它应用到任何你喜欢的地方——它简单易用。

但是它如何帮助我们呢?许多开发人员都有使用DDD的经验,但是几乎没有人使用Specification模式。它真的那么强大吗?

让我们开始吧。

进行验证

图片

Specification模式的第一个用例是验证。我们主要验证表单中的用户输入,但这是在表示层。有时,我们在创建期间执行验证,比如在Value Objects中。

在域层的上下文中,我们可以使用规范来验证实体的状态,并从集合中过滤实体。因此,域层上的验证已经比用户输入具有更广泛的意义。

让我们检查下面的例子:

一个简单的实体

enum MaterialType {    Plastic,}
class Product {  constructor(    public id: string,    public material: MaterialType,    public isDeliverable: boolean,    public quantity: number) {}}


一个简单的规格说明界面

interface Specification<T> {  isValid(item: T): boolean;}
class HasAtLeast implements Specification<Product> {  constructor(private pieces: number) {}
  isValid(item: Product): boolean {    return item.quantity >= this.pieces;  }}

在上面的例子中,有一个通用接口  Specification ,它只定义了一个方法  IsValid ,该方法接受任何项目的实例,如果项目通过验证规则,则返回一个布尔值。

作为类 HasAtLeast 的具体实现表明,如果 Product 提供了正确的数量,那么 IsValid 方法应该返回给我们。因此,它只是检查 Product  中的一些小细节。

为了避免我们需要为每个细节定义一个类,我们可以做一个小的升级,如下面的例子所示:

功能规格​​​​​​​

class FunctionSpecification<T> implements Specification<T> {  constructor(private func: (item: T) => boolean) {}
  isValid(item: T): boolean {    return this.func(item);  }}
const isPlasticProduct = (product: Product): boolean => {  return product.material == MaterialType.Plastic;}
const isDeliverableProduct = (product: Product): boolean => {  return product.isDeliverable;}

更有趣的验证器是两个函数,isPlasticProduct 和 isDeliverableProduct,我们可以用泛型类型 FunctionSpecification 包装这些函数。

这种类型使用与上述两种类型相同的签名嵌入函数,此外,它还提供了一些遵循  Specification  接口的方法。

因此,我们可以简单地定义验证函数,并用实现  Specification  接口的类包装它,而不是为每个新的 Specification 定义一个类。

最后一部分是提供  Specification  接口的实现,我们可以使用它来组合多个规范。

合并规格​​​​​​​

class AndSpecification<T> implements Specification<T> {  private specifications: Specification<T>[];
  constructor(...specifications: Specification<T>[]) {    this.specifications = specifications;  }
  isValid(item: T): boolean {    for (const specification of this.specifications) {      if (!specification.isValid(item)) {        return false;      }    }    return true;  }}
class OrSpecification<T> implements Specification<T> {  private specifications: Specification<T>[];
   constructor(...specifications: Specification<T>[]) {    this.specifications = specifications;  }
  isValid(item: T): boolean {    for (const specification of this.specifications) {      if (specification.isValid(item)) {          return true;      }    }    return false;  }}
class NotSpecification<T> implements Specification<T> {  constructor(private specification: Specification<T>) {}
  isValid(item: T): boolean {    return !this.specification.isValid(item)  }}

现在还有唯一的规范,AndSpecificationOrSpecification 和  NotSpecification 。
 这样的类帮助我们使用一个对象,该对象实现了 Specification 接口,但对所有规范进行分组验证。

在我们的例子中, AndSpecification 和 OrSpecification ,通过尊重  AND  和  OR  的逻辑,结合了它们所持有的多个规范。最后一个是  NotSpecification  ,它否定了嵌入式规范的结果。

最后,让我们来看看如何将所有这些类一起使用:

最后使用​​​​​​​

const firstProduct = new Product('', MaterialType.Iron, false, 1);const secondProduct = new Product('', MaterialType.Plastic, true, 50);
const simpleSpec = new AndSpecification(  new HasAtLeast(10),  new FunctionSpecification(isPlasticProduct),  new FunctionSpecification(isDeliverableProduct),);
console.log(simpleSpec.isValid(firstProduct));// output: false
console.log(simpleSpec.isValid(secondProduct));// output: true
const complexSpec = new OrSpecification(  new AndSpecification(    new HasAtLeast(10),    new FunctionSpecification(isPlasticProduct),    new FunctionSpecification(isDeliverableProduct),  ),  new AndSpecification(    new NotSpecification(new HasAtLeast(10)),    new NotSpecification(new FunctionSpecification(isPlasticProduct)),  ),);
console.log(complexSpec.isValid(firstProduct));// output: true
console.log(complexSpec.isValid(secondProduct));// output: true

上面的示例包含了 Specification 接口用于验证的具体用法,我们可以使用相同的业务规则来测试我们想要的每个实体。

这种方法提供了一种优雅的解决方案,在域层上提供复杂的验证逻辑,代码不言自明。

查询

图片

Specification 模式在 ORM 框架中扮演着重要的角色,在很多情况下,我们不需要为这个用例实现 Specification,至少在我们使用任何 ORM 的情况下是这样。

尽管如此,当我们发现在域级别上对存储库的查询可能太复杂时,我们需要更多的可能性来过滤所需的实体。

实现可以如下所示。

查询规范​​​​​​​

interface Specification {  query(): string;  value(): any[];}
abstract class CombineSpecification implements Specification {  private specifications: Specification[];  private separator: string;
  constructor(separator: string, ...specifications: Specification[]) {    this.specifications = specifications;    this.separator = separator;  }
  query(): string {    const queries: string[] = [];    for (const specification of this.specifications) {      queries.push(specification.query());    }        return `(${queries.join(' ' + this.separator + ' ')})`;  }    value(): any[] {    const values: any[] = [];    for (const specification of this.specifications) {      values.push(...specification.value());    }        return values;  }}
class AndSpecification extends CombineSpecification {  constructor(...specifications: Specification[]) {    super('AND', ...specifications);  }}
class OrSpecification extends CombineSpecification {  constructor(...specifications: Specification[]) {    super('OR', ...specifications);  }}
class FunctionSpecification implements Specification {  constructor(private func: () => string) {}
  query(): string {    return this.func();  }    value(): unknown[] {    return [];  }}
const isPlasticProduct = (): string => {  return `material = 'plastic'`;}
const isDeliverableProduct = (): string => {  return 'deliverable = 1'}

在新的实现中,Specification 接口提供了两个方法,Query 和 Values 。我们使用它们来获取特定 Specification 的查询字符串及其可能持有的值。

我们再次看到额外的Specification, 
AndSpecification 和 OrSpecification 。在这种情况下,它们连接所有底层查询,这取决于它们所呈现的操作符,并合并所有值。

在域层上使用这样的Specification是有问题的。正如您从输出中看到的,Specification提供了类似SQL的语法,这过于深入到技术细节中。


使用规范​​​​​​​

const spec = new OrSpecification(  new AndSpecification(    new FunctionSpecification(isPlasticProduct),    new FunctionSpecification(isDeliverableProduct),  ),  new AndSpecification(    new FunctionSpecification(isPlasticProduct),  ),  );
console.log(spec.query());// ((material = 'plastic' AND deliverable = 1) OR (material = 'plastic'))

在这种情况下,解决方案可能是在域层上为不同的规范定义接口,并在基础架构层上实现实际的实现。

或者重构代码,使 Specification 包含字段名、操作和值的信息,然后在基础架构层上使用映射器将这些 Specification 映射到 SQL 查询。

为了创造

图片

Specification的一个简单用例是创建一个可以变化很多的复杂对象。在这种情况下,我们可以将其与工厂模式结合起来,或者在域服务中使用它。

让我们检查下面的例子。​​​​​​​

interface ProductSpecification {  create(product: Product): Product;}
class AndSpecification implements ProductSpecification {  private specifications: ProductSpecification[];
  constructor(...specifications: ProductSpecification[]) {    this.specifications = specifications;  }
  create(product: Product): Product {    for (const specification of this.specifications) {      product = specification.create(product);    }    return product;  }}
class HasAtLeast implements ProductSpecification {  constructor(private pieces: number) {}
  create(product: Product): Product {    return {      ...product,      quantity: this.pieces    };  }}
const isPlastic = (product: Product): Product => {  return {    ...product,    material: MaterialType.Plastic  };}
const isDeliverable = (product: Product): Product => {  return {    ...product,    isDeliverable: true  };}
class FunctionSpecification implements ProductSpecification {  constructor(private func: (product: Product) => Product) {}
  create(product: Product): Product {    return this.func(product);  }}
const spec = new AndSpecification(  new HasAtLeast(10),  new FunctionSpecification(isPlastic),  new FunctionSpecification(isDeliverable),);
console.log(spec.create({  id: "id-1"} as Product));// output: { "id": "id-1", "quantity": 10, "material": 0, "isDeliverable": true }

在上面的例子中,我们可以找到 Specification 的第三种实现,在这个场景中,ProductSpecification 支持一个方法,Create,它期望  Product ,调整它,并返回它。

再次,有 AndSpecification 来应用从多个规范定义的更改,但没有 OrSpecification 。在创建对象期间,我找不到 or 算法的实际用例。

即使它不存在,我们也可以引入  NotSpecification ,它可以与特定的数据类型一起工作,比如布尔值。在这个小例子中,我仍然找不到一个适合它的方法。

结论

规范是一种我们随处使用的模式,它出现在许多不同的案例中。今天,如果不使用规范,在域层上提供验证就不容易了。

至此,我们的TypeScript领域驱动设计(DDD)系列就全部介绍完了,在后续的文章中,会介绍一下TypeScript中实用的 SOLID 原则,欢迎关注。

 TypeScript领域驱动设计(DDD)系列:

1. TypeScript中的实用领域驱动设计(DDD):为什么重要?

2. TypeScript 中 DDD 的实践:值对象

3. 在TypeScript中实践DDD(领域驱动设计):实体

4. 在TypeScript中实践DDD(领域驱动设计):域服务

5. TypeScript中的DDD实践(五):域事件

6. TypeScript中的DDD实践(六):模块

7.   TypeScript中的DDD实践(七):聚合

8.   TypeScript中的DDD实践(八):工厂

9.   TypeScript中的DDD实践(九):库

欢迎关注公众号:文本魔术,了解更多

  • 67
    点赞
  • 53
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值