我和 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?
规范,但在您的应用程序中,区分这些}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?))
优缺点
优点
- 迫使您理解您领域中的不同名称及其上下文
- 跨使用值规范的命名空间重复使用值规范,而不是反复定义它们
- 可能避免了为同一事物使用两个名称
- 防止您意外地将自己耦合到特定结构的实例
- 允许函数自由定义它们喜欢操作的架构
- 跨项目重复使用公共类型规范和规范构造器/实用程序
缺点
- 您可能意外地使用了同一值规范用于两个本质上不同的事物,有一天您意识到需要区分它们,因此需要提取值规范并将其拆分为两个。
- 您可能会得到重复的结构规范,但这是故意的,因为我们假设它们更有可能需要一个或多个键在某个命名空间中更多或更少或需要它放在不同的集合中。