clojure - clojure 将协议定义保存在与实现不同的命名空间中
问题描述
我一直在尝试建立一个规则,将我的协议定义分离到它们自己的命名空间中,主要是作为一种风格选择。我不喜欢这种方法的一件事是,由于命名空间所需的任何东西实际上都是该命名空间的“私有”,因此想要从另一个命名空间调用协议函数的用户必须在他们的协议代码中添加一个 require 语句。
例如:
协议定义命名空间:
(ns project.protocols)
(defprotocol Greet
(greet [this greeting]))
实现命名空间:
(ns project.entities
(:require [project.protocols :as protocols]))
(defrecord TheDude
[name drink]
protocols/Greet
(greet [this greeting]
(println "The Dude sips a" drink)
(println greeting)))
核心命名空间:
(ns project.core
(:require [project.protocols :as protocols]
[project.entities :refer [TheDude]]))
(let [dude (TheDude. "Jeff" "white russian")]
(protocols/greet dude "not on the rug, man..."))
这工作得很好,但我并不特别喜欢用户需要意识到 requireproject.protocols
这实际上是project.entities
. 在其他语言中,我只会在其中引用,project.entities/greet
但project.core
命名空间不会在 Clojure 中“导出”它们所需的变量,它们仅在所需命名空间的内部。我看到两个明显的替代方案,第三个可能是使用像 Potemkin 这样的东西:
- 不要将协议定义放在单独的命名空间中,只需将它们定义在与实现相同的文件中(例如
project.entities
此处)。 - 在实现文件中,创建指向每个协议函数的变量(这很丑陋,感觉不对,但有效)。
以数字 2 为例:
(ns project.entities
(:require [project.protocols :as protocols]))
(defrecord TheDude
[name drink]
protocols/Greet
(greet [this greeting]
(println "not on the rug, man...")
(println "guess i'll have another " drink)))
(def greet protocols/greet) ; ¯\_(ツ)_/¯
我的问题,我认为这主要是一个偏好,是处理这种关注点分离的“最佳实践”(如果有的话)方式是什么?我意识到添加require
inproject.core
只是多一行,但我关心的不是行数,而是最小化用户需要注意的内容。
编辑:我认为实现这一点的明显方法是不要期望用户需要两个名称空间,而是创建一个核心名称空间来为他们做到这一点:
(ns project.core
(:require [project.protocols :as protocols]
[project.entities :refer [TheDude]]))
;; create wrapper 'constructor' functions like this for each record in `project.entities`
(defn new-dude
[{:keys [name drink] :as dude}]
(map->TheDude dude))
;; similarly, wrap each protocol method
(defn greet [person phrase]
(protocols/greet person phrase))
现在任何用户都可以只需要核心,如果他们想将协议扩展到他们自己在不同命名空间中的记录,他们可以这样做,并且调用core/greet
将获取新的实现。此外,如果需要进行任何前/后处理,可以在“更高级别”的 API 函数中进行处理core/greet
。
解决方案
在需要协议的程序中,通常对象在某些地方被实例化并在其他地方(通过协议)被消费。也许在某些情况下并非如此,您实际上并不需要协议。实际上,“要求”协议及其实现的名称空间并不太常见。如果它经常发生,那就是代码异味。
pete23 的回答提到使用点语法来调用记录的方法而不涉及协议的命名空间。但是使用协议功能有一些适度的优势。
如果没有实现继承,协议仅包含基本(“原始”)功能。这样的功能实现起来很方便,但对调用者不一定超级友好。协议命名空间是添加非原始访问器的好地方,您可能以面向对象的方式声明为接口上的默认方法或抽象基类上继承的非抽象方法。使用协议命名空间的消费者可以调用原语和非原语。
有时,原语需要对所有实现都通用的预处理或后处理。无需在每个实现中重复常见的东西!只需轻轻重构:将协议函数从f
to重命名-f
,更新实现,并f
在协议的命名空间中添加一个函数,该函数包含-f
必要的 pre 和 post。调用者不需要任何更改。
推荐阅读
- excel - 如何使用 VBA 对 ActiveX 列表框执行事件处理
- blazor - EditForm 等内置组件在哪里记录?
- reactjs - material-ui CardActionArea onClick 在某些情况下什么都不做
- keras - 预期 simple_rnn_input 有 3 个维度,但得到了形状为 (32, 150, 150, 1) 的数组
- reporting-services - 在 SSRS 中,'swich' 语句是否执行所有选项,然后只返回条件为真的选项?
- windows - 是否可以撤消 Windows 命令行上的粘贴?
- javascript - Dockerode 等价于命令
- excel - 为什么这个 CountIf 函数返回“0”?
- python - lambda函数中python中两个不同事件(sns和emr)之间可能不同的函数
- r - 将具有多个标题的数据重新格式化为长格式,一个标题行成为新列中的数据