软件设计是怎样炼成的——Gregory T. Brown

一、谨记自底向上,优化软件设计

假设你是一门软件设计课程的客座讲师,并且你希望缩小理论与实践的差距

这门课程是你的朋友 Nasir 开设的,但他目前的教学效果不太理想。于是,他请你来帮忙。

在进行案例分析的时候,Nasir 的学生很容易就能抓住要点,并能提出富有创意的问题,从而引出精彩的讨论。但是一旦涉及在自己的项目中运用设计概念,大部分学生都很难把理论和实践联系起来。

目前的问题是,大部分学生并没有构建软件系统的实战经验。这使得学生把软件设计看成是抽象的练习,而不是具象且必要的技能。

课本上的例子强化了自顶向下的软件设计方式,其中的设计理念出现得很突兀。真正的设计不同于此,但是学生经常照葫芦画瓢,进而产生气馁情绪。

为了揭示设计决策的来龙去脉,你将搭建一个小型的实时项目,并在此过程中与学生进行讨论。通过这样的形式,学生将有机会参与迭代设计过程,发挥主动性,一砖一瓦地完成系统的构建。

1. 找出关键词,认清问题

在第一堂课上,Nasir 简单地介绍了你将要搭建的系统:一个即时制生产工作流的小型仿真。

Nasir 没有用理论引入这个主题,而是描述了即时配送如何使 上购物变得更加可行。

  • 当顾客从 上购买商品后,只要其住处与出货点的距离不超过 160 公里,货物一般都可以在一天内送达,最多也不超过两天。

  • 地方仓库的库存量会在防止产品脱销的前提下保持最低水平。补货会持续进行:每当地方仓库向顾客卖出一件商品,紧接着就会有相应的订单发往更大的仓储中心,以便及时为此仓库补货。

  • 仓储中心到地方仓库的货流是持续不断的,所以任何一个需要补充的物品可以立即被扔到卡车、飞机或火车上,前往目的地。

  • 一旦货物从仓储中心运抵地方仓库,补货订单便会自动提交至第三方供应商。很多供应商会使用即时制生产工作流,这样就可以进行小批量的补货。

  • 虽然整个订货流程从头至尾可能会需要几周的时间,但由于有效设置了货流,顾客能够从离其最近的地方仓库接收商品,而且仓库永远都有库存。同时,制造商提供的产品数量和实际卖出的数量大致相同。

在这一模式下,货物即时流向需要它们的地方,这样能使整个生产系统中的浪费和等待时间最小化。这种工作方式在现在已经很常见了,但在几十年前却被看作开创性的行业创新。

Nasir 花了一点时间进行介绍,然后示意你开始上课。为了跟上节奏,你给学生讲了一个小趣闻,以此引入今天所讲内容的几个细节。

:我父亲一生都工作在流水线上,见证了他所在的公司从大批量生产到即时制工作流的变迁过程。

学生:那变化一定很大!好像是完全不同的两种工作方式。

:对,就是这样。公司在业务层面经历了巨大变迁,但生产层面几乎没什么变化。

公司转型之前,器件要一箱一箱地从上游供应方运过来,然后由工人进行加工,再被运往生产线下游。

当公司转型为即时制生产后,这一过程几乎和以前一样——只有一个很小的变化。工作流转向了:只有当空箱子从下游返回时,才会加工新的器件。

学生:所以换句话说,你父亲只有在下一个站点需要货物时才会开始工作/p>

:没错!从单个站点看不出什么变化,但是整个系统从一开始最简单的部件到最后的成品都被串了起来。

以客户订单为准,从后往前进行生产。整条生产线只需要通过保持相邻站点一致,就能确定需要生产多少元件,以及何时生产。

这种过程深深吸引着我,因为它很有趣,看似简单的基础单元也具有实时性需要。出于这个原因,我觉得从无到有地对这种行为进行建模会很有意思,而且在此过程中我们也可以讨论一些有趣的软件设计原则。

Nasir 问学生是否通过刚才的故事理解了即时制生产工作流并已做好仿真的准备。学生尴尬地笑了,好像不知道他到底是在开玩笑还是认真的。但他随即又问了一个更清楚的问题:这个故事中的关键词是什么/p>

过了好几分钟,学生终于找出了和仿真相关的很多关键词,如器件(widget)、箱子(crate)、供应方(supplier)、订单(order),以及生产(produce)。

然后,你让学生从这些词中选出两个,组成一个比较容易实现的简单句子。沉思了一会儿后,其中一个学生喊出了他的建议:

“我知道了!我们来做一个箱子,然后往里面放一个器件!”

从这点着手很好,你谢过了那名学生,然后开始工作。

2. 从实现最小化功能入手

开始演示时,你准备了几个极小的 UI 元素,包含的全都是简单的几何图形。你梳理了几个基本逻辑,同时学生看着你工作。

几分钟之内,你就在屏幕上做出一个小矩形,其中有一个圆形,初步代表“箱子中的器件”。

当你按下笔记本电脑上的空格键时,圆形消失了。再次按下时,圆形又出现了。唯恐学生不理解,你重复演示了好几遍……

再次集中注意力后,你列了一个图表,用以描述对象 的 API。

一个学生问对象 的意义是什么。如果让 直接操作 ,不是更好吗/p>

这个问题问得很好,尤其是在项目的早期开发阶段。在设计中引入的任何对象都会增加概念包袱,所以无疑需要避免引入多余的对象。

但在此例中,如果不为 建模,就很难区分系统的物理行为和逻辑行为。

在真实的生产车间里,上游的供应方直接将材料装入箱子,让人觉得好像 才是需要操作的对象。然而,箱子本身只是容器,其传达的信息不过是容量。

关于箱子目的地的真实信息可能存储在工人脑中,也可能打印在一张纸上或箱子外的标签上。这些信息就是 所代表的内容。这个对象很容易被忽略,因为它并不像箱子中进进出出的材料那么明显,但无论如何它仍是模型的一部分。

总结完关于 的全部问题之后,你开始实现填充箱子的工作流。过了一小会儿,你的仿真中又增加了一个三角形和一条线,而且你已经准备好要讲一堂基础几何课了。

这个很小但很重要的改动使你的模型可以支持软件设计中的 3 个至关重要的量:0、1 和“许多”。{1[这被称为软件设计的 0-1-无穷规则(Zero-One-Infinity Rule),由 Willem van der Poel 提出。]} 前面的例子只涉及前两种情况,但是从现在开始,你需要考虑所有情况。

由于已经建立了箱子填充机制,因此你需要实现的便是,一有物品从箱子里移出,就自动触发填充动作。你问学生如何实现这一功能,一名学生建议在调用 后立即调用 。

于是,你按照学生的建议做了细微的变更,然后启动了仿真器。一个被装满的箱子出现了。你告诉学生,已经按照他们的建议设置好了空格键。随后,你按下空格键,屏幕上没有任何变化。你又按了一下,还是没有任何变化。然后你狂按键盘,屏幕闪了一下,但那个被装满的箱子仍然没有变化。

你加了几处日志记录代码,以确定仿真器能接收键盘输入, 和 被成功调用,以及没有产生死循环或递归调用等。看起来一切正常。你注释掉 那一行,又按了几下空格键,器件被一个一个地移除了。你从空箱子开始,注释掉 调用,然后器件又一个一个地填满了箱子。

Nasir 问学生是否知道哪里出了问题。一名学生很快指出,对器件的移除操作和填充操作发生在同一帧里。因为两个动作之间没有间隔,所以箱子看起来没有任何变化。

为了验证此猜想,你暂时给生产出的器件随机配了颜色。虽然演示结果乱七八糟,但它很好地证实了学生的猜想。

:现在我们已经知道哪里出了问题,那么怎样修复呢生:在产出新的器件之前,让 暂停一秒,如何/p>

:这个想法很好,但我们现在的编程环境是异步的,所以并没有直接让进程休眠的方法。需要设置某种回调函数,令其在预设好的延迟之后执行。

学生:好的,那就这样做吧。

:我会的,但是没那么容易。目前,调用 会立即触发对 的调用,后者会返回一个 对象。该对象随即会被送入 。如果在 中使用异步的回调函数,就没法得到有效的返回值,这样整条供应链就会断掉。

Nasir:所以现在的情况是典型的时间耦合。在 、 和 这几个对象之间,存在着时间依赖,这是由它们的设计方式导致的。如果要彻底解决这个问题,就需要重新设计,但现在暂时可以延迟订单提交进程,让它在系统接收到键盘输入之后一秒左右再运行。

你根据 Nasir 的建议做了修改,然后又试了一次。果然,刚一按下空格键,就有一个器件从箱子中消失了。过了大约一秒钟,箱子才被再次填充。随后,你连续快速移除 3 个器件,把箱子清空。过了一会儿,箱子又满了,而且所有新器件都几乎同时出现在箱子中。

看到系统正常工作,学生都很高兴。但你马上提醒他们,这样做治标不治本,其实有点投机取巧。为了使一切正常,需要改良工作流。

你绘制了一张顺序图,用来描述当有订单提交时,系统中的新事件流。

Nasir 想让学生解释一下怎样实现这一新模型,但看起来他们都被这问题难住了。你思考了一会儿,寻找其中的原因。你发现学生的注意力都集中在寻找新系统和之前有什么不同上,所以他们看不到二者的相同点。

你降低了要求,让学生考虑如何使用他们已经熟悉的组件实现一个简化的系统。

重构之后,对象 没剩多少代码了,所以不能把它当作基类。你从剩余部分中复制并粘贴了一些有用的代码,然后开始实现对象 。

你首先加入了几个基本功能,使机器和上游箱子联系起来,这部分进展顺利。但此后的工作就变得有些复杂了:需要对 进行一些调整,以支持新添加的 结构。

你最终做的变更并不大,但很有代表性。当对象在与其设计初衷不同的场景中被复用时,经常需要这样的变更。

  • 在只包含一个供应方和一个箱子的场景中,知道箱子是否空着并不重要。只要在器件从箱子中被移除时,能够立即提交填充订单即可。但机器只有在其上游箱子都装有器件时才能完成订单,所以你实现了方法 来获取此信息。

  • 每个订单对象都会引用一个箱子对象,但反过来则不会。 和与之对应的 都已定义,因此系统在顶层运行良好。但在其中引入机器时,一切就变得乱七八糟了。为了让机器在消耗上游器件的同时提交填充订单,你用了一个涉及闭包的技巧,但解释起来既不简洁也不容易。{2[对这个问题的正确处理方法应该是回过头,在对象 中给特定的 加引用,但假设客座讲师的时间非常有限,你不想再去仔细考虑设计决策了。而这时出现了一种补救方法,可以掩盖种种细节,让学生能够将注意力集中在更重要的知识点上。]}

你坦言,这种意外出现在对象连接点上的设计瑕疵,其实正是自底向上进行设计的缺点。但为了让大家重拾乐观情绪,你给学生展示了机器的一个可用版本,其订单数量可以实时更新。

刚开始,Nasir 以为你已经预料到学生可能会问这种问题,所以提前写好了部分代码,但你很快指出并非如此。

实际上,这和你对合并器的定义有关:这种机器会从它的每一个供应箱中获取一个器件,然后输出一个器件。

根据这一定义,很容易实现纯化器(即只有一个供应箱的合并器)。因此,你可以很快实现这一新特性,而不用写任何新代码。

另一名学生甚至提出了更深一层的建议。他认为可以创建一种新机器,使其和对象 的工作方式一样,即没有供应箱,因为任何集合与空集取并,其结果总为该集合本身。

这个提议令你很吃惊,因为你在创建对象 时,从来没想过这个问题。但是果然,这个想法行得通!

声明:本站部分文章及图片源自用户投稿,如本站任何资料有侵权请您尽早请联系jinwei@zod.com.cn进行处理,非常感谢!

上一篇 2018年8月26日
下一篇 2018年8月26日

相关推荐