我在 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]
或永远不会有一个只接受 :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,如果某个名称在不同的上下文中意味着不同的事情,例如对于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?))
优点/缺点
优点
- 迫使您理解您领域中的不同名称及其上下文
- 在引用它的命名空间中重复使用值规范,而不是反复定义它们
- 可能避免了对同一事物的两种不同名称
- 防止您无意中将同一个值规范用于两件实际上不同的事情,并且有朝一日您会发现您需要区分它们,因此需要提取值规范并将它分成两部分。
- 允许函数自由定义它们喜欢的结构
- 跨项目重用常用类型规范和规范构造程序/实用工具
缺点
- 您可能会无意中用同一个值规范为两件实际上不同的事情,并且有朝一日您会发现您需要区分它们,因此需要提取值规范并将它分成两部分。
- 您可能会遇到重复的结构规范,但我们故意这样做,因为我们假设在某个命名空间中可能需要更多的或更少的键,或者需要在不同的集合中。