撰文 | Krste Šižgorić
翻译 | 藏痴
审校 | Nuor
代码结构之间的关联是随时间推移逐渐形成的,因为我们将不同的部分视为整体的一部分,在实际操作中,我们应尽量避免这样做。
最近几个月有很多工作需要做,我过得比较艰难,需要休息一下。我的放松方式是阅读,我选择了刘慈欣的《三体》。开始阅读之前,我从未了解过这本书,也不了解三体问题,但是读完之后我震惊了。《三体》是一本科幻小说,是地球往事三部曲中的第一部。这本书构造了一个三体问题——经典力学中最复杂的问题之一,并围绕它讲述了一个故事。所以让我,在不破坏最初故事的前提下,用我自己的方式做同样的事情。
三体问题
为了解释三体问题以及它和软件开发的联系,让我从单体问题开始解释。单体问题更常被称为有心力问题(the central-force problem)。有心力问题试图决定一个受到中心力作用的粒子的运动状态,这个有心力的力源位置固定。更直白地讲,恒星可以视为静止的。行星的运动可以用三角函数表示。
考虑稍微复杂一点的情况,让我们想象两个有质量的物体通过引力互相影响彼此的运动状态,也就是二体问题。这可以被用来描述地球和月球围绕对方做的运动。或者举一个更好的例子,冥王星和冥卫一,就像下面动图描述的一样。
冥王星—冥卫一系统的侧视图,显示出冥王星绕着它外面的一点公转丨来源:维基百科
对于包括引力在内的许多种力来说,广义的二体问题可以被转化成两个单体问题,因此二体问题可以完全求解。因此,二体问题也存在着对应的解决方案。
但如果我们再加入一个有质量的物体,将整个系统转变为三体问题,那么事情就变得不可预测起来,常常陷入混乱。对于绝大多数初始条件,三体问题不像单体或二体问题那样有一般的封闭解。
软件开发中n体问题的建立
三体问题与软件开发有什么关系呢?事实上并无关系。但如果思考这两件事,我们可以发现它们有相似之处。彼此影响的强耦合的功能与极弱耦合功能可以在同一个系统中和平共存,而不会迫使其中之一发生改变。让我们将软件开发与n体问题做一个比较。
最开始事情很简单而且易于理解。我们有一个主角,也就是一个中心,其他所有事情都围绕它展开。软件功能不多,不会彼此冲突。
比如,我们打算开发一个库存管理应用。我们需要做的只是插入新条目、增删数量以及了解库存状态。所以我们完成了这些功能。
过了一段时间我们需要添加新东西。最好能开一家网店来线上售卖产品。因此我们开始为库存管理软件添加新功能。
首先,要添加一个网页。我们获取包含可用数量的库存状态。现在这个网页需要描述可用库存的状态,但这并不意味着这些原本可用的商品不在库存中了,它们只是换了一种状态。所以我们需要在库存中设置一种新状态。我们需要掌握“现货”状态的商品数量,以及“可售”状态的商品数量。
但是现在需要更改为网店获取可用数量的操作,以反映这一变化。如果我们不能卖掉它们,那么仓库里到底有多少商品其实并不重要。我们只想在网店展示“现货”数量。我们需要再次修改网店。
系统中一部分物体的重力吸引其他部分改变运动状态。这两部分之间存在竞争,直到二者达到稳定状态。一旦我们优化了功能,事情就会回到最初可预测的状态。所有事情都按计划运行,这让我们很高兴。我们仍然可相对容易地预测接下来会发生什么,并知道一个部分的改变会如何影响其他部分。
两个质量有“微小”差别的物体绕共同的质心运动丨来源:维基百科但事情可以被优化。我们可以为顾客提供快递服务。因此我们检查了现有的系统,并在每一步都做出改变。快递服务改变了库存,库存改变了网站设计,网站又反过来改变了快递服务,快递服务改变了库存……
快递服务扩展了商业贸易。一个仓库不再能满足需求,我们希望在更多地方发展生意,并且系统需要能支持这种工作方式。但这将如何影响现有的系统?库存需要调整以适应多个地点。而由于网站会减少库存的商品数量,它也需要被改造以支持多个地点。但怎么做到这些呢?这又要求库存和快递服务也做出改变……好混乱啊。
位于不等边三角形顶点的三个初始速度为零的相同物体的近似轨迹丨来源:维基百科
回到单体问题
我们该如何避免这个问题?我们有怎样才能避免某一个功能对其他功能产生严重影响?
太阳系有许多质量足够大的行星,它们可以影响彼此的运动状态。然而,如果我们试图预测地球围绕太阳公转的轨道,我们完全可以忽略所有行星而只关注太阳和地球。这会给我们足够好的关于实际运动的初始近似。对于木星和其他任何行星轨道的预测也是一样。
如果我们将软件系统的两个功能解耦,我们就可以像太阳系中的行星一样处理它们。一个行星的重力不足以影响另一个行星的轨道。虽然它们确实仍然互相影响,但这些影响带来的改变不会十分明显,一些情况下甚至不存在。
设想如果我们试图计算未来十年复活节的日期。复活节是一个基督教节日,在每年春分后第一个月圆夜后的第一个周日。当我们计算这些日期时,我们真的在意木星的79个卫星吗?当然不,我们也不必这么做。
我们把软件开发的解决方案分解成许多小部分,让每一部分都围绕着“太阳”运行。这里的“太阳”可以是信息中介、服务总线、或者只是已经建立好的契约(接口)。我们决定我们的“太阳系”需要多大程度的解耦。环绕太阳运动的小部件是模块、域还是微服务并不重要,重要的是部件之间要尽可能地独立。这会让它们更容易被理解。用这种方式计算复活节日期甚至不需要知道木星有79个卫星。
月亮显著地影响了地球的轨道,这是事实。如果我们关心太阳和地球的关系,那么我们并不是在谈论地球和月亮环绕太阳的轨道,我们只是在谈论地球的轨道。无论一个功能多么复杂(比如木星有79个卫星),在整个系统(比如太阳系)中我们只需要将它们视为一个整体。
用这种方法我们并不需要处理太阳系中(大约)一百二十万个天体,也不需要处理大约700个行星、小行星或卫星。我们只考虑八大行星。因为一般而言,当我们谈论太阳系时,我们只关心这八大行星。尽管这样计算结果并不完美,但对我们来说已经足够精确,不会在工作中带来问题。
简化问题
当我们考虑一个仓库的库存,我们到底在考虑什么?或许是一个大的仓库和里面存储的许多货品。仓储的工作是什么?为了存储商品直到被它们被卖出。我们的软件系统应该只考虑这些功能。
网店应该只负责展示商品并创建购买。但网店中的购买需要改变库存状态。所以如何解决这个问题?现在有很多解决方法,但请恕我直言。
购买已经是一个足够复杂的功能了。它获取订单,检查库存以查看是否有可购买的商品,执行付款并创建运输。这可能看起来只是另一个功能,但由于它的大小,它可以被很容易地分成单独的部分。
我们为库存创建严格的合同,根据合同我们可以得到所有货物的清单、所有可用货物的清单,可以检查这些货物是否可以售卖并减少它们的数量。网店只需知道可用的货物。如果我们决定在某一点上支持软删除,或多个状态,就像我们在上面的例子中做的那样,网店并不需要知道这些变化。只需更改网店收到的数据,就可以在网店不知情的情况下完成上述操作。
对于“购买”这一功能,我们需要做相同的事。合同需要一个操作以完成购买。网店发起这一操作,然后它的任务就完成了。“购买”这一功能接管。它检查是否有可用的货品,然后如果一切正常,它完成购买并相应地减掉库存商品数量。
纵观整个系统,我们已经清晰地分开了所有可以独立存在的功能(一些而不是另一些)。我们是从库存开始的,所以它当然有自己的系统。接下来我们添加了“网店”和“购买”两个可以独立实现的功能。
我们确实有快递服务,但目前为止整个流程并不需要知道它的存在。所以我们也应该将其视为一个独立系统。我们不应该把它强行加入已有的系统中并让它们相匹配。
好了。现在我们还没有一个有着许多有复杂依赖性的功能的系统。我们有许多子系统,每一个子系统都有特定的复杂度,它们共同构成了一个相容并可持续的解决方案。
结论
建立、维护并扩展软件是一件复杂的事。最开始可能看起来很容易。“只是添加这个功能而已。”但是我们添加的东西越多,方程就变得越复杂。如果我们想强行加入太多东西,最终我们会发现自己陷入了一个无法解决的问题中。而二者之间只有一线之隔。
对于多体问题,我们可以尽量简化系统,把它分成许多小部分。有很多人都可以来解决这些小的部分,比如小学生都可以解决单体问题。三体问题看起来无解,然而,这两类问题之间的差异乍一看却很小,因此可以利用软件设计的思路,尝试化整为零,对问题做一些简化近似。
本文经授权转载自微信公众号“中科院物理所”,原标题为《三体,但是写软件》。
原文链接:
https://krste-sizgoric.medium.com/the-three-body-problem-in-software-development-74adeda6807c
0
推荐