首页 > 解决方案 > 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/greetproject.core命名空间不会在 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) ; ¯\_(ツ)_/¯

我的问题,我认为这主要是一个偏好,是处理这种关注点分离的“最佳实践”(如果有的话)方式是什么?我意识到添加requireinproject.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

标签: clojurenamespacesprotocols

解决方案


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

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

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

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


推荐阅读