这是第8部分,该系列的最后一部分称为“ Functional Java by Example”。
我在本系列的每个部分中发展的示例是某种“提要处理程序”,用于处理文档。 在上一期文章中,我们已经使用Vavr库看到了一些模式匹配,并且还将故障也视为数据 ,例如,采用替代路径并返回到功能流程。
在本系列的最后一篇文章中,我将功能发挥到了极致 :一切都变成了功能。
如果您是第一次来,最好是从头开始阅读。 它有助于了解我们从何处开始以及如何在整个系列中继续前进。
这些都是这些部分:
- 第1部分–从命令式到声明式
- 第2部分–讲故事
- 第3部分–不要使用异常来控制流程
- 第4部分–首选不变性
- 第5部分–将I / O移到外部
- 第6部分–用作参数
- 第7部分–将失败也视为数据
- 第8部分–更多纯函数
我将在每篇文章发表时更新链接。 如果您通过内容联合组织来阅读本文,请查看我博客上的原始文章。
每次代码也被推送到这个GitHub项目 。
最大化运动部件
您可能已经听过Micheal Feathers的以下短语:
OO通过封装运动部件使代码易于理解。 FP通过最大程度地减少运动部件来使代码易于理解。
好的,让我们稍稍忘记上一期中的故障恢复,然后继续下面的版本:
FeedHandler { class FeedHandler { List<Doc> handle(List<Doc> changes, Function<Doc, Try<Resource>> creator) { changes .findAll { doc -> isImportant(doc) } .collect { doc -> creator.apply(doc) }.map { resource -> setToProcessed(doc, resource) }.getOrElseGet { e -> setToFailed(doc, e) } } } private static boolean isImportant(doc) { doc.type == 'important' } private static Doc setToProcessed(doc, resource) { doc.copyWith( status: 'processed' , apiId: resource.id ) } private static Doc setToFailed(doc, e) { doc.copyWith( status: 'failed' , error: e.message ) } }
替换为功能类型
我们可以使用对函数接口类型的变量(例如Predicate
或BiFunction
的引用来替换每种方法。
A)我们可以替换一个接受1个参数并返回boolean的方法 。
private static boolean isImportant(doc) { doc.type == 'important' }
由谓词
private static Predicate<Doc> isImportant = { doc -> doc.type == 'important' }
B),我们可以替换一个接受2个参数并返回结果的方法
private static Doc setToProcessed(doc, resource) { ... } private static Doc setToFailed(doc, e) { ... }
具有双功能
private static BiFunction<Doc, Resource, Doc> setToProcessed = { doc, resource -> ... } private static BiFunction<Doc, Throwable, Doc> setToFailed = { doc, e -> ... }
为了实际调用封装在(Bi)Function中的逻辑,我们必须对其调用apply
。 结果如下:
FeedHandler { class FeedHandler { List<Doc> handle(List<Doc> changes, Function<Doc, Try<Resource>> creator) { changes .findAll { isImportant } .collect { doc -> creator.apply(doc) .map { resource -> setToProcessed.apply(doc, resource) }.getOrElseGet { e -> setToFailed.apply(doc, e) } } } private static Predicate<Doc> isImportant = { doc -> doc.type == 'important' } private static BiFunction<Doc, Resource, Doc> setToProcessed = { doc, resource -> doc.copyWith( status: 'processed' , apiId: resource.id ) } private static BiFunction<Doc, Throwable, Doc> setToFailed = { doc, e -> doc.copyWith( status: 'failed' , error: e.message ) } }
将所有输入移至功能本身
我们将所有内容移至方法签名,以便FeedHandler的handle
方法的调用者可以提供自己的那些功能的实现。
方法签名将更改为:
List<Doc> handle(List<Doc> changes, Function<Doc, Try<Resource>> creator)
至
List<Doc> handle(List<Doc> changes, Function<Doc, Try<Resource>> creator, Predicate<Doc> filter, BiFunction<Doc, Resource, Doc> successMapper, BiFunction<Doc, Throwable, Doc> failureMapper)
其次,我们将重命名原始(静态) 谓词和BiFunction变量
-
isImportant
-
setToProcessed
-
setToFailed
转换为类顶部的新常量 ,反映它们的新作用。
-
DEFAULT_FILTER
-
DEFAULT_SUCCESS_MAPPER
-
DEFAULT_FAILURE_MAPPER
客户端可以完全控制是否将默认实现用于某些功能,或者何时需要接管自定义逻辑。
例如,当仅需要定制故障处理时,可以这样调用handle
方法:
BiFunction<Doc, Throwable, Doc> customFailureMapper = { doc, e -> doc.copyWith( status: 'my-custom-fail-status' , error: e.message ) } new FeedHandler().handle(..., FeedHandler.DEFAULT_FILTER, FeedHandler.DEFAULT_SUCCESS_MAPPER, customFailureMapper )
如果您的语言支持,则可以通过分配默认值来确保客户端实际上不必提供每个参数。 我正在使用支持将默认值分配给方法中的参数的Apache Groovy :
List<Doc> handle(List<Doc> changes, Function<Doc, Try<Resource>> creator, Predicate<Doc> filter = DEFAULT_FILTER, BiFunction<Doc, Resource, Doc> successMapper = DEFAULT_SUCCESS_MAPPER, BiFunction<Doc, Throwable, Doc> failureMapper = DEFAULT_FAILURE_MAPPER)
在我们将应用另一个更改之前,请看一下代码:
FeedHandler { class FeedHandler { private static final Predicate<Doc> DEFAULT_FILTER = { doc -> doc.type == 'important' } private static final BiFunction<Doc, Resource, Doc> DEFAULT_SUCCESS_MAPPER = { doc, resource -> doc.copyWith( status: 'processed' , apiId: resource.id ) } private static final BiFunction<Doc, Throwable, Doc> DEFAULT_FAILURE_MAPPER = { doc, e -> doc.copyWith( status: 'failed' , error: e.message ) } List<Doc> handle(List<Doc> changes, Function<Doc, Try<Resource>> creator, Predicate<Doc> filter = DEFAULT_FILTER, BiFunction<Doc, Resource, Doc> successMapper = DEFAULT_SUCCESS_MAPPER, BiFunction<Doc, Throwable, Doc> failureMapper = DEFAULT_FAILURE_MAPPER) { changes .findAll { filter } .collect { doc -> creator.apply(doc) .map { resource -> successMapper.apply(doc, resource) }.getOrElseGet { e -> failureMapper.apply(doc, e) } } } }
介绍两者
您是否注意到以下部分?
.collect { doc -> creator.apply(doc) .map { resource -> successMapper.apply(doc, resource) }.getOrElseGet { e -> failureMapper.apply(doc, e) } }
请记住, creator
的类型是
Function<Doc, Try<Resource>>
表示它返回一个Try
。 我们在第7部分中介绍了Try ,它是从Scala等语言中借来的。
幸运的是, collect { doc
的“ doc”变量仍在传递给我们需要它的successMapper
和failureMapper
范围内 ,但是Try#map
的方法签名(接受一个Function )与我们的successMapper
之间存在差异一个BiFunction 。 Try#getOrElseGet
也是Try#getOrElseGet
,它也只需要一个Function 。
从Try Javadocs:
- map(Function <?super T,?extended U>映射器)
- getOrElseGet(Function <?super Throwable ,?
简而言之,我们需要从
- BiFunction <文档,资源,文档> successMapper
- BiFunction <文档,Throwable,文档> failureMapper
至
- 函数<资源,文档> successMapper
- 函数<Throwable,Doc> failureMapper
同时仍然可以将原始文档作为输入 。
让我们介绍两个简单的类型,它们封装了2个BiFunction的2个参数:
class CreationSuccess { Doc doc Resource resource } class CreationFailed { Doc doc Exception e }
我们将论点从
- BiFunction <文档,资源,文档> successMapper
- BiFunction <文档,Throwable,文档> failureMapper
改为功能 :
- 函数<CreationSuccess,Doc> successMapper
- 函数<CreationFailed,Doc> failureMapper
现在, handle
方法如下所示:
List<Doc> handle(List<Doc> changes, Function<Doc, Try<Resource>> creator, Predicate<Doc> filter, Function<CreationSuccess, Doc> successMapper, Function<CreationFailed, Doc> failureMapper) { changes .findAll { filter } .collect { doc -> creator.apply(doc) .map(successMapper) .getOrElseGet(failureMapper) } }
…… 但是还行不通 。
Try
使map
和getOrElseGet
需要分别。 一个
- 函数<资源,文档> successMapper
- 函数<Throwable,Doc> failureMapper
这就是为什么我们需要将其更改为另一个著名的FP结构,称为Either 。
幸运的是Vavr有要么太。 它的Javadoc说:
任一代表两种可能的值。
通常使用这两种类型来区分正确的值(“正确”)或错误的值。
它变得非常抽象:
Either可以是Either.Left或Either.Right。 如果给定的Either是Right并投影到Left,则Left操作对Right值没有影响。 如果给定的Either是Left并投影到Right,则Right操作对Left值没有影响。 如果将“左”投影到“左”或将“右”投影到“右”,则操作会生效。
让我解释以上神秘的文档。 如果我们更换
Function<Doc, Try<Resource>> creator
通过
Function<Doc, Either<CreationFailed, CreationSuccess>> creator
我们将CreationFailed
分配给“ left”参数,该参数通常会保留错误(请参见Either上的Haskell文档 ), CreationSuccess
是“ right”(和“正确”)值。
在运行时,该实现曾经返回一个Try
,但是现在可以返回一个Either.Right ,如果成功,例如
return Either.right( new CreationSuccess( doc: document, resource: [id: '7' ] ) )
或Either.Left ,但发生故障时除外- 两者都包括原始文档 。 是。
因为现在类型最终匹配,所以我们终于压扁了
.collect { doc -> creator.apply(doc) .map { resource -> successMapper.apply(doc, resource) }.getOrElseGet { e -> failureMapper.apply(doc, e) } }
进入
.collect { doc -> creator.apply(doc) .map(successMapper) .getOrElseGet(failureMapper) }
现在, handle
方法如下所示:
List<Doc> handle(List<Doc> changes, Function<Doc, Either<CreationFailed, CreationSuccess>> creator, Predicate<Doc> filter, Function<CreationSuccess, Doc> successMapper, Function<CreationFailed, Doc> failureMapper) { changes .findAll { filter } .collect { doc -> creator.apply(doc) .map(successMapper) .getOrElseGet(failureMapper) } }
结论
我可以说我已经实现了我一开始制定的大多数目标:
- 是的,我设法避免了重新分配变量
- 是的,我设法避免了可变数据结构
- 是的,我设法避免了状态 (至少在FeedHandler中)
- 是的,我设法支持函数 (使用某些Java内置函数类型和某些第三方库Vavr)
我们已经将所有内容移到了函数签名,以便FeedHandler的handle
方法的调用者可以直接传递正确的实现。 如果您从头到尾回顾原始版本,您会注意到在处理更改列表时,我们仍然承担所有责任:
- 通过某些条件过滤文档列表
- 为每个文档创建资源
- 成功创建资源后执行某些操作
- 无法创建资源时执行其他操作
然而,在第一部分中,这些责任是势在必行写出来,for语句声明,都在一个大聚集在一起handle
方法。 现在,最后,每个决定或动作都由具有抽象名称的函数表示,例如“过滤器”,“创建者”,“ successMapper”和“ failureMapper”。 实际上,它以一个或多个函数为参数,成为一个高阶函数。 提供所有参数的责任已经转移到了客户的上层。 如果您查看GitHub项目,您会注意到,对于这些示例,我不得不不断更新单元测试。
有争议的部分
在实践中,如果不需要,我可能不会编写我的(Java)商业代码,例如FeedHandler
类在传递通用Java函数类型(即Function
, BiFunction
, Predicate
, Consumer
, Supplier
)方面的使用方式所有这些极端的灵活性。 所有这些都是以可读性为代价的。 是的,Java是一种静态类型的语言,因此,使用泛型时,必须在所有类型参数中都明确使用一种语言,从而导致以下功能的签名困难:
handle(List<Doc> changes, Function<Doc, Either<CreationFailed, CreationSuccess>> creator, Predicate<Doc> filter, Function<CreationSuccess, Doc> successMapper, Function<CreationFailed, Doc> failureMapper)
在普通JavaScript中,您将没有任何类型,并且您必须阅读文档以了解每个参数的期望。
handle = function (changes, creator, filter, successMapper, failureMapper)
但是,这是一个折衷方案。 Groovy,也是一种JVM语言, 将允许我在本系列的所有示例中省略类型信息,甚至允许我使用Closures(就像Java中的lambda表达式一样)是Groovy中功能编程范例的核心。
更极端的做法是在类级别指定所有类型,以使客户端具有最大的灵活性,以便为不同的FeedHandler
实例指定不同的类型。
handle(List<T> changes, Function<T, Either<R, S>> creator, Predicate<T> filter, Function<S, T> successMapper, Function<R, T> failureMapper)
什么时候合适?
- 如果您完全控制代码,则在特定上下文中使用它来解决特定问题时,这将过于抽象而无法产生任何收益。
- 但是,如果我将一个库或框架开源(或者在一个组织内向其他团队或部门使用),该库或框架正在各种不同的用例中使用,那么我可能不会事先想到,为灵活性而设计可能值得。 让呼叫者决定如何过滤以及成功或失败的构成是明智之举。
最终,上述内容在API设计 ,是和解耦方面略有涉及,但是在典型的Enterprise Java Java项目中“使一切成为函数”可能需要与您和您的团队成员进行一些讨论。 多年来,一些同事已经习惯了一种更为传统,惯用的代码编写方式。
好的零件
- 我绝对希望使用不可变的数据结构 (和“参照透明性”)来帮助推断我的数据所处的状态。想想
Collections.unmodifiableCollection
的集合。 在我的示例中,我将Groovy的@Immutable
用于POJO,但在普通的Java库(例如Immutables , AutoValue或Project Lombok)中也可以使用。 - 最大的改进实际上是导致了一种更具功能性的样式:使代码讲故事 ,这主要是关于分离关注点并适当地命名事物。 在任何编程风格(即使是OO:D)中,这都是一个好习惯,但这确实消除了混乱,并允许引入(纯)函数。
- 在Java中,我们习惯于以特定方式进行异常处理,以至于像我这样的开发人员很难提出其他解决方案。 像Haskell这样的功能语言仅返回错误代码,因为“ Niklaus Wirth认为异常是GOTO的转世,因此省略了它们” 。 在Java中,可以使用
CompletableFuture
或… - 通过引入第3方库(例如Vavr)可在您自己的代码库中使用的特定类型(例如
Try
和Either
)可以极大地帮助您启用更多以FP样式编写的选项 ! 我以流畅的方式编写“成功”或“失败”路径并且可读性很强,这让我非常着迷。
Java不是F#的Scala或Haskell或Clojure,它最初遵循的是面向对象编程(OOP)范例,就像C ++,C#,Ruby等一样,但是在Java 8中引入了lambda表达式并结合了一些很棒的功能之后如今,开放源代码库如今,开发人员绝对可以选择OOP和FP必须提供的最佳元素 。
做系列的经验教训
我在很早以前就开始了这个系列的讨论 。 早在2017年,我发现自己在一段代码上进行了一些FP风格的重构,这启发了我去寻找一系列名为“ Functional Java by Example”的文章的示例 。 这成为我在每个批次中一直使用的FeedHandler
代码。
那时我已经对所有的代码进行了更改,但是当我计划编写实际的博客文章时,我常常想到:“我只是不能展示重构,我必须进行实际解释!” 那就是我为自己埋下陷阱的地方,因为在整个过程中,我坐下来写作的时间越来越少。 (曾经写过博客的任何人都知道,简单地分享要点和撰写可理解的英语co的连贯段落在时间上的区别)
下次当我想到进行一系列学习时,我将向Google返回一些经验教训:
- 如果您不准备在发布新文章时每次准备发布的每期文章中都没有更新所有链接,则不要在每篇文章的顶部都包含目录(TOC)。 如果将这些交叉发布到公司的公司博客中,那么工作量是原来的2倍🙂
- 随着时间的流逝,您可能会得出自己宁愿偏离主要用例的结论,也就是刚开始使用的Big Coding Example。 我宁愿展示更多的FP概念(例如, 使用FP技术时的生硬,记忆,懒惰以及不同的心态),但我不能很好地适应以前做过的重构和我在一开始建立的TOC 。 如果您正在撰写有关特定概念的文章,通常会找到一个合适的示例来帮助说明手头的特定概念,并且仍然与读者相关。 随着时间的流逝,我将获得更好的洞察力,从而可以确定接下来要写的更好的东西以及要使用的更合适的示例。 下次,我将不得不寻找一种方法来给(更好:允许)我自己一些创作上的自由😉
- 《功能性思维:语法惊人的范式 》,尼尔·福特(Neil Ford)着,它展示了FP思维的新方法,也以不同的方式处理问题。
- 40分钟内的函数式编程 Russ Olsen的Youtube视频解释说:“这些数学家证明1 + 1 = 2需要379页。 让我们看看我们可以从中窃取什么好主意。”
- 为什么不对函数进行规范编程? 理查德·费尔德曼(Richard Feldman)的Youtube视频,他解释了为什么OOP变得非常流行,以及FP为何不是常态。 正如您所知,他是Elm核心团队的成员,与FP有一定的联系。
- (耦合)控制的倒置有关“托管功能”的深思熟虑的文章。 您想要抽象吗?
如果您有任何意见或建议,我很想听听他们的意见!
编程愉快! 🙂
翻译自: https://www.javacodegeeks.com/2019/12/functional-java-by-example-part-8-more-pure-functions.html