一种提供许多应用的模式,比如在验证、创建和查询中 — 规范。
我小时候住的地方,互联网还不是那么普及,在接下来的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)
}
}
现在还有唯一的规范,AndSpecification
,OrSpecification
和 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):为什么重要?
3.
在TypeScript中实践DDD(领域驱动设计):实体
4.
在TypeScript中实践DDD(领域驱动设计):域服务
欢迎关注公众号:文本魔术,了解更多