clojure 将协议定义保存在与实现不同的命名空间中

clojure keeping protocol definition in a separate namespace from implementation

我一直在尝试建立一个原则,将我的协议定义分离到它们自己的命名空间中,主要是作为一种风格选择。我不喜欢这种方法的一件事是,由于命名空间需要的任何东西实际上都是该命名空间的“私有”,因此想要从另一个命名空间调用协议函数的用户必须在他们的协议代码中添加一个 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..."))

这工作得很好,但我不是特别喜欢用户需要意识到需要 project.protocols 的需要,这实际上是 project.entities 内部的一个实现细节。在其他语言中,我只会在 project.core 中引用 project.entities/greet,但命名空间不会在 Clojure 中“导出”它们所需的变量,它们仅在需要的命名空间内部。我看到两个明显的替代方案,第三个可能是使用类似 Potemkin 的东西:

  1. 不要将协议定义放在单独的命名空间中,只需将它们定义在与实现相同的文件中(例如 project.entities 此处)。
  2. 在实现文件中,创建指向每个协议函数的变量(这非常丑陋,感觉不对,但有效)。

以数字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) ; ¯\_(ツ)_/¯

我想我的问题主要是偏好之一,即处理这种关注点分离的“最佳实践”(如果有的话)方法是什么?我意识到在 project.core 中添加 require 只是多了一行,但我关心的不是行数,而是更多地减少用户需要注意的内容。


编辑: 我认为实现这一点的明显方法是不要期望用户需要两个名称空间,而是创建一个核心名称空间来为他们做到这一点:

(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 将获得新的实现。此外,如果有任何 pre/post 处理要完成,可以在“更高级别” API 函数 core/greet.

中处理

在需要协议的程序中,通常对象在某些地方被实例化,而在其他地方(通过协议)被消费。也许在某些情况下并非如此,您实际上并不需要协议。实际上,“要求”协议及其实现的名称空间并不太常见。如果经常出现,那就是code smell。

pete23 的回答提到使用点语法调用记录的方法而不涉及协议的命名空间。但是使用协议功能有一些适度的优势。

缺少实现继承,协议仅包含基本(“原始”)功能。这样的功能实现起来很方便,但不一定对调用者超级友好。协议命名空间是添加非原始访问器的好地方,您可能以面向对象的方式声明为接口上的默认方法或抽象基类上的继承非抽象方法 class。使用协议命名空间的消费者可以调用原语和非原语。

有时原语需要预处理或 post 所有实现通用的处理。无需在每个实现中重复常见的东西!只需轻轻重构:将协议函数从 f 重命名为 -f,更新实现,并在协议的命名空间中添加一个函数 f,用必要的 pre 和post。调用者不需要任何更改。