我与一些人讨论了这个话题。一个想法(还有很多其他想法,我甚至可能会写第二个回答描述另一种方法)如下:
- 在共享命名空间中将值规格放在一起,例如,
specs.clj
- 将结构规格放在依赖于该结构的命名空间中,例如,在代码中,如
ships.clj
和stars.clj
内部或它们相应的规格命名空间中,如ships_specs.clj
和stars_specs.clj
- 在共享命名空间中将类型规格放在一起,也许甚至在一个共享库中,因为您可能希望在使用它时甚至能够跨越无关的项目,如
type_specs.clj
或common_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?
对那些是名字的 string?
有意义。因此,你的所有值规范都应该有类似的有意义的名称,这些名称是你在讨论应用程序领域时使用的通用语言的一部分。如果你向我解释你的应用程序做什么,你不会说要取字符串并将其存储在数据库中,你会说它处理客户的姓氏和名字。
因此,由于它们是领域概念,具有广泛的领域意义,它们进入您的共享规范命名空间。
这里有一个澄清,如果你意识到你有不止一种类型的first-name,例如,你意识到你一直在谈论customer-first-name和animal-first-name。这意味着你需要有两个值规范,一个对应每个。基本上,如果你的应用程序的上下文中有子上下文,在其中使用相同的名称,但具有不同的隐含含义,你需要使其明确。
什么是结构规范?
结构规范是将值规范临时排列成某个集合或结构的一种安排。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 中,如果某个名称在不同的上下文(例如 state
和 velocity
)中有不同的含义,您会将上下文显式地体现在其名称中。
- 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?))
优点/缺点
优点
- 迫使您理解领域中的不同名称及其上下文
- 跨命名空间重用值规格,而不是反复定义它们
- 可能避免了同一事物的两个名称
- 防止您无意中将其耦合到特定结构的实例
- 允许函数自由定义它们要操作的结构的偏好
- 跨项目重用通用的类型规格和规格构建器/实用程序
缺点
- 您可能无意中将相同的值规格用于两件实际上不同的事情,有一天您发现您需要区分它们,因此您需要提取值规格并将其拆分为两部分。
- 您可能最终会拥有重复的结构规格,但我们有意为之,因为我们假设这些规定可能需要在一个命名空间中需要更多或更少的键,或者需要以不同的集合形式。