中文编程
首发于中文编程

[转载] 做煎饼果子的N种方式——From Sequential to Reactive

注: 此文中的示例代码使用了中文命名且例子非常形象接近生活. 已获得原作者@hepin1989许可转载. 原文地址: 做煎饼果子的N种方式--From Sequential to Reactive

需要注意的是
本文不是真正的讨论如何做煎饼果子。

引子

相信南方人都吃过煎饼果子——一种裹着生菜和火腿肠的鸡蛋煎饼。虽然好吃,其制作过程却也不比汉堡包简单,让我们一起来拆解下吧。

图 1 ——煎饼果子拆解

从上图(这个?没有卷起来)我们可以看到,一个煎饼果子分为下面的几个部分:

  1. 最下面的是鸡蛋煎饼——鸡蛋是打在煎饼上的
  2. 中间是一层生菜——若干
  3. 在上面是一根火腿肠——只有一根

最后把它卷起来,套上食品塑料袋,就是煎饼果子了,如下图:

看到如此美味的煎饼果子,是不是很想自己做来吃吃,甚至是去开个店,专门卖煎饼果子呢?当上CEO,迎娶白富美,这都不是梦啊~~。那么,下面就让我们一起看看如果做一个煎饼果子吧。

如何做?

1. 准备材料

煎饼果子需要下面几种食材:

  • 面粉——我们需要做煎饼,
  • 鸡蛋N枚——取决于你要做的?的数量,一般一个煎饼果子,使用一枚?
  • 生菜若干——平均一个煎饼果子,使用1到2片生菜,
  • 火腿肠N根——一般,都会加上火腿肠,可选,一般一个煎饼果子,一根火腿肠。

准备好了原材料,让我们来做煎饼果子吧,

2. 制作步骤

2.1 使用平底锅,将高筋面粉,调匀,然后变成煎饼,如下所示:

2.2 待煎饼7分熟之后,将鸡蛋打在煎饼上,并刮匀,如下所示:

2.3 放上生菜若干(1-2片),如下所示:

现在送脆饼

2.4 放上火腿肠 1根 并卷起来

2.5 从中间纵向断开,如下所示:

制作步骤回顾

从上面的图解中我们可以看到,流程是这样的:

  1. 面粉 + 水 -> 面浆;面浆 + 烘焙 -> 煎饼
  2. 煎饼 + 鸡蛋 + 烘焙 -> 鸡蛋煎饼
  3. 鸡蛋煎饼 + 生菜 -> 带有生菜的鸡蛋煎饼
  4. 带有生菜的鸡蛋煎饼 + 火腿肠 -> 带有生菜和火腿肠的鸡蛋煎饼
  5. 带有生菜和火腿肠的鸡蛋煎饼 + 卷曲 + 切断 -> 煎饼果子

我们对上面的步骤进行化简:

  1. 面粉 -> 煎饼
  2. 煎饼 + 鸡蛋 -> 鸡蛋煎饼
  3. 鸡蛋煎饼 + 生菜 + 火腿肠 -> 煎饼果子

我们对上面的步骤继续化简

面粉 -> 煎饼 -> 鸡蛋 -> 鸡蛋煎饼 -> 生菜 -> 火腿肠 -> 煎饼果子

下面让我们使用代码先来模拟下,验证怎么样才可以高效的做出煎饼果子吧。

N 中制作方式

一个人(傻等)

面粉 -> 煎饼 -> 鸡蛋 -> 鸡蛋煎饼 -> 生菜 -> 火腿肠 -> 煎饼果子

实现代码:

让我们运行上面的代码,其输出结果如下:

开始计时
好吃的煎饼果子勒
耗时:9 秒

如果我们偶尔做一次,其实还好啦,满足,不过作为有梦想的程序员,我们要快点,这样才有竞争力啊,对,快,更快。

一个人(手脚利索,异步做)

这就是我们牛逼的异步的方式了,即基于EventLoop/CSP的方式,事情都是一个人做,这个好了接着做下一个,在做煎饼的时候,不会等待煎饼烤熟。而是回去洗菜、准备火腿肠等。

多请三个帮手

这个时候,我们就会想,是不是多招聘几个人呢?掐指一算,对,招聘4个人吧:


这里我们,招聘了4个人,如下所示:

private static ExecutorService executorService = Executors.newFixedThreadPool(4);

人手多了,让我们来看看效果吧:

开始计时
好吃的煎饼果子勒
耗时:5 秒

是的,我们变快了~~~,只需要 5秒,我们投入了这么大的人力成本不就是要,客户第一,让顾客更加快速地买到煎饼果子么?

做点创新(换汤不换药)

上面的方式稍显老套,作为弄潮儿,最新的技术,搞搞搞,瞧,我们的透明后厨,简洁着呢:

通过上面的方式,真的好简洁,一套一套的,让我们来看看效果吧:

开始计时
好吃的煎饼果子勒
耗时:5 秒

额,竟然还是这样?

再进一步(成本优化/一个人当两个人用)

感觉这个人有点多,生意也不够好,还是换方式搞吧,某某某,你帮帮他吧,某某某,你也别闲着,把XXX也搞了。

不行,这样不行,一定要从根本上解决问题,对,我们来梳理一下。

开店成功需要几个要素?!

  1. 成本节约,在等待的时候,可以帮忙做点别的事情。
  2. 结构优化,效率提升,建立完整的上下行监管机制,消息顺达。
  3. 精简语义,使用专用词沟通,减少自然语言表意不明。
  4. 客户第一,提高服务质量,尽快返回结果,不要超出购买者的能够忍受的最长等待时间。

所以,对我们的例子使用Actor模型来进行建模,然后应用CQRS来减少语义。当然,这里我们的事件并没有持久化,算是部分实现。

  1. Actor模型 + Facade门面模式:


其中,使用 Ask 模式,我们接驳了传统的门店服务,以及基于Actor 模型的店员。而对于我们的店员,我们又使用了Actor模型以及监管机制,同时使用 CQRS 来对命令和事件进行分离。

其中我们有命令:


在我们的门店服务内部,将会对这些命令进行处理,并且产生相应的事件。

再和门店的接驳处,使用了Ask模式:

而对于老板/店长来说,他肯定是自己不处理的,所以他将任务,派发给了煎饼果子大侠,即:

注意,上面我们又一次使用了Ask模式,而非FSM。

这项任务就到达了我们的煎饼果子大侠了,他的任务可重了,因为他需要等待的东西有:

  1. 鸡蛋煎饼
  2. 洗好的生菜
  3. 撕开好的火腿肠

所以:我们的煎饼果子大侠会分别和,鸡蛋煎饼太郎、生菜小二哥以及火腿肠大叔形成依赖关系,并且会依赖于他们的结果,才可以做出一个完整的煎饼果子:

当收到老板的命令的时候,他将任务进行了拆解,分发给了和他合作的其他店员:


这里,我们搭配使用了Ask模式和Aggregator模式,将从各个部分收集到的结果,进行汇总,并产生了最终的美味的煎饼果子。

需要注意的是,我们在这里,并没有看到煎饼是怎么来的,而是直接看到的是鸡蛋煎饼,这就是DDD中的领域分层和依赖了:

我们的鸡蛋煎饼太郎和煎饼西施之间是一个强依赖关系:


这里,我们看到了一个奇怪的地方,即不对称的超时设置。因为太郎对西施特别好,所以顶住压力,不管怎么催他,他都会给西施说,别着急,慢慢来。

这就引发一个问题了,不正确的超时设置,可能让消费者非常不耐烦,本来消费者已经等了30s了,结果你对他说,哎呀,我们的鸡蛋煎饼太郎太忙了,然后顾客灰溜溜的走了,丢失了大量的潜在客户。

即,不合适的超时配置,会造成服务质量的下降。

让我们来把店开起来,并且提供服务吧

现在店开起来了,让我们来看一看运行结果吧。

开始计时
老板,煎饼果子来一个
大侠,做个煎饼果子
太郎,做鸡蛋煎饼
小二哥,切生菜
大叔,撕火腿肠
西施,做煎饼
太郎:我在做鸡蛋煎饼
大侠:我在开始做煎饼果子了
老板:大侠已经做好了啊?!
好吃的煎饼果子勒
耗时:5 秒

喔,完美的组织

架构和模式应用!当然,我们这里没有对Event进行持久化,这一点儿是不利于回溯的,同时也没有应用断路器模式,以及还有多处不合理的超时配置。

上面的这些方式,都让我们不难想,如何让客户更少的等待,如何提供更好的服务,如果我们的心更加大,如果我们要把店面做大,甚至要开连锁店,或者开煎饼果子工厂呢?

技术,不是给业务以限制,而是助力其想象。

Reactive-Stream/反应式流的方式

如果,我们想要将这个模式,复制到更多的场景,甚至开一条生产,N调生产线,如果我们要这些生产线能实现智能的调控,达到最小的资源占用,来达到最大的效力,那么我们应该怎么做呢?

对于这个问题,在2013年到2014年,业界也在思考,后来几经波折,想到了一种基于流的拓扑描述的方式。下面就让我们使用反应式流的方式,来实现上面的业务吧。

首先来一个不太清晰的例子,这个例子中,我们使用了Akka-Stream——一个ReactiveStream的实现。


在上图中,我们对抽象进行了下面几点改进:

  1. 我们的消费请求,被抽象为了一个流,这个流,类似于我们做的供给侧改革,使用消费请求,来指导我们的生产。
  2. 我们的店员,不再是基本的店员了,把他们想成持续提供煎饼,鸡蛋煎饼,生菜,和撕开的火腿肠,以及煎饼果子的流,即生产线。
  3. 我们可以动态地控制生产速度,如果消费者多,就在能力满足的情况下,尽量地多生产,在某项能力不能满足目前需求的情况下,就进行复制这些服务,对其进行复制,以提高更搞的生产力。比如,做煎饼是个比较缓慢的动作,那么我们可以再加一条生产线,这个生产线专门生产煎饼。
  4. 有了这个流,我们发现,煎饼的生产和鸡蛋煎饼的生产,总是强依赖的,那么我们可以将他们部署到临近的生产线,减少成本。
  5. 同上,我们发现鸡蛋煎饼和生菜以及火腿肠的产生,也是最终要进行合并使用,那么我们也可把这三种生产线,排布在一起,这样降低了将这三种材料,运输到煎饼果子大哥的时间。
  6. 如果我们发现煎饼果子大哥这个只做煎饼的过程很慢了,这个时候我们可以只在这个流程节点上,进行复制,从而加快煎饼果子的卷曲和打包过程。
  7. 我们如果一个发货窗口发不过来,我们可以多开两个门店/物流发货窗口。
  8. 我们可以使用类似于由仓库直接发货的方式,将生产好的热腾腾的煎饼果子,直接交给购买者,而不用通过我们的店长或者店面,在消息模式中,这叫做Forward模式。
    即,我们描述了依赖,而再有了这样的依赖之后,基于我们的需求和供应信息。我们可以处处都进行复制流程,复制处理节点,从而做到最优化地让热腾腾的煎饼果子到达用户的手里。
  9. 当然,如果我们实在是,实在是不能再扩展生产线了,那么我们就会进行回压,回压的时候,我们在最前面,就会让客户等待,或者在满足SLA的情况下,进行一些策略,但是,我们整体的服务质量,依然是那么得好,RS,就是双向的流,控制流,数据流。

即,类似于下面的结构:

我们将生产好的结果,直接递交给了消费者。

即,可以做到图中的任意一个方框内的结构拓扑,都是可以单独地进行复制,和调控的,甚至是智能地进行调控:)。智慧物流,我们也有智慧服务。

分别使用Akka-Stream、RxJava2以及Flux来实现

有了上面的实现,我们可以使用现在的一些语义化的工具来进行描述,比如:

好了,我们再看一种:

然后,我们再看一种:

然后仔细一下对比:

我们都需要煎饼Flow/Source,而且从煎饼Flow变成了鸡蛋煎饼Flow/Source

我们都还需要生菜Flow/Source,以及火腿肠Flow/Source。

我们从鸡蛋煎饼Flow/Source 、生菜Flow/Source以及火腿肠Flow/Source,构建了一条煎饼果子的Flow/Source。如果我们把Flow/Source看做生 产线,喔喔,我们根据三个现有的Flow/Source,构建了一条煎饼果子的Flow/Source。

最后,我们只从里面拿了一个煎饼果子出来:)

上面我们还有一点,就是异步和并发怎么控制?我如何并发动做一些事情呢?

或者

非常简单,如果我们想要同步的呢?去掉红框中的部分就好了。

也就是说,我们描绘了整个服务的编排,然后我们便可以方便地对任意特定的子拓扑进行优化了。开辟新的生产线,或者通过复制某个处理的过程来对某个流程进行并发执行,以提高其生产效率,当然在真的扩不了的情况下,保护我们的系统,保障我们SLA。

那么,我们如何不断地获取我们的美味煎饼果子呢?早上喜欢吃煎饼果子的人太多,都要疯掉了,好,请看:

或者

剩下的留作练习:)

只要我们的请求不断,我们的

就会持续的产生结果。多么简单直接,而且非常的优雅。复用这样的模式,开连锁店,开工厂,智能化的工业生产,人生巅峰不是梦。

拓展思考?

基于这个例子,我们可以看到,如果我们的服务代码,通过上面的方式来编写,那么势必更加地简洁优雅,而且我们也具备了更细腻的控制力,并且也有了更 高屋建瓴的拓扑、思维建模以及全局优化的可能。同时,我们的服务,都是由反应式流中流转的信号量进行驱动的,从而实现了动态的推拉结合——对,没错,控制 论中的知识:)

我相信,通过面向流的编程,从数据first,切换服务编排,请求/响应拓扑first的思维,将会大大地提高我们对链路的理解能力。同时,有了这 些工具,我们常见的反应式设计模式,都可以非常方便地应用,并完全可以结合FP以及DDD的一些思路,打造更加清晰、明了、性能优异的系统。而且,我们仅 仅描述了服务的编排,从而产生拓扑,而剩下的事情,只需要动动手指,也许手指都不需要动:),这一切,都将会有下一代的架构来智能地保证。

小结

从上面,大家已经看到了,我们的编程模式,是如何一步一步地从传统的方式,变成我们的Reactive 化的方式。通过Reactive 的方式,我们可以方便的实现服务编排,有了服务编排之后,我们可以做更多的事情,服务将会是更加智能化的,而非一成不变,提升了客户体验、资源利用率,并 降低了资源的浪费。

编辑于 2018-12-27

文章被以下专栏收录

    在所有编程语言和领域中尝试编写中文代码,开发相关工具,总结经验,一致代码风格。包括中文命名,汉化现有语言,创造中文语法的编程语言等等。作为最熟悉的母语,用来编写代码会让代码更容易被自己和母语相同的其他开发者理解。基于英文的编程语言和框架中,使用中文命名有时有技术问题。希望这里为后人趟雷,填坑。多数现有API是英文的,这里也会对其中一些常用的进行汉化。当然,这里也会对基于中文的编程语言进行探讨。包括汉化基于英文的编程语言,以及创造新的编程语言。