
1.3.7 使用装饰器进行AOP
在TypeScript中,装饰器是我最喜欢的功能之一。装饰器是作为一种实验性功能引入的,它是一段代码,可以在不修改类的内部实现的情况下,改变一个类的行为。通过这种概念,我们不必继承一个类,就可以修改它的行为。
如果你以前使用的是Java或C#等语言,可能会注意到装饰器与AOP技术很相似。AOP技术允许我们切开一段代码并将其分离到另外一个位置,从而提取出重复性代码。这意味着在我们的实现中,不必到处夹杂很大程度上是样板代码,但在运行应用程序时必须用到的一些代码。
要解释装饰器,最简单的方法是先来看一个示例。假设有一个类,只允许特定角色的用户访问某些方法,如下所示:

接下来,我们创建一个具有admin和user角色的用户,意味着能够调用这个类的两种方法:

我们将得到期望的输出,如下所示:

如果要创建一个只具有user角色的用户,那么我们期望的是该用户不能运行只能由管理员运行的代码。因为我们的代码没有检查角色,所以无论分配给用户什么角色,AdminOnly方法都会执行。要修改这段代码,一种方式是添加检查角色的代码,然后将其添加到每种方法中。
首先,我们创建一个简单的函数来检查当前用户是否属于特定的角色:

我们将修改现有的实现,在函数中调用这个检查函数,以确定是否允许user运行该方法:

观察这段代码会发现其中存在大量重复的代码。不只如此,这个实现中还存在一个bug。AdminOnly的IsInRole块中没有return语句,所以AdminOnly代码仍将运行,告诉我们该用户不属于admin角色,但是之后仍然会输出消息。这突显了重复代码的一个问题:很容易无意中引入不易察觉的bug。最后,这里的实现违反了良好的面向对象开发实现的一个基本原则。我们的类和方法在做一些不属于它们的工作。一段代码只应该做一件事,因此检查角色的工作不应该在这里完成。第2章在深入探讨面向对象开发思想时,将深入介绍这个主题。
接下来,我们来看看如何使用装饰器移除样板代码,解决单一职责的问题。
因为装饰器是一种实验性的ES5功能,所以在编写代码前,我们需要确保TypeScript知道我们要使用装饰器。通过在命令行运行下面的代码,我们可以实现这一点:

也可以在tsconfig文件中做如下设置:

启用了装饰器编译功能后,就可以编写我们的第一个装饰器,确保用户属于admin角色:


每当看到与上面的代码类似的函数定义时,就可以知道那是一个方法装饰器。TypeScript要求必须按照下面的顺序提供这些参数:

第一个参数用来指代我们要把该装饰器应用到的元素,第二个参数是该元素的名称,最后一个参数是要应用装饰器的方法的描述符,这就允许我们修改该方法的行为。我们必须有一个具有这种签名的函数来用作装饰器:

装饰器方法的内部机制没有看起来那么令人生畏。我们只不过是从描述符复制原来的方法,然后用自定义实现替换该方法。这个包装后的实现将被返回,遇到时就会执行:

在包装的实现中,执行了相同的角色检查。如果通过检查,就应用原来的方法。通过使用这种方法,就能够以一种一致的方式,保证在没有必要的时候,不会调用我们的方法。
要应用这个实现,我们需要在类中该方法前面的装饰器工厂函数名称之前使用@符号。添加装饰器时,必须避免在装饰器和方法之间使用分号,如下所示:

这段代码对于AdminOnly能够起到作用,但并不是特别灵活。随着我们添加更多角色,将需要添加越来越多实质上相同的函数。如果有一种方法让我们能够创建一个通用的函数来返回一个装饰器,并且让该函数接受一个参数来设置我们希望允许的角色,那就太好了。幸运的是,通过使用装饰器工厂,我们可以实现这种目的。
简而言之,TypeScript装饰器工厂是一个函数,它能够接受参数,并使用这些参数来返回实际的装饰器。只需要对我们的代码在几个地方做小调整,就能够得到一个可以工作的工厂,指定想要检查的角色:

唯一真正的区别是,这里使用了一个函数来返回装饰器(装饰器不再有名称),并且在装饰器内使用了工厂函数参数。现在就可以修改我们的类来使用这个工厂:

做出这种修改后,当调用方法时,只有管理员才能访问AdminOnly方法,而任何用户都能够调用AnyoneCanRun。另外一点需要注意的是,装饰器只能在类中使用。不能对一个独立的函数使用装饰器。
之所以将这种方法称作装饰器,是因为它遵循了装饰器模式。装饰器模式代表的技术用来向单独的对象添加行为,而不影响相同类的其他对象,也不需要创建子类。模式是针对软件工程中经常出现的问题提出的一种形式化解决方案,所以常常作为一种有用的名称来描述某种功能。除了装饰器模式,还有一种工厂模式。在学习本书的过程中,我们还将遇到其他模式,所以当学习完本书后,使用模式不会让你感到不知如何下手。
我们也可以将装饰器应用到类中的其他项。例如,如果我们想阻止未授权用户实例化类,就可以定义一个类装饰器。类装饰器添加到类定义上,期望收到类的构造函数作为函数。下面是从工厂创建的构造函数装饰器的示例:

应用这个装饰器时,同样需要使用@前缀。当代码试图为非管理员创建这个类的一个新实例时,应用程序将抛出一个错误,阻止创建新实例:

注意,我们一直没有在类中声明装饰器。因为装饰器的用法不适合装饰一个类,所以总是将它们创建为顶层函数,而不会看到类似于@MyClass.Role("admin")这样的语法。
除了装饰构造函数和方法,还可以装饰属性和访问器等。这里不详细介绍,但在本书后面将会看到装饰器的其他应用场景。另外还将介绍如何连用装饰器,其语法将与下面类似:
