将操作与数据分离 - 面向数据编程 v1.1

面向数据编程 (DOP) 非常注重数据,此次讨论的原则涉及实现大多数域逻辑的方法,它建议将操作与数据分开。

1.示例场景

此次讨论面向数据编程 v1.1的将操作与数据分离原则的具体示例是以销售平台作为示例,该平台销售书籍、家具和电子设备,每个产品都由一条简单记录建模。它们都实现了密封接口Item,该接口未声明任何方法,因为这三个子类没有共享任何方法。

2.操作

在探索如何对数据进行建模时,我列出了哪些方法适合记录,哪些方法不太适合。我基本上排除了所有包含非平凡域逻辑或与不代表数据的类型交互的方法 - 我们称之为操作。操作将广泛但最终毫无生气的数据表示转变为具有活动部件的可以用于具体应用场景的系统。

在面向数据的编程中,操作不应定义在记录上,而应定义在其他类上。将商品添加到购物车既不是 Item.addToCart(Cart),也不是 Cart.add(Item),因为 Item 和 Cart 是数据,因此是不可变的。相反,订购系统 Orders 应该接管此任务,例如使用 Orders.add(Cart, Item),它返回一个反映操作结果的新 Cart 实例。

如果其他子系统需要当前购物车,它们应该引用 Orders,而不是引用变化的购物车,并在必要时通过 Orders.getCartFor(User) 查询用户的当前购物车。子系统之间的通信不是通过共享可变状态隐式实现的,而是通过请求当前状态显式实现的。状态更改仍然是可能的,但对于更改发生的位置有限制 - 理想情况下仅在负责相应子域的子系统中。

但是这些操作是如何实现的呢?乍一看,如果接口没有定义任何方法,似乎很难用 Item 做任何有用的事情。

3.模式匹配

这就是 switch 模式匹配发挥作用的地方。 switch 语句最近在很多方面得到了改进:

  • 它可以用作表达式,例如使用 var foo = switch … 为变量赋值。
  • 如果 case 标签后面跟着箭头 ->(而不是冒号 :),则不会出现 fall-through。
  • 选择器表达式(关键字 switch 后面括号中的变量或表达式;通俗地说,就是被“切换”的内容)可以是任何类型。

这里至关重要的是最后一点:如果选择器表达式不具有任何最初允许的类型(数字、字符串、枚举),则不会将其与具体值匹配,而是与模式匹配 - 因此是模式匹配。选择器表达式的值与一个又一个模式进行比较,从上到下,直到匹配为止。然后,执行标签右侧的分支。(实际实现经过优化,并且非线性工作。)

最简单的形式是,模式是类型模式,就像我们在实现 equals 时使用的类型模式一样。例如,处理一个项目如下所示:

public ShipmentInfo ship(Item item) {return switch (item) {case Book book -> // use `book`case Furniture furniture -> // use `furniture`case ElectronicItem eItem -> // use `eItem`}
}

在这里,变量 item 与左侧的类型进行比较,如果它是(例如)一件家具,则类型模式 case Furniture furniture 匹配。这会声明一个 Furniture 类型的变量 furniture,并在执行相关分支之前将 item 转换为该变量,然后就可以使用 furniture 了。在箭头的右侧,可以执行与操作(此处:运送物品)和特定数据(此处:Book、Furniture 或 ElectronicItem 的实例)匹配的逻辑。由于数据是透明建模的,因此所有信息都可供操作使用。

这最终实现了动态调度:选择应该为给定类型执行哪段代码。如果我们在接口 Item 上定义了方法 ship,然后调用 item.ship(…),则运行时将决定最终执行 Book.ship(…)、Furniture.ship(…) 和 ElectronicItem.ship(…) 中的哪一个实现。使用 switch,我们可以手动执行此操作,这样我们就不必在接口上定义方法。我们已经强调了这样做的一些原因:

  • 记录不应实现非平凡的域逻辑,而应保持简单数据。
  • 记录不应执行操作,而应由操作处理。
  • 许多操作很难在不可变记录上实现。

还有一个更为重要的原因:对中心领域概念进行建模的类型往往会吸引过多的功能,因此难以维护。DOP 通过将操作放在相应的子系统中来避免这种情况,即 Shipments.ship(Item) 而不是 Item.ship(Shipments)(其中 Shipments 是负责交付的系统)。

在 OOP 中,将操作与操作类型分开的要求也是众所周知的。前辈们甚至记录了一种设计模式(与模式匹配无关),称为访问者模式,它完全满足这一要求。在这方面,DOP 是优秀的,但由于现代语言特性,它可以使用模式匹配,这比访问者模式更简单、更直接。

4.更详细的模式

switch 中的类型模式对于面向数据编程至关重要。这可能不适用于 Java 支持(或即将支持)的其他五种模式,但它们肯定很有用,这就是我们将在这里简要讨论它们的原因。每个部分都包含对详细介绍该功能的 JDK 增强提案 (JEP) 的引用。

5.记录模式

记录模式由 JEP 440 在 Java 21 中最终确定,并允许在匹配期间直接解构记录:

switch(item) {case Book(String title, ISBN isbn, List<Author> authors) -> // use `title`, `isbn`, and `authors`// more cases...
}

您也可以使用 var,在这种情况下,括号中的代码将是 var title、var isbn、var authors,或者如果您想让您的同事真正生气,则可以混合使用 var 和显式类型。

6.未命名模式

拆分记录非常方便,但每次列出所有组件时,如果您只需要其中的一部分,那就太烦人了。这就是未命名模式的用武之地,它已由 Java 22 中的 JEP 456 标准化。它们允许用单个下划线 _ 替换不必要的模式:

switch(item) {case Book(_, ISBN isbn, _) -> // use `isbn`// more cases...
}

未命名的模式也可以在顶层使用:

switch(item) {case Book book -> // use `book`case Furniture _ -> // no additional variable in scope// more cases...
}

7.嵌套模式

自从 JEP 441 在 Java 21 中最终确定了 switch 中的模式后,您可以使用嵌套模式将模式嵌套在彼此内部。这使我们能够更深入地挖掘记录,例如使用两个嵌套的记录模式。假设 ISBN 也是一条记录,它看起来可能像这样:

switch(item) {case Book(_, ISBN(String isbn), _) -> // use `isbn`// more cases...
}

8.保护模式

如果域逻辑不仅需要通过类型区分,还需要通过值区分,那么在右侧简单地使用 if 似乎很自然:

switch(item) {case Book(String title, _, _) -> {if (title > 30)// handle long titleelse// handle regular title}// more cases...
}

保护模式也是 JEP 441 的一部分,它们允许将此类条件推到左边:

switch(item) {case Book(String title, _, _) when title > 30 -> // handle long titlecase Book(String title, _, _) -> // handle regular title// more cases...
}

这有几个优点:

  • 所有条件(即选择了哪种类型和哪个值)都显示在左侧,从而改善了代码的结构和可读性。
  • 如果不同分支需要不同的组件,则可以方便地忽略不需要的组件。
  • 受保护的模式集成到我们将在下一节讨论的完整性检查中。

9.原始模式

原始模式是由 JEP 455 作为 Java 23 中的预览功能引入的。它们允许使用模式扩展针对原始类型(即“经典”开关)的 switch 语句,这使得捕获选择器表达式的值变得更容易,并允许它在受保护的模式中使用它:

switch (Rankings.of(book).currentRank()) {case 1 -> firstPlace(book);case 2 -> secondPlace(book);case 3 -> thirdPlace(book);case int n when n <= 10 -> topTenPlace(book, n);case int n when n <= 100 -> nthPlace(book, n);case int n -> unranked(book, n);
}

10.可维护性

按类型比较的 switch 肯定会让不少 OOP 老手起鸡皮疙瘩。美化的 instanceof 检查真的应该成为整个编程范式的基础吗?

这个想法值得追求。为什么 instanceof 不受欢迎?答案由两部分组成:

  • 与接口配合使用的代码应该适用于其所有实现。
  • 添加新实现时,一系列 instanceof 检查长期难以更新,因为很难找到。

换句话说:通过 instanceof 检查进行动态调度是不可靠的。

这正是访问者模式在面向对象中广泛使用的原因:它还实现了动态分派。(如果您数不清:继接口/实现、带类型模式的 switch 和 instanceof 之后,这是实现动态分派的第四种方法。)访问者模式以一种可靠的方式实现这一点,尽管由于其间接性而有些麻烦且有时难以理解。这是因为访问接口的每个新实现都会生成一系列编译错误,只有让每个现有访问者(即每个操作)考虑新类型才能修复这些错误。

关键点来了:同样适用于带模式的 switch!

11.详尽性

这样的 switch 必须是详尽的,这意味着对于具有选择器表达式类型的每个可能实例,必须有一个与之匹配的模式,否则编译器会报告错误。有三种不同的方法可以实现这一点:

1.最后捕获所有剩余实例的默认分支:

 switch (item) {case Book book -> // ...case Furniture furniture -> // ...default -> // ...}

2.与选择器表达式具有相同类型并因此具有与默认值相同的效果的模式:

 switch (item) {case Book book -> // ...case Furniture furniture -> // ...case Item i -> // ...}

3.列出密封类型的所有实现:

 switch (item) {case Book book -> // ...case Furniture furniture -> // ...case ElectronicItem eItem -> // ...}

不幸的是,前两种变体无法帮助我们实现目标。这种切换在添加新实现时仍然是详尽的,因此不会产生编译错误。因此,如果将海报添加到网上商店,(1.它们将默默地以默认方式结束)(2.以案例项目结束)。然而,在第三种变体中,没有海报分支,因此我们会收到编译错误,这迫使我们更新操作。太好了。

为了使操作可维护(意味着如果它们没有明确涵盖所有情况,则会导致编译错误),不能有默认或全部分支,这仅在以下情况下才有可能:

  • 切换到密封接口(或密封抽象类,但我们忽略它们)
  • 列出所有实现

最后一点也解释了为什么密封接口比密封类效果更好(还记得两篇文章中提到的要点吗?)。如果 Item 是非抽象类,则包含 Book、Furniture 和 ElectronicItem 分支的 switch 不会详尽无遗,因为 Item 本身可能有实例,但没有针对它们的分支。但是,如果使用 case Item 来处理它,则此分支还会处理每个新项目,例如海报,并且不会出现编译错误。

switch(item) {case Book(String title, _, _) -> {if (title > 30)// handle long title}// more cases for other types...
}

在此示例中,标题较短的书籍将被忽略,这可能是一个疏忽,在较长的代码中可能并不明显。使用受保护的模式不会发生这种情况:

switch(item) {case Book(String title, _, _) when title > 30 -> // handle long titlecase Book _ -> { /* ignore short titles */ }// more cases...
}

这里,在案例 Book … when … 之后,必须有一个针对所有书籍的分支,然后该分支要么修复遗忘短标题书籍的错误,要么(如图所示)明确表明它们被故意忽略。

12.避免默认分支

最后,关于默认分支以及如何避免它们,需要说明一下。有时,switch 实际上只想处理某些情况,而忽略其他情况,或者以其他方式集体处理它们 - 默认分支似乎是显而易见的解决方案:

switch(item) {case Book book -> createTableOfContents(book);default -> { }
}

然而,正如讨论的那样,这种情况应该不惜一切代价避免,而 Magazine 工具 Item 的添加(它们不是书籍,但仍需要目录)再次凸显了这个问题。相反,可以将几个具有未命名模式的案例标签组合成一个:

switch(item) {case Book book -> createTableOfContents(book);case Furniture _, ElectronicItem _ -> { }
}

这比默认的代码 -> 多一点,但在添加杂志时会产生所需的编译错误,因此应该是首选。

如果您暂时坚持使用 Java 21,则只能使用未命名模式作为预览功能。由于它在 Java 22 中未经更改就已完成,因此这是可以想象的。但请注意,当使用 --enable-preview 激活预览功能时,所有功能都可用,并且您必须小心不要使用其他更不稳定的预览功能(例如字符串模板)。

13.总结

为了使数据建模记录不受非平凡域逻辑的影响并防止 API 臃肿,不应在它们上面实现操作,而应在专用子系统中实现。然后,操作通常会处理密封接口,这些接口通常提供很少的方法来与其交互。相反,它们将切换这些接口并枚举所有实现,从而实现自己的动态调度。只要避免使用默认和捕获所有分支,这就可以保证未来安全,因为新的接口实现将使这些切换变得不详尽。这会导致编译错误,直接导致开发人员需要为新类型更新操作。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/web/25415.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

欢乐打地鼠小游戏html源码

这是一款简单的js欢乐打地鼠游戏&#xff0c;挺好玩的&#xff0c;老鼠出来用鼠标点击锤它&#xff0c;击中老鼠获得一积分。 欢乐打地鼠小游戏html源码

kopf,一个实用的 Python 库!

更多Python学习内容&#xff1a;ipengtao.com 大家好&#xff0c;今天为大家分享一个实用的 Python 库 - kopf。 Github地址&#xff1a;https://github.com/nolar/kopf 在 Kubernetes 中&#xff0c;Operator 是一种用于扩展 Kubernetes 功能的强大工具。Operator 可以自动化应…

MySQL之查询性能优化(十三)

查询性能优化 优化LIMIT分页 在系统中需要进行分页操作的时候&#xff0c;我们通常会使用LIMIT加上偏移量的办法实现&#xff0c;同时加上合适的ORDER BY子句。如果有对应的索引&#xff0c;通常效率会不错&#xff0c;否则&#xff0c;MySQL需要做大量的文件排序操作。一个非…

【Python】成功解决TypeError: ‘int’ object is not iterable

【Python】成功解决TypeError: ‘int’ object is not iterable &#x1f308; 欢迎莅临我的个人主页&#x1f448;这里是我深耕Python编程、机器学习和自然语言处理&#xff08;NLP&#xff09;领域&#xff0c;并乐于分享知识与经验的小天地&#xff01;&#x1f387; &#…

MySQL的group by与count(), *字段使用问题

文章目录 问题group by到底做了什么举个例子简单来说为什么select字段&#xff0c;count()不能和*共同使用总结 问题 这是一段摘抄自MySQL官网的文字。其大致意思是MySQL拓展了group by的使用&#xff0c;MySQL允许选择没有出现在group by中的字段。换句话说&#xff0c;标准SQ…

【Python】成功解决ZeroDivisionError: division by zero

【Python】成功解决ZeroDivisionError: division by zero &#x1f308; 欢迎莅临我的个人主页&#x1f448;这里是我深耕Python编程、机器学习和自然语言处理&#xff08;NLP&#xff09;领域&#xff0c;并乐于分享知识与经验的小天地&#xff01;&#x1f387; &#x1f393…

【QT5.14.2】编译MQTT库example的时候报No such file or directory

【QT5.14.2】编译MQTT库example的时候报No such file or directory 前几天导师让跑一下MQTT库&#xff0c;用的5.14.2版本的QT&#xff0c;于是就上网搜了一个教程&#xff1a;https://www.bilibili.com/video/BV1dH4y1e7hG/?spm_id_from333.337.search-card.all.click&v…

Fedora的远程桌面

要在 Fedora 40 上开启远程桌面功能。 首先&#xff0c;要确保已安装 gnome-remote-desktop 和 vino 包。 这些软件包通常默认安装在 Fedora 的 GNOME 桌面环境中。 可以按照以下步骤操作&#xff1a; 1、判断电脑是否安装了 gnome-remote-desktop 和 vino 包: tomfedora:…

第十三周 5.28 三个修饰符知识点

一、abstract[抽象的] 1.abstract可以修饰类: (1&#xff09;被abstract修饰的类称为抽象类 (2) 语法:abstract class 类名{} (3) 特点:抽象类只能声明引用&#xff0c;不能创建对象 (4) 抽象类中可以定义属性和成员方法、构造方法 2.abstr…

SpringSecurity提供了哪些核心功能?

Spring Security 是一个强大且高度可定制的身份验证和访问控制框架&#xff0c;它是为保护基于Spring的应用程序而设计的。Spring Security 提供了下列核心功能&#xff1a; 1. 全面的身份验证支持 Spring Security 支持广泛的身份验证机制&#xff0c;包括表单基础认证、HTT…

【Linux】匿名管道的应用场景 --- 进程池

&#x1f466;个人主页&#xff1a;Weraphael ✍&#x1f3fb;作者简介&#xff1a;目前正在学习c和算法 ✈️专栏&#xff1a;Linux &#x1f40b; 希望大家多多支持&#xff0c;咱一起进步&#xff01;&#x1f601; 如果文章有啥瑕疵&#xff0c;希望大佬指点一二 如果文章对…

Tomcat中轻松部署Java Web项目

Tomcat 是一个广泛使用的 Java Servlet 容器和 Web 服务器&#xff0c;它允许你部署 Java Web 应用程序。以下是使用 Tomcat 部署 Java 项目的基本步骤&#xff1a; 1. 准备 Java 项目 确保你的 Java 项目是一个 Web 应用程序&#xff0c;即它包含了一个 WEB-INF 目录&#x…

Qt qtpropertybrowser使用实例(1)

属性界面实例&#xff1a; 代码如下&#xff1a; #include <QDate> #include <QLocale> #include "qtpropertymanager.h" #include "qtvariantproperty.h" #include "qttreepropertybrowser.h" int main(int argc, char *argv[]) {…

nginx mirror流量镜像详细介绍以及实战示例

nginx mirror流量镜像详细介绍以及实战示例 1.nginx mirror作用2.nginx安装3.修改配置3.1.nginx.conf3.2.conf.d目录下添加default.conf配置文件3.3.nginx配置注意事项3.3.nginx重启 4.测试 1.nginx mirror作用 为了便于排查问题&#xff0c;可能希望线上的请求能够同步到测试…

TalkingData 是一家专注于提供数据统计和分析解决方案的独立第三方数据智能服务平台

TalkingData 是一家专注于提供数据统计和分析解决方案的独立第三方数据智能服务平台。通过搜索结果&#xff0c;我们可以了解到 TalkingData 的一些关键特性和市场情况&#xff0c;并将其与同类型产品进行比较。 TalkingData 产品特性 数据统计与分析&#xff1a;提供专业的数…

OSX-KVM - 在 QEMU/KVM上运行macOS

文章目录 依赖安装准备安装Headless macOSSetting Expectations Right安装后这合法吗&#xff1f;动机回馈贡献 OSX-KVM 支持早 QEMU/KVM上运行macOS。现在支持OpenCoreMontereyVenturaSonoma&#xff01; 现在仅提供商业&#xff08;付费&#xff09;支持&#xff0c;以避免垃…

【每日算法】

算法第15天| (二叉树part02)层序遍历、226.翻转二叉树(优先掌握递归)、101. 对称二叉树(优先掌握递归) 文章目录 算法第15天| (二叉树part02)层序遍历、226.翻转二叉树(优先掌握递归)、101. 对称二叉树(优先掌握递归)一、层序遍历二、226. 翻转二叉树(优先掌握递归)三、101. 对…

Elasticsearch index 设置 false,为什么还可以被检索到?

在 Elasticsearch 中&#xff0c;mapping 定义了索引中的字段类型及其处理方式。 近期有球友提问&#xff0c;为什么设置了 index: false 的字段仍能被检索。 本文将详细探讨这个问题&#xff0c;并引入列式存储的概念&#xff0c;帮助大家更好地理解 Elasticsearch 的存储和查…

在Tomcat 10.1.x上使用jstl

通过在Web应用程序项目的/WEB-INF/lib文件夹中放入以下两个Jar包 jakarta.servlet.jsp.jstl-3.0.1.jarjakarta.servlet.jsp.jstl-api-3.0.0.jar 在 jsp 页面导入 taglib 标签 <% taglib prefix"c" uri"jakarta.tags.core" %>

区分live(居住v)、live(直播的adj、直播地adv)、life/lives(生活n及其复数)的读音

文章目录 区分live&#xff08;居住v&#xff09;、live&#xff08;直播的adj、直播地adv&#xff09;、life/lives&#xff08;生活n及其复数&#xff09;的读音 区分live&#xff08;居住v&#xff09;、live&#xff08;直播的adj、直播地adv&#xff09;、life/lives&…