2024 Clojure 状况调查中分享您的想法!

欢迎!请参阅关于页面以获取更多关于如何使用此功能的信息。

+3
规格

我对 Clojure/Spec 相对较新,阅读了文档后,它对我来说并不明显是最佳实践:在验证模块内(重新)声明规格,还是声明通用规格以便在多个模块中重复使用。

例如,一个游戏可能有多个不同的对象,它们都需要包含 x、y 坐标的坐标位置。例如,在这个存储库中,::x 和 ::y 规格定义被重新声明在“实体”模块中(云,爆炸等)。这是否比有一个针对位置的单一规格并在多个模块中导入它更受推崇?

2个答案

+8

精选
 
最佳答案

我与其他人讨论了这个问题。其中一个出现的想法(还有许多,我甚至可能还会提供一个关于另一种方法的第二答案)是以下内容

  • 将值规格放入共享命名空间中,例如,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? 的规范,但在您的应用程序中,区分那些作为姓名和姓氏的 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?))

有时在 type_specs 命名空间中,您不仅可以将规范放入其中,还可以放入规范创建函数/宏,以及帮助您定义类型规范的工具。例如一个 (string-of-length len),它会返回一个验证字符串是否具有特定长度的规范。

这就是类型规范及其命名空间的目的。因为它们基本上是领域无关的,您可以将它们放在一个库中,以便在多个项目中重用。

它们看起来是怎样的?

好吧,我不会涵盖整个 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 投票

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

如果它们意在表示跨多个命名空间中的相同实体,它们应放置在特定命名空间中,该命名空间需要从其他命名空间中发布。

至于放置,这取决于场景。很多人会将与数据相关的规范放在自己的命名空间中,按领域关注点分组,但将功能相关的规范放在与之“相关”的功能的同一命名空间中。我们工作就是这样做的。

在我关于我们在工作场合使用Spec的博客上做了介绍,这绝对不是最终决定,但似乎是相当普遍的做法。

这篇博客文章对我的理解很有帮助,了解了经验丰富的团队实际使用规范的目的,谢谢您提供的链接。
...