我正在尝试在许多 GenServer 中使用 Phoenix 框架的 PubSub 模块,但是我希望有一个明确定义的主题列表来引用,而不是原始字符串。
目前我的代码看起来像这样:
# ...
def init(_args) do
Phoenix.PubSub.subscribe(:my_pubsub, "topic_name")
end
# ...
def handle_info({"topic_name", data = %SomeData{}}, state) do
# do stuff with data ...
end
# in a different module:
Phoenix.PubSub.broadcast(:my_pubsub, "topic_name", %SomeData{key: value})
这可行,但是使用原始字符串作为主题名称是有缺陷的,原因有几个:它要求我检查所有使用“topic_name”的地方以验证其拼写是否正确,如果存在现有主题名称,我需要查看代码才能找到它,如果我拼写错误,我不会收到警告,如果我想检查所有可能主题的列表,我必须查看代码并找到订阅或中给出的所有唯一主题广播电话——这也意味着我无法给他们提供合理的文档。
就我而言,使用原子作为主题名称也会有同样的问题,但 PubSub 只允许主题是字符串。
我想要的是一种在一个模块中定义所有主题并引用它们并对它们进行模式匹配的方法,如下所示:
def handle_info({Topics.some_topic, data = %SomeData{}}, state) do
# do stuff...
end
在我使用过的所有其他语言中,这种模式都很简单。 然而,Elixir 不提供任何在模块之间共享常量的方法(至少我找不到)。 有什么方法可以实现我正在寻找的东西,或者我需要习惯按照我在开头描述的方式进行事情吗?
不幸的是,这是 Erlang VM 的一个已知限制,它没有真正的常量机制。
如果您在同一个模块中,最简单的方法是仅使用模块属性:
@topic "my_topic"
def subscribe, do: Phoenix.PubSub.broadcast(:my_pubsub, @topic, ...)
def match_topic({@topic, ...}), do: ...
但这需要您重新组织代码以适应一个模块,这在某些情况下可能是一个很好的解决方案,但并不总是有效。
跨模块的解决方案可能是:
a.定义一个函数,将其作为模块属性调用
defmodule A do
def topic, do: "my_topic"
end
defmodule B do
@topic A.topic()
def match_topic({@topic, ...}). do: ...
end
b.定义一个宏(可能有点矫枉过正)
defmodule A do
defmacro topic do
# in this particular case, `quote` is overkill, "my_topic" being its own AST
quote do: "my_topic"
end
end
defmodule B do
require A # necessary to use it
def match_topic({A.topic(), ...}). do: ...
end
由于宏是在编译时调用的,因此可以在模式中使用它(与函数不同)。
c.有一些库可以实现此目的,例如 defconstant 如果您发现自己使用此模式,它可能会很有用。