请在2024 年 Clojure 状态调查!中分享您的想法。

欢迎!请参阅关于页面,了解更多关于此网站如何运作的信息。

+3
规范

我是 Clojure/Spec 的新手,阅读完文档后,我不太清楚在与验证的模块中(重新)声明规范是否是最佳实践,还是最好分别声明用于跨多个模块重用的公共规范。

例如,一个游戏可能有许多不同的对象,它们都需要一个由 x 和 y 坐标组成的位置。比如在这个 仓库 中,::x 和 ::y 规范定义在每一个 "entity" 模块(云、爆炸等)中重新声明。这是否比有一个用于位置的单一规范并在模块间导入更好?

2 个答案

+8

选中
 
最佳答案

我在 slack 上与几个人就这个问题进行了讨论。出现的一个想法(还有许多,我甚至可能还会有一个第二答案描述另一种方法)是以下内容:

  • 将值规范一起放在共享命名空间中,例如 specs.clj
  • 将结构规范放入依赖该结构的命名空间中,即在命名空间内部,例如在 ships.cljstars.clj 内部,或者在其相应的规范命名空间中,如 ships_specs.cljstars_specs.clj
  • 将类型规范放在一起存储在一个共享的命名空间中,甚至可能是一个共享库,因为你可能想要在甚至非相关项目中使用这些规范,例如 type_specs.cljcommon_specs.clj

这种方法中棘手的部分是确定哪些规范属于哪种类别。我在这方面还有一些困扰,但以下解释很有帮助。

什么是值规范?

值规范是对不可分割的数据的规范。

任何不是集合的东西都会是值规范,比如字符串、数字、关键字等。

有时,它也可能是一个集合,但在你的应用程序的上下文中,你永远不会将这个集合拆分。这有点模糊,我建议可能开始时不将任何集合作为值规范,但例如,一个 point 就是: {:x 0-255 :y 0-255}。如果你永远不会以任何不同的方式表示一个点,比如永远不会: [:x 0-255 :y 0-255] 或永远不会有一个只接受 :x 坐标而不对应 :y 的函数,如示例,那么它就是值规范,否则不是。

值规范对你的应用程序也很有意义。这意味着值规范的名字在你的应用程序中有意义。例如,你可以有 (s/def ::first-name string?)(s/def ::last-name string?)。当你看到这些时,你会发现它们都是相同的类型规范 string?,但在你应用程序中,区分第一个名字和姓氏的意义是有的。因此,所有你的值规范都应该有像这样有意义的名字,它们是你讨论应用程序域时使用的无处不在的语言的一部分。如果你告诉我你的应用程序做什么,你不会说你只是在数据库中存储字符串,你会说它存储客户的首名和姓。

因此,由于它们是域概念,具有全局含义,它们应该存储在共享规范命名空间中。

在这里有一点的说明,如果你发现你有一种以上的首名字符,例如,你意识到你一直在谈论客户首名字符和动物首名字符。这意味着你需要有两个值规范,一个是每个的。基本上,如果你的应用程序上下文中有子上下文,其中相同名称使用不同的隐含含义,你需要明确这一点。

什么是结构规范?

结构规范是将值规范临时安排到某个集合或结构中的方法。Rich Hickey称之为“信息一起旅行”。有时,你有一个需要三个输入的函数,并决定一起在映射中获取这些输入,这是一个结构规范,它与该函数及其在执行过程中需要的数据排列方式相关,因此它与其命名空间一起存在。

很多其他事情是结构规范,而不仅仅是最初想到的。例如,你可能会认为用户是值规范。但深入研究,你总是会以完全相同的信息和完全相同的结构一起使用用户吗?你很快就会意识到,有时用户不存在出生日期,或没有相关的购物清单等。有时,用户有一个密码散列,而其他时候则没有。

因此,我建议你首先尝试使用这个规则,即所有集合都是在其各自的命名空间(或伴随的规范命名空间)中定义的结构规范,并使用它。只有非集合值规范。这实际上训练你的这个规范模型,一旦你很好地掌握了它,那些好的值规范候选集合将更容易被你识别。

因此,像s/keys、s/map-of、s/tuple、s/coll-of等等很可能是结构规范。

通常情况下,您不需要规格化您应用程序的所有结构。从边界处的输入和输出开始。例如,用户提供的数据、您保存到文件或数据库中的数据、发送和返回到您的API中的数据等。这些地方的数据验证最为关键。如果您想要提高代码的安全性或编写文档,您可以将其扩展到更多内部事物。

类型规范是什么?

最后,类型规范是指名称对您的应用程序没有意义的规范。记得我提到 过,值规范必须有有意义的名称,当一个值规范没有有意义的名称时,它就是一个类型规范。例如,我们有过 ::first-name::last-name 值规范,并且它们都是 string?。假设你想要一个非空字符串,并且想要 ::first-name::last-name 都是那种类型?您可以创建一个类型规范:

(s/def ::non-blank-string (complement str/blank?))

这就是类型规范及其命名空间的作用点。因为这些基本上是领域无关的,所以您可以将它们放入库中,跨项目重用。

所有这些放在一起会是什么样子?

好吧,我不会介绍整个spacewars代码库,但例如,使用这种规格建模风格,您可能会有:

  • value_specs.clj
  • (s/def ::x number?)
  • (s/def ::y number?)
  • (s/def ::velocity-1d number?)
  • (s/def ::velocity-2d ::type/num2tuple)
  • (s/def ::age number?)
  • (s/def ::direction number?)
  • (s/def ::romulan-state #{:invisible :appearing :visible :firing :fading :disappeared})
  • (s/def ::battle-state #{:no-battle :flank-right :flank-left :retreating :advancing})
  • (s/def ::battle-state-age number?)

基本上您会将所有值规范放入value_specs,如果某个名称在不同的上下文中意味着不同的事情,例如对于statevelocity,您会明确在名称中表达上下文。

  • klingons.clj
  • (s/def ::klingon (s/keys :req-un [::value/x ::value/y ::value/velocity2d ::value/battle-state-age ::value/battle-state ...] ...)

等等

  • type_specs.clj
  • (s/def ::num2tuple (s/tuple number? number?))

优点/缺点

优点

  • 迫使您理解您领域中的不同名称及其上下文
  • 在引用它的命名空间中重复使用值规范,而不是反复定义它们
  • 可能避免了对同一事物的两种不同名称
  • 防止您无意中将同一个值规范用于两件实际上不同的事情,并且有朝一日您会发现您需要区分它们,因此需要提取值规范并将它分成两部分。
  • 允许函数自由定义它们喜欢的结构
  • 跨项目重用常用类型规范和规范构造程序/实用工具

缺点

  • 您可能会无意中用同一个值规范为两件实际上不同的事情,并且有朝一日您会发现您需要区分它们,因此需要提取值规范并将它分成两部分。
  • 您可能会遇到重复的结构规范,但我们故意这样做,因为我们假设在某个命名空间中可能需要更多的或更少的键,或者需要在不同的集合中。
这是一个非常好的回答,信息量非常丰富。我也很愿意看到你开头提到的另一种方法。
信息/指南非常好!只是有一点要注意,你的第二个“缺点”:规范2将有可能通过将可能的键集(模式)与所需性规范(选择)分开来解决这个问题,所以我预计模式规范将是可以共享的,但基于这些模式的规范将是特定于使用名称空间(甚至使用的功能)。
+2

请记住,在各个名称空间中,::x::y 是不同的规范,因为 :: 解析为当前名称空间,所以实际上它们是 :a.b/x:c.d/x(如果它们位于这些名称空间中)。

如果它们意在代表多个名称空间中的同一实体,则应将它们放置在由其他名称空间required(所需)的特定名称空间中。

至于放置,这取决于情况。许多人将数据相关的规范放在自己的命名空间中,按领域关注分组,而将功能相关的规范放在与它们“所属”的功能相同的命名空间中。我们工作中的做法主要是这样。

我在这里探讨了我们在工作中对Spec的使用,这绝不是最终的,但似乎是一种相当常见的做法。

这篇博客文章对我的理解有很大帮助,使我清楚了解在经验丰富的团队中该规范的实际用途,感谢你的链接。
...