9.4 末日金字塔
读者已经看到了如何使用回调和事件编写非阻塞的并发代码,但是我还没提起房间里的大象。如果编写代码时使用了大量的回调,代码会变得难于阅读,即便使用了Lambda表达式也是如此。让我们通过一个具体例子来更好地理解这个问题。
在编写聊天程序服务器端代码时,我写了很多测试,从客户端的角度描述了verticle对象的行为。代码如例9-7中的messageFriend测试所示:
例9-7 检测聊天服务器上两个朋友是否能发消息的测试
@Testpublic void messageFriend() {withModule(() -> {withConnection(richard -> {richard.dataHandler(data -> {assertEquals("bob>oh its you!", data.toString());moduleTestComplete();});richard.write("richard\n");withConnection(bob -> {bob.dataHandler(data -> {assertEquals("richard>hai", data.toString());bob.write("richard<oh its you!");});bob.write("bob\n");vertx.setTimer(6, id -> richard.write("bob<hai"));});});});}
我连上两个客户端,分别是Richard和Bob,Richard对Bob说“嗨”,Bob回答“哦,是你啊”。我已经将建立连接的通用代码重构,即使这样,读者依然会注意到那些嵌套的回调形成了一个末日金字塔。代码不断地向屏幕右方挤过去,就像一座金字塔。(别看我,这名字又不是我起的!)这是一个众所周知的反模式,让代码难于阅读和理解。同时,将代码的逻辑分散在了多个方法里。
上一章我们讨论过如何通过将一个Lambda表达式传给with方法的方式来管理资源。读者会注意到,在测试代码中我多次用到了该方法。withModule方法部署Vert.x模块,运行一些代码然后关闭模块。还有一个withConnection方法连接到ChatVerticle,使用完毕后关掉连接。
这里使用with方法,而不使用try-with-resources的方式,好处是它符合本章我们使用的非阻塞线程模型。我们可以重构代码,让它变得易于理解,如例9-8所示。
例9-8 分成多个方法后的测试代码,测试聊天服务器上两个朋友是否能发消息
@Testpublic void canMessageFriend() {withModule(this::messageFriendWithModule);}private void messageFriendWithModule() {withConnection(richard -> {checkBobReplies(richard);richard.write("richard\n");messageBob(richard);});}private void messageBob(NetSocket richard) {withConnection(messageBobWithConnection(richard));}private Handler<NetSocket> messageBobWithConnection(NetSocket richard) {return bob -> {checkRichardMessagedYou(bob);bob.write("bob\n");vertx.setTimer(6, id -> richard.write("bob<hai"));};}private void checkRichardMessagedYou(NetSocket bob) {bob.dataHandler(data -> {assertEquals("richard>hai", data.toString());bob.write("richard<oh its you!");});}private void checkBobReplies(NetSocket richard) {richard.dataHandler(data -> {assertEquals("bob>oh its you!", data.toString());moduleTestComplete();});}
例9-8中的重构将测试逻辑分散在了多个方法里,解决了末日金字塔问题。不再是一个方法只能有一个功能,我们将一个功能分散在了多个方法里!代码还是难于阅读,不过这次换了一个方式。
想要链接或组合的操作越多,问题就会越严重,我们需要一个更好的解决方案。
