9.4 末日金字塔

读者已经看到了如何使用回调和事件编写非阻塞的并发代码,但是我还没提起房间里的大象。如果编写代码时使用了大量的回调,代码会变得难于阅读,即便使用了Lambda表达式也是如此。让我们通过一个具体例子来更好地理解这个问题。

在编写聊天程序服务器端代码时,我写了很多测试,从客户端的角度描述了verticle对象的行为。代码如例9-7中的messageFriend测试所示:

例9-7 检测聊天服务器上两个朋友是否能发消息的测试

  1. @Test
  2. public void messageFriend() {
  3. withModule(() -> {
  4. withConnection(richard -> {
  5. richard.dataHandler(data -> {
  6. assertEquals("bob>oh its you!", data.toString());
  7. moduleTestComplete();
  8. });
  9. richard.write("richard\n");
  10. withConnection(bob -> {
  11. bob.dataHandler(data -> {
  12. assertEquals("richard>hai", data.toString());
  13. bob.write("richard<oh its you!");
  14. });
  15. bob.write("bob\n");
  16. vertx.setTimer(6, id -> richard.write("bob<hai"));
  17. });
  18. });
  19. });
  20. }

我连上两个客户端,分别是Richard和Bob,Richard对Bob说“嗨”,Bob回答“哦,是你啊”。我已经将建立连接的通用代码重构,即使这样,读者依然会注意到那些嵌套的回调形成了一个末日金字塔。代码不断地向屏幕右方挤过去,就像一座金字塔。(别看我,这名字又不是我起的!)这是一个众所周知的反模式,让代码难于阅读和理解。同时,将代码的逻辑分散在了多个方法里。

上一章我们讨论过如何通过将一个Lambda表达式传给with方法的方式来管理资源。读者会注意到,在测试代码中我多次用到了该方法。withModule方法部署Vert.x模块,运行一些代码然后关闭模块。还有一个withConnection方法连接到ChatVerticle,使用完毕后关掉连接。

这里使用with方法,而不使用try-with-resources的方式,好处是它符合本章我们使用的非阻塞线程模型。我们可以重构代码,让它变得易于理解,如例9-8所示。

例9-8 分成多个方法后的测试代码,测试聊天服务器上两个朋友是否能发消息

  1. @Test
  2. public void canMessageFriend() {
  3. withModule(this::messageFriendWithModule);
  4. }
  5. private void messageFriendWithModule() {
  6. withConnection(richard -> {
  7. checkBobReplies(richard);
  8. richard.write("richard\n");
  9. messageBob(richard);
  10. });
  11. }
  12. private void messageBob(NetSocket richard) {
  13. withConnection(messageBobWithConnection(richard));
  14. }
  15. private Handler<NetSocket> messageBobWithConnection(NetSocket richard) {
  16. return bob -> {
  17. checkRichardMessagedYou(bob);
  18. bob.write("bob\n");
  19. vertx.setTimer(6, id -> richard.write("bob<hai"));
  20. };
  21. }
  22. private void checkRichardMessagedYou(NetSocket bob) {
  23. bob.dataHandler(data -> {
  24. assertEquals("richard>hai", data.toString());
  25. bob.write("richard<oh its you!");
  26. });
  27. }
  28. private void checkBobReplies(NetSocket richard) {
  29. richard.dataHandler(data -> {
  30. assertEquals("bob>oh its you!", data.toString());
  31. moduleTestComplete();
  32. });
  33. }

例9-8中的重构将测试逻辑分散在了多个方法里,解决了末日金字塔问题。不再是一个方法只能有一个功能,我们将一个功能分散在了多个方法里!代码还是难于阅读,不过这次换了一个方式。

想要链接或组合的操作越多,问题就会越严重,我们需要一个更好的解决方案。