使用mixin隐藏模块中的私有函数

时间:2018-01-19 12:58:34

标签: elixir mixins

让我们说我有一个具有长功能的mixin,如下所示:

defmodule Mixin do
    @callback cb ...
    defmacro __using__(_) do
    quote do
        def long_function do
            #a lot of code
            cb(a, b, c)
            #even more code
        end
    end
    end
end

所以,为了便于阅读,我把它分成了更小的函数:

defmodule Mixin do
    @callback cb ...
    defmacro __using__(_) do
    quote do
        def long_function do
            subfunction_1()
            subfunction_2()
        end
        defp subfunction_1 do
            #some code
            cb(a, b, c)
        end
        defp subfunction_2 do
            #some code
            cb(a, b, c)
            #some code
        end
    end
    end
end

然后我用这个mixin

defmodule MyModule do
    use Mixin

    @Impl Mixin
    def cb(a, b, c) do
        # ...
    end

    def some_other_fun do
        # ...
    end
end

问题是,现在MyModule我可以访问subfunction_1subfunction_2(例如来自some_other_fun),这是不受欢迎的。此外,如果有人意外地实施了例如subfunction_1 MyModule中会出现隐藏的错误。由于子函数调用回调,它们或者不能被移出使用,或者模块名称需要传递给它们,这是一个丑陋的解决方案。

有什么办法吗?

2 个答案:

答案 0 :(得分:3)

首先让我们澄清一下符号:您正在使用模块来提供默认的回调实现。谈论mixins有点奇怪,因为你没有混合模块内容,而是生成新的东西。

通常,在用户模块中生成大量内容是一种不好的做法,这完全是因为您提出的所有问题。相反,您应该调用一个传递所有相关参数的函数,如下所示:

defmodule Mixin do
  @callback long_function(...) :: ...

  defmacro __using__(_) do
    quote do
      def long_function(arg1, arg2) do
        Mixin.long_functtion(arg1, arg2)
      end
    end
  end

  def long_function(arg1, arg) do
    ... actual implementation ...
  end
end

不需要在long_function模块中定义Mixin,它可以在任何地方。这个想法只是为了保持生成的代码简短。它也应该有助于单元测试,因为您可以直接测试long_function而无需生成大量模块。

我们将讨论此主题in the Macro guides too。我还可以推荐Chris McCord的Metaprogramming书。

答案 1 :(得分:1)

扩展现有答案:重构您给出的问题代码:

defmodule Mixin do
  @callback cb ...
  defmacro __using__(_) do
    quote do
      def long_function(args) do
        # you must use the module.fun syntax here, as this code
        # will be injected into the host
        # Also, pass __MODULE__ so that the Mixin functions can find the callback
        Mixin.long_function(__MODULE__, args)
      end
    end
  end

  # host_module is where `use Mixin` appears, and where `cb` is implemented
  def long_function(host_module, args) do
    subfunction_1(host_module, args)
    subfunction_2(host_module, args)
  end

  defp subfunction_1(host_module, args) do
    #some code
    cb(host_module, a, b, c)
  end

  defp subfunction_2(host_module, args) do
    #some code
    cb(host_module, a, b, c)
    #some code
  end

  defp cb(hostmodule, a, b, c) do
    # This is a potential source of runtime errors, you must 
    # manually check that it is being called correctly:
    hostmodule.cb(a, b, c)
  end
end

def MyModule do
  use Mixin
  # expands to: 
  # def long_function(args)

  @impl Mixin
  def cb(a, b, c), do: :something
end

现在你有了JoséValim提到的好处:有趣的代码是纯函数,可以很容易地测试,宏代码特别小。创建一个非常简单,非常可预测的CB模块版本,可以轻松测试混音功能/子功能。您还可以获得所需的语法:能够在不泄漏子功能的情况下调用MyModule.long_function(args)

如果非玩具示例有用,请查看create方法和create_changeset回调如何在我的库BaseModel中进行交互,后者使用此确切模式。