【问题标题】:Check that module implements behaviour检查模块是否实现了行为
【发布时间】:2022-01-25 20:06:05
【问题描述】:

我有一个行为和一个函数,它需要一个应该实现该行为的模块列表。我想检查传入的每个模块是否确实实现了该行为。我可以通过下面的MyBehaviour.implemented_by?/1 做到这一点,但我想知道是否有更直接的方法。

defmodule MyBehaviour do
  @callback do_something(String.t(), String.t()) :: no_return()

  def implemented_by?(module) do
    :attributes
    |> module.module_info()
    |> Enum.member?({:behaviour, [__MODULE__]})
  end
end

这是最好的检查方法吗?我在文档或 Elixir 论坛或任何地方都找不到任何东西。

我应该检查一下吗? 还是应该让责任完全由调用者承担?行为是否更多是关于“我想确保实现所需的一切”而不是“我希望其他人都知道我实现了所需的一切”?

有没有办法在 typespecs 中使用行为作为类型?我的函数规范可以说 args 应该实现我的行为,还是应该只使用 module()/atom()

【问题讨论】:

  • 您为什么要检查它,因为它的开发人员工作是在特定模块中实现行为,它是否与用户输入有某种关系?如果它与用户输入无关,我认为你不应该检查。
  • 主要是为了使错误更像是“这应该实现这种行为”而不是“这个函数是未定义的”,并在做任何其他事情之前检查它,但我倾向于只做假设一切顺利

标签: elixir


【解决方案1】:

有趣的问题。

行为是否更多是关于“我想确保实现所需的一切”而不是“我希望其他人都知道我实现了所需的一切”?

我的理解是,行为是模块作者和该模块的用户之间的约定:“我希望您为我提供一个可以完成所有这些事情的模块”。因此,模块用户有责任这样做。

行为函数的关键字是@callback,在我看来,通常定义行为的模块也是将使用该行为的模块(换句话说,调用回调)。行为实现者似乎有责任确保它正确实现行为,并通过编译时检查来帮助他们,但是对于需要行为的模块用户没有运行时帮助确保他们确实提供了有效的实现。

您提供运行时警告的解决方案对我来说看起来不错 - 但是可以在不提供 @behaviour 属性的情况下实现行为,因此在这种情况下它不会很好地工作。

如果行为实现者在其代码中声明了@behaviour,但忽略了编译器警告,则会出现更有用的错误消息:

警告:行为 ExpectBehaviour 所需的函数 foo/0 未实现(在模块 ClaimsItImplementsButDoesNot 中)

iex> ExpectBehaviour.use_behaviour(ClaimsItImplementsButDoesNot)
** (UndefinedFunctionError) function ClaimsItImplementsButDoesNot.foo/0 is undefined or private,
   but the behaviour ExpectBehaviour expects it to be present

但是,如果您只是传递一个未实现该行为的不相关模块,则情况并非如此:

iex> ExpectBehaviour.use_behaviour(DoesNotImplementOrClaimTo)
** (UndefinedFunctionError) function DoesNotImplementOrClaimTo.foo/0 is undefined or private

有没有办法在类型规范中使用行为作为类型?

行为不是类型,它是一组函数的规范,一个模块可以实现多种行为,所以我认为这没有意义。如上所述,将行为回调的使用限制在定义它的模块中似乎是明智的。

【讨论】:

  • 感谢您的详细回复!我想我在考虑的行为太像其他更静态类型的语言中的接口。
  • Necro-comment:String.t() 也不是真正的独特类型,它是更一般的 binary 类型的限制。当我以这种方式思考时,用它实现的行为来装饰一个模块是很有意义的。特别是因为一个重要的用途是依赖注入。
【解决方案2】:

如果你想在编译时强制执行,你也可以这样做:

defmodule Lib.EnforcedBehaviour do

  defmacro __using__(_) do
    quote location: :keep do
      @aftercompile Lib.EnforcedBehaviour
      @behaviour Lib.EnforcedBehaviour
    end
  end

  @callback enforced_function() :: atom

  def __after_compile__(%{module: module} = _env) do
    if !Kernel.function_exported?(module, :enforced_function, 0) do
      raise "expected implementation of 'enforced_function/0' in #{inspect module}."
    end  
  end

end

这很丑陋,但有效。

您现在可以这样做:

# Will fail during compilation
defmodule Lib.EnforcingBehaviour do
  use Lib.EnforcedBehaviour
end

# Will not fail during compilation
defmodule Lib.EnforcingBehaviour do
  use Lib.EnforcedBehaviour

  def enforced_function() do
  end

end

但是:请不要这样做:微笑:

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2016-09-29
    • 2018-10-19
    • 2010-09-21
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多