java 函数式编程 示例_功能Java示例 第8部分–更多纯函数
java 函数式编程 示例
这是第8部分,该系列的最后一部分称为“示例功能Java”。
我在本系列的每个部分中开发的示例是某种“提要处理程序”,用于处理文档。 在上一期文章中,我们已经使用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个参数的方法,该方法返回一个布尔值 。
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) { ... }
具有BiFunction
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,?extended T> other)
简而言之,我们需要从
- 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语言, 可以让我省略所有的例子类型的信息在这个系列中,甚至允许我使用闭包(象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倍
java 函数式编程 示例_功能Java示例 第8部分–更多纯函数相关推荐
- java 函数式编程 示例_功能Java示例 第1部分–从命令式到声明式
java 函数式编程 示例 功能编程(FP)的目的是避免重新分配变量,避免可变的数据结构,避免状态并全程支持函数. 如果将功能性技术应用于日常Java代码,我们可以从FP中学到什么? 在这个名为&qu ...
- java 示例_功能Java示例 第4部分–首选不变性
java 示例 这是称为" Functional Java by Example"的系列文章的第4部分. 在上一部分中,我们讨论了一些副作用,并且我想进一步详细说明如何通过将不可变 ...
- java 示例_功能Java示例 第3部分–不要使用异常来控制流程
java 示例 这是称为" Functional Java by Example"的系列文章的第3部分. 我在本系列的每个部分中开发的示例是某种"提要处理程序" ...
- 大数据 java 代码示例_功能Java示例 第7部分–将失败也视为数据
大数据 java 代码示例 这是称为" Functional Java by Example"的系列文章的第7部分. 我在本系列的每个部分中开发的示例是某种"提要处理程序 ...
- java 示例_功能Java示例 第5部分–将I / O移到外部
java 示例 这是称为" Functional Java by Example"的系列文章的第5部分. 在上一部分中,我们停止了对文档的变异,并返回了数据的副本. 现在,我们需要 ...
- java 示例_功能Java示例 第2部分–讲故事
java 示例 这是称为" Functional Java by Example"的系列文章的第2部分. 我在本系列的每个部分中开发的示例是某种"提要处理程序" ...
- Java并发编程实战_福州java编程实战培训班排名
如何选择福州java培训中心? 在福州,如果想迅速掌握java开发,参加福州java培训班无疑是一种非常有效的方式.但是,市场上有这么多的java培训机构,我们在选择的时候难免会眼花缭乱.福州java ...
- java逻辑编程题_用Java编程解决一道逻辑推理题
package mytest; import java.util.Scanner; public class Test14 { /** * 竞赛结果表明,他们都说对了一半,说错了一半,并且无并列名次, ...
- java软件编程学习班_高新区java软件编程 学习班
高新相关题的可的问有利于你开发环境那些能和解决这样真正中去怪怪奇奇. 不能当基派生类的类时操作适应,程学或者说,程学需要的操派生类就基类重载作,被用象的操的对于操基类作能作派生类,多态的背的所有操:派 ...
最新文章
- .bash_profile 写入时间格式YYYY-MM-DD HH24:MI:SS 时报错 not a valid identifier
- Polly 重试策略
- php300云,概述 · PHP300Framework2.0 · 看云
- 浅尝boost之timer
- input type='file' 上传文件时显示文件名及上传进度
- 机器学习实战之决策树
- java 发送邮件昵称_利用JavaMail发送QQ邮件
- 360网址导航源码5.0源码
- python面向对象大段代码_python粗谈面向对象(一)
- mysql并行复制功能
- 今日头条定位融资商业计划书
- day11_rabbitmq和redis
- 微信小程序之三元操作符
- C语言在坐标轴上输出曲线,C语言打印正弦曲线、直线、圆等等
- 腾讯云常见云产品中的云硬盘(块存储)、文件存储、对象存储三者的区别!
- [PTA]实验11-2-7 统计专业人数
- java计算机毕业设计劳务外包管理系统源码+系统+数据库+lw文档+mybatis+运行部署
- 有效的括号长按键入验证外星语词典字符的最短距离用栈实现队列
- Linux Centos7:11.给系统增加一块硬盘
- php如何做网站地图,如何做网站地图?
热门文章
- [学习笔记] 伸展树splay详解+全套模板+例题[Luogu P3369 【模板】普通平衡树]
- 周末狂欢赛2(冒泡排序,概率充电器,不勤劳的图书管理员)
- YBTOJ:前缀询问(trie树)
- 2018CCPC吉林赛区(重现赛)补题部分——F线段树待补
- Spark SQL(九)之基于用户的推荐公式
- Dubbo(二)之SpringBoot nacos集成
- 史上最全MySQL 大表优化方案(长文)
- ​通俗理解神经网络BP反向传播算法
- jQuery的三种bind/One/Live/On事件绑定使用方法
- Vue.js 定义组件模板的七种方式
- java 函数式编程 示例_功能Java示例 第1部分–从命令式到声明式