关于软件开发中问题解决的一个寓言

2020-10-16 19:49:56

这些年来,我给很多人讲过这个故事。大多是在喝醉的时候。人们的回答通常都很相似--欢笑、怀疑和一点点“如果不是上帝的恩典,我就会去”式的同情。我有很多人要求我把它写出来,所以我最终默许了。

这是一个寓言,讲的是当你总是在解决眼前的问题时会发生什么,而不是质疑这是否是你真正需要解决的问题。不幸的是,这大部分都是真的。我已经匿名和虚构了其中的一些内容,主要是为了保护无辜和有罪的人(偶尔也会让它成为一个更好的故事),但我在下面描述的事情有90%是真实发生的,其中90%或多或少发生了我所描述的事情(据我所知)。如果你认识我,你大概能想出是在哪里发生的。如果你不认识我,我就不会告诉你。

在我们故事的开始,我们只有一个中央API关闭,可能有十几个较小的应用程序和服务挂起。API控制我们的数据存储,以及您可以合理地对其执行的操作,并且通常封装了我们的模型。它和每个应用程序都驻留在自己的源代码管理repo中,并且是单独部署的。

此API是通过HTTP上的JSON-RPC实现的。不是很安静,但可能有点安静。也许是RESTISH。

挺管用的。它并不完美,但至少在功能上是模糊的。

与之对话的每个应用程序都编写了自己的客户端库(或者只是将原始HTTP调用直接包含在代码中)。

除了核心API之外,我们还有一个消息队列系统。相当不错。我们并不经常使用它--只是一些作业排队和发送给用户的通知--但它在这方面工作得很好。我们在客户端库方面遇到了一些问题,但它们很容易修复。

在某种程度上,我们中的一个人想到我们的HTTP API速度慢的原因当然是HTTP速度慢。因此,很明显,最好的解决方案是用基于RPC的热门新消息队列替换我们缓慢而糟糕的HTTP RPC。会出什么差错!

嗯,你知道,这并不是真的出了问题。它大体上起作用了。那是…。有点奇怪,但它基本上奏效了。我们编写了一个事件驱动的服务器,它实现了我们使用HTTP API所做的大部分工作(包括我们对ORM进行的所有阻塞调用)。哎呀)。它轮询消息队列,客户端将创建自己的消息队列来接收响应。然后,客户端将向服务器发送一条消息,服务器将在客户端的消息队列上回复(我认为消息中添加了一些标记,以确保一切井然有序。我希望有,否则这一切听起来都非常危险)。

这基本上是没有问题的。这甚至可能是对我们以前的系统的轻微改进。毕竟,消息队列上的RPC是一种合法的策略。我们当然没有任何基准,因为你为什么要以性能的名义对你所做的全面改变进行基准测试,但它至少不会明显比以前的系统差。

我们的下一个问题是我们在各地重新实现的各种客户端库。这显然很愚蠢。代码重用很好,不是吗?

因此,我们将它们合理化,将它们全部拉到自己的回购中,并生成了一个客户包。您可以通过在您的系统上安装它来使用它(这只是使用我们正在使用的打包系统的单个命令),然后您可以与API服务器对话。这已经够直截了当的了。

所以我们已经解决了重新实现的问题,而且我们至少声称我们已经解决了性能问题(甚至可能已经解决了)。在这么晚的阶段,我真的不能告诉你)。

事情是…。事实证明,针对这一点开发实际上相当令人恼火。

以前已经有点痛苦了,但是现在要添加一个特性,您必须执行以下所有步骤:

我们注意到客户机和服务器之间的许多代码无论如何都是重复的(毕竟两端的结构相似)。因此,我们最终将其通用化,并将客户端库与服务器放在repo中。显然,客户端库中并不需要所有的服务器代码,但只需将所有代码放在一个目录中,并使用一个标志来测试您是在客户端模式还是在服务器模式下运行,会容易得多。因此,现在至少您必须对客户端和服务器进行的所有更改都在一个repo中,而且它们甚至可能是相同的代码。

目前我们这里有一个稍微有点巴洛克风格的建筑,但从根本上说,它并不比你在野外遇到的许多建筑糟糕得多。不是很好,但是从外面看你可以看出我们是从哪里来的。接下来的是一切开始完全进入茶话会的时刻。

特别是数据模型复制。如果您有一个Kitten模型,那么在客户端和服务器中都需要一个Kitten模型,并且您需要同时维护这两个模型。这真是一件令人讨厌的事。

在这一点上,一些聪明的火花(我发誓不是我)意识到了一些事情:我们的ORM支持高度可插拔的后端。它们甚至不需要是SQL--有人将其用于文档存储数据库,甚至RESTAPI。我们有这个API服务器,为什么不把它作为ORM后端呢?

如果我们做到了这一点,我们能以一种重用我们已经使用的模型的方式来做这件事吗?我们已经在检测我们是在客户机模式还是在服务器模式下运行,难道我们不能让它在这两种情况下使用不同的后端吗?

当然,拥有ORM的真正好处是如何链接事物和构建丰富的查询。因此,我们确实希望支持ORM的全部查询语法。

一个周末的咖啡因促进了这个人的发育。后来,我们都在周一早上到达,发现了一个宏伟的新愿景。下面是它的工作原理:

如果我们处于客户端模式,则我们的后端使用JSON-RPC服务器,而不是与数据库对话。

给定一个查询对象,我们对服务器上相应的后端方法执行JSON RPC调用。这将返回一组模型

我们将此查询对象传递给自定义JSON序列化程序,该序列化程序必须支持所有查询子类型。

我们的服务器从消息队列中弹出JSON,对其进行反序列化,然后调用自定义方法来构建查询对象。

无论如何,我们在周一早上到达时会发现这一切都已就位,而且大体上是有效的(“只有几个细节需要润色”)。

还有,你知道吗?我们决定顺其自然。我们对现状感到非常恼火,这显然会让我们的生活变得更容易-当我们想要添加功能时,需要编写的代码要少得多,而且我们确实需要添加功能。所以,虽然我们可能有点怀疑,但我们还是决定放过这件事。

当然是…了。你看到那边那条长长的管道了吗?活动部件很多,不是吗?很多都是我们写的定制的废话。我打赌那会坏掉的,你说呢?

很自然地,就像发生的那样,这里的抢劫犯就是负责修复这些错误的人(这是怎么发生的?我不知道。我认为问题在于,当志愿者的号召到来时,我没有足够快地后退。或者,也许人们有一种不可思议的发现技巧,尽管我尽了最大努力假装自己不是,但实际上我还是相当擅长这一点)。

最常见的错误来源之一是用户错误。具体地说,我们的设置使用户错误变得非常容易。

将代码更改推送到您的应用程序需要三个步骤:您必须重新启动服务器,您必须安装软件包,您必须重新启动您的应用程序。如果您忘记了这三个步骤中的任何一个,您的客户端和服务器代码将不同步(请记住其中有多少是共享的),并且所产生的错误将是微妙而令人困惑的。这经常让人感到绝望。