这是称为“ Functional Java by Example”的系列文章的第4部分。
在上一部分中,我们讨论了一些副作用,并且我想进一步详细说明如何通过将不可变性引入代码中来防止以意外的方式操纵数据。
如果您是第一次来,最好是从头开始阅读。
它有助于了解我们从何处开始以及如何在整个系列中继续前进。
这些都是这些部分:
- 第1部分–从命令式到声明式
- 第2部分–讲故事
- 第3部分–不要使用异常来控制流程
- 第4部分–首选不变性
- 第5部分–将I / O移到外部
- 第6部分–用作参数
- 第7部分–将失败也视为数据
- 第8部分–更多纯函数
我将在每篇文章发表时更新链接。 如果您通过内容联合组织来阅读本文,请查看我博客上的原始文章。
每次代码也被推送到这个GitHub项目 。
纯功能
关于我们之前讨论的内容的小结。
- 函数式编程鼓励使用无副作用的方法(或:函数),以使代码更易于理解且更易于推理 。 如果某个方法仅接受某些输入并每次都返回相同的输出(这使其成为一个纯函数),则各种优化都可以在后台进行,例如通过编译器或缓存,并行化等。
- 我们可以再次用纯函数(计算出的值)替换纯函数,这称为参考透明度 。
这是上一部分重构后当前的内容:
class FeedHandler {Webservice webserviceDocumentDb documentDbvoid handle(List<Doc> changes) {changes.findAll { doc -> isImportant(doc) }.each { doc ->createResource(doc).thenAccept { resource ->updateToProcessed(doc, resource)}.exceptionally { e ->updateToFailed(doc, e)}}}private CompletableFuture<Resource> createResource(doc) {webservice.create(doc)}private boolean isImportant(doc) {doc.type == 'important'}private void updateToProcessed(doc, resource) {doc.apiId = resource.iddoc.status = 'processed'documentDb.update(doc)}private void updateToFailed(doc, e) {doc.status = 'failed'doc.error = e.messagedocumentDb.update(doc)}}
我们的updateToProcessed
和updateToFailed
是“不纯的”-它们都将更新现有文档in 。 从Java的返回类型void
可以看出,这意味着:什么都不会出来 。 下沉Kong。
private void updateToProcessed(doc, resource) {doc.apiId = resource.iddoc.status = 'processed'documentDb.update(doc)
}private void updateToFailed(doc, e) {doc.status = 'failed'doc.error = e.messagedocumentDb.update(doc)
}
这些类型的方法都围绕您的典型代码库。 因此,随着代码库的增长,在将数据传递给这些方法之一之后,往往很难对数据的状态进行推理。
请考虑以下情形:
def newDocs = [new Doc(title: 'Groovy', status: 'new'),new Doc(title: 'Ruby', status: 'new')
]feedHandler.handle(newDocs)println "My new docs: " + newDocs
// My new docs:
// [Doc(title: Groovy, status: processed),
// Doc(title: Ruby, status: processed)]
// WHAT? My new documents aren't that 'new' anymore
罪魁祸首一直在破坏我文件的地位; 首先,它们是“新的”,其次不是。 那不行! 一定是该死的FeedHandler。 谁创作的东西? 为什么会触碰我的数据?
考虑另一种情况,即有多个参与者处理您的业务。
def favoriteDocs = [new Doc(title: 'Haskell'),new Doc(title: 'OCaml'),new Doc(title: 'Scala')
]archiver.backup(favoriteDocs)feedHandler.handle(favoriteDocs)mangleService.update(favoriteDocs)userDao.merge(favoriteDocs, true)println "My favorites: " + favoriteDocs
// My favorites: []
// WHAT? Empty collection? Where are my favorites????
我们从一组项目开始,然后在4种方法中发现我们的数据不见了。
在每个人都可以改变任何事物的世界中,很难在任何给定时间推断任何状态。
它本身甚至还不是“全局状态”,任何拥有(引用到)数据的人都可以清除传递给方法的集合,并可以更改变量。
首选不变性
那是什么 如果对象在实例化后不更改其状态,则该对象是不可变的。
看起来合理吧?
图片来源: 应对并适应持续变化
关于如何使用您的特定语言进行处理,这里有大量资源。 例如,Java默认不支持不变性。 我必须做些工作。
如果有第三方在处理过程中发生问题并更改数据(例如清除我的收藏夹),则可以通过将我的收藏夹传递到不可修改的包装中来快速清除麻烦制造者,例如
def data = [...
]// somewhere inside 3rd-party code
data.clear()// back in my code:
// data is empty *snif*
预防故障:
def data = Collections.unmodifiableCollection([])// somewhere inside 3rd-party code
data.clear() // HAHAA, throws UnsupportedOperationException
在您自己的代码库中,我们可以通过最小化可变数据结构来防止意外的副作用(例如,在某处更改我的数据)。
在大多数FP语言中,例如Haskell , OCaml和Scala ,默认情况下 ,语言本身会促进不变性 。 虽然不是真正的FP语言,但使用ES6编写不可变JavaScript也趋于成为一种好习惯。
首先只读
使用到目前为止所学的原理,并努力防止意外的副作用,我们希望确保实例化实例后, 不能对Doc
类进行任何更改 –甚至不包括updateToProcessed
/ updateToFailed
方法。
这是我们当前的课程:
class Doc {String title, type, apiId, status, error
}
Groovy不需要进行使Java类变为不可变的所有手动工作,而是借助Immutable
-annotation进行了抢救。
当放置在类上时,Groovy编译器进行了一些增强,因此创建后再也没有人可以更新其状态。
@Immutable
class Doc {String title, type, apiId, status, error
}
该对象实际上变为“只读”,并且任何尝试更新属性的操作都将导致恰当命名的ReadOnlyPropertyException
private void updateToProcessed(doc, resource) {doc.apiId = resource.id // BOOM! // throws groovy.lang.ReadOnlyPropertyException: // Cannot set readonly property: apiId...
}private void updateToFailed(doc, e) {doc.status = 'failed' // BOOM! // throws groovy.lang.ReadOnlyPropertyException: // Cannot set readonly property: status...
}
但是,等等,这是否意味着updateToProcessed
/ updateToFailed
方法实际上将无法将文档status
更新为“已处理”或“失败”?
吉普,这就是不变性带给我们的。 如何修复逻辑?
复制第二
Haskell关于“不可变数据”的指南为我们提供了如何进行操作的建议:
纯功能程序通常在不可变数据上运行。 代替更改现有值,而是创建更改的副本并保留原始副本。 由于结构的未更改部分无法修改,因此它们通常可以在旧副本和新副本之间共享,从而节省了内存。
答:我们克隆它!
我们没有更新的原始数据,我们应该做的一个副本-原来不是我们的,应保持不变。 我们的Immutable
-annotation支持一个名为copyWith
的参数。
@Immutable(copyWith = true)
class Doc {String title, type, apiId, status, error
}
因此,我们将更改方法以制作状态更改 (以及api id和错误消息) 的原始副本,并返回此副本 。
(总是返回Groovy方法中的最后一条语句,不需要显式的return
关键字)
private Doc setToProcessed(doc, resource) {doc.copyWith(status: 'processed',apiId: resource.id)
}private Doc setToFailed(doc, e) {doc.copyWith(status: 'failed',error: e.message)
}
数据库逻辑也已上移,将返回的副本存储起来。
我们已经控制了我们的状态!
现在就这样
如果您以Java程序员的身份担心过多的对象实例化对性能的影响,请在此处发表一篇令人放心的文章 。
作为参考,这是重构代码的完整版本。
class FeedHandler {Webservice webserviceDocumentDb documentDbvoid handle(List<Doc> changes) {changes.findAll { doc -> isImportant(doc) }.each { doc ->createResource(doc).thenAccept { resource ->documentDb.update(setToProcessed(doc, resource))}.exceptionally { e ->documentDb.update(setToFailed(doc, e))}}}private CompletableFuture<Resource> createResource(doc) {webservice.create(doc)}private boolean isImportant(doc) {doc.type == 'important'}private Doc setToProcessed(doc, resource) {doc.copyWith(status: 'processed',apiId: resource.id)}private Doc setToFailed(doc, e) {doc.copyWith(status: 'failed',error: e.message)}}
翻译自: https://www.javacodegeeks.com/2018/06/functional-java-part-4-immutability.html