Elixir: 函数装饰器

Java 模式里面有一个概念叫「对修改关闭, 对扩展开放」, 这是一个面向对象的组件可重用原则. 装饰器就是实现该原则的一个经典实例.

装饰器原理

通过符号注解的方式, 给被注解的函数或对象添加新功能, 重写现有的功能, 而又不对现有的代码做变化的一种方法. 它对使用者是透明的. 通过装饰器可以实现的常用功能包括:

  • 访问控制

  • 计时器探针, 检测函数的运行时间

  • 日志记录

在Elixir中, 主要是对函数进行装饰. 一个函数装饰器是形似@decorate 的一个符号注解, 紧接着函数定义的上一行, 它可以用于给Elixir函数添加额外的功能. 函数装饰器运行时开销为0, 因为它是在编译时执行的.

装饰器以函数作为参数, 并且返回一个经过修改, 或添加了新功能的函数. 它是一个高阶函数.

defmodule MyModule do
  use PrintDecorator

  @decorate print()
  def square(a) do
    a * a
  end
end

函数装饰器实际上是Elixir宏.

Elixir 中的函数装饰器, 我们用到了 decorator 这个库.

装饰器的定义

定义装饰器是比较简单的, 创建一个模块, 并且在模块中 use Decorator.Define, [print: 0], 这样就定义了一个名称为print的装饰器了.

下面是一个装饰器的完整定义示例:

defmodule PrintDecorator do
  # 声明装饰器的名号, 参数数量
  use Decorator.Define, [print: 0]

  # 实现装饰器
  def print(body, context) do
    quote do
      IO.puts("Function called: " <> Atom.to_string(unquote(context.name)))
      unquote(body)
    end
  end
end

装饰器函数的参数(def print(...)) 为函数体(AST, 抽象语法树), 以及一个context参数, context 持有函数名称, 定义模块, 参数数量, 以及参数AST等信息.

编译时传参

装饰器可以进行编译时参数传递, 例如日志模块仅打印消息级别为:debug 的日志.

@decorate print(:debug)
def foo() do
...

对此, 需要修改装饰器模块的定义:

defmodule PrintDecorator do
  use Decorator.Define, [print: 1]

  def print(level, body, context) do
  # ...
  end
end

装饰器上下文

除了传入装饰器函数的函数体AST, 装饰器函数还有一个传入的上下文参数 context, context 参数包含了函数调用的相关信息:

def print(body, context) do
  Logger.debug("Function #{context.name}/#{context.arity} called in module #{context.module}!"
end

上下文具体包含哪些东西, 可通过 IO.puts #{inspect context} 打印出来.

下面是一个有用的示例, 是一个Phoenix框架中用户检查用户认证的一个装饰器宏.

defmodule Auth do
  def is_authorized(body, %{args: [conn, _params]}) do
    quote do
      if unquote(conn).assigns.user do
        unquote(body)
      else
        unquote(conn)
        |> send_resp(401, "unauthorized")
        |> halt()
      end
    end
  end
end

~完 (^_^!)

相关推荐