磕叨

在公司做项目是见到前辈们写的一段任务链的代码,大概如下

Runnable task = new TaskA(new TaskB(new TaskC(new taskD())));
task.run();

taskA执行run调用并完成TaskA声明的任务逻辑之后,内部会自动调用构造参数传入的TaskB的run方法,过程类似TaskA,TaskB完成之后一样会调用参数传入的task,直到最后一个没有带下一个task类传入的任务完成,即完成一个管道式调用。

爱思考的我在想,可用,不好用重用,于是动手改改。


准备

经过一段时间开发后,有了一个常用的工具类,方便快速开发,但是这里用到的东西很少,还是要说明一下,这里用到一个我称作ecommon的包,当然我只用了两个很基础的额部分。这两个部分完全可以用你自己的实现,是非常简单的。

思路简述

我们先明确,jdk8以下的情况不作考虑。

pipeline我更多的印象是来自终端上的应用

pipeline是单向的,上个task的输出作为下个task的输入,直到没有下一个task,最后一个task的作用就应该是你期望的。且后续任务只关心前者的输出结果,对于的他是谁,怎么做的,是不关心的。记为 Point1

这个特点是我视为管道与切面或职责链模式的区别所在。

首先,我们得有第一推动,让管道流能有个开始,再就是有中间task,他必定是能接收到上一个任务的输出的,并且,可能有自带参数,并且有自己的输出,最后,有latest的task 与中间task区别在于他不用返回了,latest一般是以副作用的形式实现我们的企图的,如上图的 wc -l 作为最后一个任务是直接把结果打印到屏幕上,而不是返回一个变量给我们读取。根据java的强类型属性,以及刚刚一段的分析,可以得知,有3种类型的任务,开始任务,中间任务,最后任务,并且中间任务的个数是不限的,所有任务至于相邻的任务有一个关联点,那就是 前者的输出类型与后者的输入类型一致 (网文中大部分说自己实现的pipeline的模式都是传递Object类型,到各个子任务中自己强转到需要的类型的,不说好与不好,但我肯定不喜欢)。这个特性记为 Point2

而且,每个子任务,本身是可以带参数的,这是一个需要支持的点。像上图命令中的管道,每个子命令(除第一个)都是同时接受前一个命令的输出作为输入,且自带参数的。但是java在这里其实并不灵活,因此我们约定 后续任务的第一个参数就是前一个任务的输入 , 这个约定是直接影响到我们的代码实现的。这个特性记为 Point3

另外,管道的入口唯一的,一定是从开始任务往后流的。如果入口不一样,那么就是像个不同的管道,他们的意图以及输入输出的期望都是不同的。这个特性记为 Point4

最后,在java中使用,我肯定不能像终端那种,错了重敲命令就是了,所以需要异常控制以及做一些相邻任务承上启下的时刻做点什么,例如日子打印,断言等。这个算附加题。

提起键盘撸

(因为我已经写完并测试完了,所以我就反过来解析我是怎么想的了)

这里以Runnable接口作为基础接口。给出其中一个测试的例子

这里初始任务是给出一个日期,中间任务是拼接成人类友好的1句话,最终任务是直接打印到屏幕。(现实中要实现这样一句话,当然是直接撸啦。这里只是为了演示),看看Pipeline初始任务的定义

先不看其他属性,看构造方法,传入一个 IFunction<R> ,按照准备一节的定义,他是一个返回类型为声明泛型R,且无参数输入的闭包函数(或称作lambda表达式)。对照上面PipelineTest中就是那个 () -> { return new Date(); } , (得益于jdk8的类型推断,在 new Pipeline<> 构造时,不用再声明其泛型,编译器能根据闭包函数的return类型推断出这里是个Date类型)。next, end 是指明管道的下接任务,这可以看出管道是极其类似于任务链/职责链的(需要注意nextend同时只能有个一个存在)。hook是异常管理以及任务间承接时做一个切面方法的,argCxt是记下传递参数,方便hook中的方法使用(这个是因为java需要的,跟管道模式并没有关系)。

再看add方法的一个重载,添加并返回中间task

add方法传入一个IFunction1<RT, R>的闭包(lambda),尾数为1,意味着接受一个 R 类型的输入,并在方法升声明了 RT ,以 RT 类型作为输出。其中 R 的泛型声明在类上,就是与构造方法的 R 是同一个类型。而 RT 的具体类型的推断会根据具体的lambda的返回类型决定。这里add方法会返回刚刚构造出来的中间任务的声明对象。add方法需要保证当前任务是没被声明过后续任务的。

再看MiddlePipeline类的定义

先看构造方法,他就是接受add方法传入的闭包。他声明了两个泛型变量 分别是 <C, P>,其中 C代表他的输入类型, P代表的他的产出类型。同初始任务一样,他也有nextend 指明他的管道后接任务(next)。可以注意到这里的 next 和初始任务的属性 next 的产出类型都是被放上了泛型通配符 ? ,是因为任务并没办法知道他的子任务的产出类型的(后面会再说一下这个问题)。

再看add方法的一个另一个重载,添加并返回最终task

类似返回中间态的task,只不过这里用了无返回的闭包。

再看EndPipeline类的定义

最终task的定义清爽很多,他只关心输入,并执行。并且他没有后续任务。

再来补充下AbstractPipeline的解析

这个写法是为了实现Point4所描述的事的,只要是同一个pipeline上的task所有入口都是初始任务上的那个run方法。(为了省事实现,后续任务的基类和所有派生类都是初始任务的非静态内部类)

再看看初版版本run方法

逻辑很简单,执行初始任务,得到结果,然后找后续任务,把结果作为输入来执行后续任务,(其中循环时满足上一个输出作为下一个输入),直到有一个管道类的中间态任务为null,然后判断最终任务是否为null ,非空则执行它。

需要说明一下这里用 @SuppressWarnings 压制了警告,是因为确信java编译器能确保连续两个add进来的task之间的输入输出的类型关系是一致的(这一点,如果不一致,在编写代码时IDE就会报错了)。

到此,一个简单的java实现的pipeline模式基本可以用,跑最开始那个demoTest是没有问题了。

再给一个样例demo

管道中的3个方法的职责就如他的名字那样(实现上我这里只是简单的new一下),然后同过Pipeline类以及它的add方法串起来,执行结果如红色部分。聪明的人肯定能想到,那么像那个java的stream的?嗯很像,stream是类似把元素放到单个跑到上,按照定义那样的自己跑到终点(这也是使用方代码方便地切换到并行流的原因,因为逻辑一致,当然,并发问题是另一个层面的问题)。

而pipeline则横向的一阶段一阶段地执行,如果要增加吞吐量怎么搞?聪明的你肯定能想到分片了,这样走下去就跟parallelstream的意图不谋而合了。那么还有别的好处吗?嗯,你想想Mock测试?职责上有没有让你更好切分了(正如这里命令的方法名那这样)?


进一步完善

上面一节基本上能把 Point1Point2的一半 、 Point3Point4 实现了,剩下 Point2中说到的,除了接收前一个任务的输入,还允许管道声明时传入参数的这个功能,以及那个附加题说到的java应用上的妥协。

pipeline声明上附带参数

这个时候就要好好用到 准备 一节中的那些 函数接口 了。说起来并不好解析,但是如果你了解过curry柯里化这个概念的话,那一看图你就懂了,看图。

就是把带参数的lambda重新包装一次为不带参数的lambda表达式。后面middlePipeline的带参数部分则是重新封装为一个只接受一个参数且返回类型相同的lambda表达式,这是类似的。

来一个测试看看,并附上图中说明

对java友好支持

附加题说的这个就跟简单了,找个地方分别设置好两个玩意,在对应的地方执行他们就是了

public class PipelineHook {

	private boolean preventThrow = false;

    // 异常发生时执行此表达式
public final IVoidFunction3<PipelineHook, Exception, Object[]> exceptionHandler; // 调用后续任务钱执行此lambda表达式
public final IVoidFunction1<Object> aspecter; public PipelineHook(IVoidFunction3<PipelineHook, Exception, Object[]> exceptionHandler, IVoidFunction1<Object> aspecter) {
this.exceptionHandler = exceptionHandler;
this.aspecter = aspecter;
} public PipelineHook(IVoidFunction1<Object> aspecter) {
this(null, aspecter);
} public PipelineHook(IVoidFunction3<PipelineHook, Exception, Object[]> exceptionHandler) {
this(exceptionHandler, null);
} // 是否阻止异常抛出
public boolean isPreventThrow() {
return preventThrow;
} // 设置标记阻止异常抛出
public void setPreventThrow() {
this.preventThrow = true;
} }

通过两个lambda变量构造出hook对象,并通过初始任务的的 addPipelineHook 方法set进去,他们具体在 run 方法体中发挥作用,现在,run方法更新为

其中 getCxtInfo 方法会把当前子任务的参数转化是字符串,让异常信息能够被人读懂。


今天先到这里了,整体下来,觉得跟stream太像了,我发现用stream码起来特爽,读起来特惨(特别是读别人的多重stream的时候),而这个pipeline正好相反耶。总的来说,就是个模式,需要提高吞吐量的话,使用分片配合线程池的话,吞吐量会得到巨量提升哦(把每个分配的大小设置为1不就是我们的parallelStream吗?哈哈)。

issue在: https://github.com/kimffy24/EJoker/issues/30

初次提交: https://github.com/kimffy24/EJoker/commit/c71e5d76a0904249b7c1399bd8ba52ec72fe9a0e

奇思妙想-java实现另类的pipeline模式的更多相关文章

  1. Java 设计模式泛谈&装饰者模式和单例模式

    设计模式(Design Pattern) 1.是一套被反复使用.多人知晓的,经过分类编目 的 代码设计经验总结.使用设计模式是为了可重用代码,让代码更容易维护以及扩展. 2.简单的讲:所谓模式就是得到 ...

  2. 用java语言实现事件委托模式

    http://blog.csdn.net/yanshujun/article/details/6494447 用java语言实现事件委托模式 2010-04-27 00:04 2206人阅读 评论(1 ...

  3. 第二篇 :微信公众平台开发实战Java版之开启开发者模式,接入微信公众平台开发

    第一部分:微信公众号对接的基本介绍 一.填写服务器配置信息的介绍 登录微信公众平台官网后,进入到公众平台后台管理页面. 选择 公众号基本设置->基本配置 ,点击“修改配置”按钮,填写服务器地址( ...

  4. Java中的简单工厂模式

    举两个例子以快速明白Java中的简单 工厂模式: 女娲抟土造人话说:“天地开辟,未有人民,女娲抟土为人.”女娲需要用土造出一个个的人,但在女娲造出人之前,人的概念只存在于女娲的思想里面.女娲造人,这就 ...

  5. java设计模式之Proxy(代理模式)

    java设计模式之Proxy(代理模式) 2008-03-25 20:30 227人阅读 评论(0) 收藏 举报 设计模式javaauthorizationpermissionsstringclass ...

  6. Java多线程编程中Future模式的详解

    Java多线程编程中,常用的多线程设计模式包括:Future模式.Master-Worker模式.Guarded Suspeionsion模式.不变模式和生产者-消费者模式等.这篇文章主要讲述Futu ...

  7. 黑马程序员:Java基础总结----静态代理模式&动态代理

    黑马程序员:Java基础总结 静态代理模式&动态代理   ASP.Net+Android+IO开发 . .Net培训 .期待与您交流! 静态代理模式 public  class  Ts {   ...

  8. Java的三种代理模式

    Java的三种代理模式 1.代理模式 代理(Proxy)是一种设计模式,提供了对目标对象另外的访问方式;即通过代理对象访问目标对象.这样做的好处是:可以在目标对象实现的基础上,增强额外的功能操作,即扩 ...

  9. Java设计模式之《桥接模式》及应用场景

    摘要: 原创作品,可以转载,但是请标注出处地址http://www.cnblogs.com/V1haoge/p/6497919.html 这里摘抄一份他处的概念,你可以不必理会,先看下面得讲解与实例, ...

随机推荐

  1. 餐厅随评系列之四:Umu日本料理(米其林二星)

    文章目录 在过去的几个月,工作和生活都极其忙碌,因此博客短暂停更了一阵子.慢慢积累下了很多素材,从近期开始恢复博客更新,不过很多内容估计得靠回忆了. 索性采取"倒叙"的方法,先从最 ...

  2. 一起了解 .Net Foundation 项目 No.8

    .Net 基金会中包含有很多优秀的项目,今天就和笔者一起了解一下其中的一些优秀作品吧. 中文介绍 中文介绍内容翻译自英文介绍,主要采用意译.如与原文存在出入,请以原文为准. IdentityModel ...

  3. 运输层协议TCP和UDP

    运输层协议TCP和UDP 一.用户数据报协议 UDP 1.1.UDP 概述 UDP 只在 IP 的数据报服务之上增加了很少一点的功能,即端口的功能和差错检测的功能. 虽然 UDP 用户数据报只能提供不 ...

  4. 小技巧(一):将文本文件txt或网页快捷方式固定到win10开始菜单

    win10不知道怎么回事不支持将文本文件和网页快捷方式固定到开始菜单 解决方法: 利用cmd 创建一个快捷方式: 路径:cmd /A /C  C:\Users\Admin\Desktop\test.t ...

  5. 二叉堆的BuildHeap操作

    优先队列(二叉堆)BuildHeap操作 \(BuildHeap(H)\)操作把\(N\)个关键字作为输入并把它们放入空堆中.显然,这可以使用\(N\)个相继的\(Insert\)操作来完成.由于每个 ...

  6. MVC09

    1.委托(delegate)调用静态方法 委托类似于C++中的函数指针. 某方法仅仅在执行的时候才能确定是否被调用. 是实现事件和回调函数的基础. 面向对象,安全性高. using System; u ...

  7. 前端每日实战:149# 视频演示如何用纯 CSS 创作一个宝路薄荷糖的动画

    效果预览 按下右侧的"点击预览"按钮可以在当前页面预览,点击链接可以全屏预览. https://codepen.io/comehope/pen/oagrvz 可交互视频 此视频是可 ...

  8. post请求与get请求的差别

    幂等的概念 在理解这两者的区别前需要先了解的概念: 幂等性在编程中一个幂等操作的特点是其任意多次执行所产生的影响均与一次执行的影响相同.简单的说就是,浏览器发起一次请求和多次请求,不会改变目标资源的状 ...

  9. 如何在普通的元素上实现enter键的绑定

    在做登录页面时候,通常当用户输入账号密码后直接按enter键就触发登录按钮了. 如果是input标签,vue中可以绑定按键修饰符,但是如果是其它标签呢.我的做法如下: document.querySe ...

  10. notepad++ 字符处理: 字符前后删除 或 删除未包含字符串的行

    字符串前后删除 删除str之后的所有字符用,打开替换(Ctrl+H) :str.*$ 删除str之前的所有字符用:^.*str 如果是其他字符就把str替换为其他字符 ---------------- ...