我和几位其他人在Slack上讨论了这个问题。一个出现(还有很多,我甚至可能会有一个用另一种方法描述的第二个答案)的想法如下
- 在共享命名空间中放值规范,例如,
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]
表示,或者永远不有一个只接受代码 without their corresponding :y
的函数,例如,那么它就符合值规范,否则就不符合。
值规范对你的应用也有意义。这意味着值规范的名字在你的应用中有意义。例如,你可以有(s/def ::first-name string?)
和(s/def ::last-name string?)
。当你看到它们时,你会发现它们都是规范:string?
,但在你的应用中,区分那些作为姓氏和作为名字的:string?
是有意义的。因此,你所有的值规范都应该是这样的有意义名称,它们是你讨论应用域时所使用的普适语言的一部分。如果你向我解释你的应用做什么,你不会说它接受字符串并将它们存储在数据库中,你会说它接受客户姓名和姓氏。
因此,由于它们是领域概念,具有全局意义,它们属于你共享的规范命名空间。
这里有一个澄清,如果你意识到你有多于一种类型的第一个名字,例如,你意识到你经常提到客户-first-name和动物-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
命名空间中,你可以放置规范创建函数/macros以及一些帮助你定义类型规范的实用工具。例如一个 (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?))
优点/缺点
优点
- 迫使用户理解领域的不同名称及其上下文
- 在重复使用它们的命名空间中,重复使用值规范,而不是重复定义它们
- 可能避免了为相同的事物给出两个不同的名称
- 防止你意外耦合到一个特定的结构实例
- 允许函数自由定义其操作的结构
- 在项目之间重复使用常见的类型规范和规范构造程序/实用工具
缺点
- 你可能会错误地使用同一个值规范来表示实际上不相同的两件事物,某天你意识到需要区分它们,所以需要提取值规范并将其分成两部分。
- 你可能会偶然使用重复的结构规范,但我们故意这样做,因为我们假设这些规范更有可能需要在一个命名空间中拥有更多或更少的键,或者需要在一个不同的集合中使用。