本文主要是介绍模型数据、所有数据以及仅数据 - 面向数据编程 v1.1,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
面向数据编程 (DOP) 以尽可能紧密地对数据进行建模为中心,这一点并不令人意外,因此 DOP 的核心原则是对数据进行建模,即对整个数据进行建模,而不是只对数据进行建模。通过混合使用记录(records)和密封类型(sealed types)以及一些对面向对象开发人员来说可能很奇怪的编程实践,但可以最好地实现这一目标。
1.密封类型(Sealed Types)
举例来说,一旦我们创建了书籍、家具和电子物品记录,中心域数据就建模了。但还不完全,因为它们之间存在一种尚未捕获的关系:我们商店中的每件商品要么是一本书,要么是(一件)家具,要么是电子物品。为了表示这种关系,我们使用密封类型。
Java 17 中已最终确定了密封类型。类或接口通过关键字 sealed 标记为密封,然后只有 permits 子句中列出的类型可以从中继承 - 其他类型被禁止这样做,否则将导致编译错误。此机制非常适合建模替代方案。将这句“一个项目是一本书、一件家具或一个电子设备”变为:
sealed interface Item permits Book, Furniture, ElectronicItem {// ...
}
而在实际应用场景中,当系统不能在添加新实现后简单地工作时,密封类型特别有用。另一个 List 实现?没问题,它将无缝运行。另一个 Item 实现?在一些现实生活中可能会涉及到的场景,例如现在必须检查增值税率,必须调整专用视图(例如公寓规划器或目录显示),并且可能必须引入新的交付方法。
还有许多其他情况,仅仅添加接口实现是行不通的。例如,身份验证提供程序或支付方式:仅仅编写 CreditCardPayment 实现支付是不够的,因为至少还必须实现相关的支付系统,并且可能还需要一个机制,在代码中的正确位置收集付款并将其传送到合适的支付系统。我们将在即将发表的有关操作的文章中看到它如何与密封类型优雅地协同工作。
首先,密封类型的几个属性:
- 允许的子类型必须与密封类型位于同一模块中,或者(如果代码未编译为模块)位于同一包中。
- 如果密封类型和允许的子类型包含在同一源代码文件中,则可以省略 permits 子句。
- 允许的子类型必须直接从密封类型继承。
- 允许的子类型必须是 final、sealed 或显式非密封(Java 的第一个带连字符的关键字!)。
虽然密封类当然是可行的,有时也很有用,但处理密封接口在一个非常具体的方面要轻松得多,我们将在讨论操作时讨论这一点。这就是为什么我通常建议关注密封接口。
2.除了数据之外什么都不建模
记录使聚合数据变得容易,而密封类型使表达替代方案变得容易。结合起来,这两种机制非常强大,甚至可以很好地建模复杂的结构。
3.定制的聚合和替代方案
记录的简单定义让我们可以创建定制的、可能多种类型。具体比如用户不必获取街道、邮政编码、城市和国家/地区的组件,这些组件可能最好存储在地址记录中,然后用户就可以拥有该记录的一个实例。
如果地址是可选的,并且用户还可以选择存储电子邮件地址和电话号码,那么您可以为类型提供一个 List contacts 字段,其中密封接口 ContactInfo 允许 Address、Email、Phone。是否至少需要一个联系信息?有一个 ContactInfo primaryContact 字段并将列表重命名为 additionalContacts。
目标是使用这些功能来定制适合实际域数据的类型。这使得开发人员更容易理解代码,因为它与他们需要知道的数据非常相似,并且也使代码更易于维护,因为非法数据更容易被拒绝 - 当我们研究如何仅表示合法状态时会对此进行更多介绍。
4.和类型模式平等
数据建模的核心部分是相等性的定义。如记录文章中所述,它们带有使用所有组件的 equals(和 hashCode)实现。这在许多情况下都很好,但特别是在处理用户和项目的系统中,ID 无处不在,大多数具有 ID 的对象可能都应该使用它来确定相等性。这是通常重写 equals(和 hashCode)的众多原因之一。
在上述的例子中,根据 ISBN 定义 Book 的相等性是有意义的。我们可以借助一个稍后会变得更加重要的功能非常优雅地做到这一点:类型模式,在 Java 16 中标准化,在本例中为 instanceof。
record Book(String title, ISBN isbn, List<Author> authors) {@Overridepublic boolean equals (Object other) {return this == other|| other instanceof Book book&& Objects.equals(isbn, book.isbn);}@Overridepublic int hashCode() {return Objects.hash(isbn);}}
类型模式位于 Book book 的其他实例中。它完成三个任务:
- 检查 other 是否是 Book 类型的实例
- 定义一个新变量 Book book,该变量在测试返回 true 时可见(“在范围内”)
- 分配 book = (Book) other
由于 book 变量在类型检查为正的地方可见,因此您可以在 && 之后直接使用它来比较所需的字段。
注意:用 instanceof 实现 equals 并不总是正确的,但这里没有问题,因为 Book 是常量。
5.方法
您可以在记录上实现任意方法,但作为数据的透明载体,它们更喜欢某些方法而不是其他方法:
- 没有参数的方法是最好的,因为它们除了返回记录的数据之外什么都做不了(除非它们引用全局变量,但这很少是一个好主意)。例如,email.tld() 可以识别并返回电子邮件地址的顶级域,或者 book.byline() 可以将书名和作者组合成一个字符串。
- 接受类型本身作为唯一参数的方法也是受欢迎的。例如,如果您实现 Comparable,这可能是 compareTo,或者 Book 可以有一个方法 commonAuthors(Book),它返回参与这两本书的作者列表。
- 接受其他记录(最好是那些已经用作组件类型的记录)的方法通常也可以:因为它们也应该是不可变的数据载体,所以可以假设没有状态被改变,所有结果都通过返回值传达。然而,在这种情况下,避免实现非平凡的域逻辑变得很重要。根据将操作与数据分开的原则,此类操作应保留给外部系统。
- 具有任意参数的方法,特别是可变参数的方法,很有可能将正在作为操作的一部分处理的数据记录转变为这些操作的执行器,这通常应该避免。
请注意,这些并不是硬性规定,而是指导方针,如果情况需要,可以暂停执行,但你应该有这样做的充分理由。
6.接口合约
如果在面向数据的编程中,记录主要只是提供对数据的访问,而几乎没有其他操作,那么您可能会问自己如何在这样的设计中使用接口 - 毕竟,我们主要使用它们来为行为建模契约。事实上,这个角色在这里并不那么重要。记录实现的(密封)接口主要不是定义类型的功能,而是定义类型是什么:
- 书籍、电子设备和家具属于物品。
- 地址、电子邮件和电话号码属于联系信息。
从这些示例中可以看出,接口下统一的类型通常很少重叠。虽然项目可能至少都有项目编号,但不同的联系信息却完全不同。因此,像 ContactInformation 这样的接口可能最终没有单一方法。这是不寻常的,而且“看起来不对劲”,但这只是一个熟悉的问题。这里定义的契约不是描述行为(这不是数据有意义的类别),而是描述分组(在接口上下文中哪些数据是彼此的替代品),并且不需要任何方法。
6.总结
使用记录将数据聚合为有意义的定制类型,并使用密封接口来表达这些类型之间的替代方案。由于数据不附带行为,因此此类记录通常声明很少或根本不声明方法,而这些方法不仅仅是以不同的形式返回数据。因此,它们实现的密封接口可能声明很少或根本不声明方法,这可能是新颖的,但这是意料之中的,因为它们描述的契约是关于数据是什么(而不是它做什么)。
这篇关于模型数据、所有数据以及仅数据 - 面向数据编程 v1.1的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!