如何阻止软件退化?
从软件退化说起
软件开发行业,尤其是互联网相关行业中,软件工程的首要职责是对于利益点、风口业务的支持与落地。而这两者都是有着与时间相关的属性的。也就是说,同样的一件事,如果没有在正确的时间内做到,那可能也就没有意义了。所以软件行业是一个争分夺秒的行业。
那么当真的风口到来时,软件工程的开发是否能快速跟上目标呢?希望总是好的,但是现实是很骨感的。当新的目标出现的时候,你会发现总有那么多的额外逻辑拖慢了脚步。
项目工程什么时候是最优秀的时候呢?如果你是一个认真负责的开发人员的话,那么这个答案理应是当这个软件最开始设计出来的时候。这时候的软件是完美的符合功能预期甚至还能满足开发人员对其未来扩展的幻想(很有可能不能成真)。而更现实的是,在进行之后的功能迭代的时候,常常会和原有设计冲突,那么在这种情况下完成需求后,软件的整体质量往往是会下降的。而在周而复始的循环里,团队会花费大量成本进行对业务的维护,最终会在一次迭代中发现,这个软件如果不重构就再也维护不下去了。
举一个订单支付的场景,第一次的需求可能是这样的:
而在第二次的版本迭代时我们需要加入对于不同折扣方式的处理,可能就变成了:
然后第三次变更的时候可能对用户人员以及支付方式加入判断:
尽管是为代码,但可以看到代码量已经比原有的逻辑多了很多。当然这肯定还没有结束,比如各种其他的消费活动:预售、秒杀、拼团、返券等等。随着业务逻辑增加,代码不可避免地会膨胀,变得更加难以维护。
从软件的本质说起
随着业务迭代,代码的质量降低了,但是这是为什么呢。这就不得不从软件的本质来说起。
软件的本质是在代码中模拟客观世界的交互。
所以软件逻辑是否正确,就可以通过其交互是否与客观世界一致来判断。
以第一节为例子我们就不难理解,假如你在餐厅点餐,无论你是用的小程序 app 来操作,还是服务员直接为你点餐,遵循的都是上面的伪代码流程。所以抛开代码规范不谈,上述支付逻辑与客观世界一致的。
如果在第一次进行系统开发的时候就已经知道了最终的这个客观逻辑的存在,那么在进行系统设计的时候就不会那么被动了。因为尽管需求方、客户可能没有发现这些客观规律,但是如果识别出来了对应的在客观世界中的逻辑,那么我们就可以提前为可能存在的业务逻辑进行设计。对于上一节中的支付场景来说的话,就是在一开始就识别出会员、折扣、支付路由这三个可能的扩展点。
那我们是不是直接按照客观世界来开发?
必然是否定的,因为他太复杂的。 因为太复杂所以无法直接输出。
所以我们软件开发实际上是在对客观世界进行抽象。提取出来简单、清晰、易于理解的逻辑。就如同第一版本的支付流程。但是随着系统的上线,更多地看起来不那么直观的需求暴露了出来,他们没有没有那么明确,但是却补充了客观世界的逻辑,满足了用户的诉求,这些功能逻辑会让软件变得更加负责,但是却会让用户感觉软件更加好用。
如果用是否接近客观世界来判断软件是好用还是不好用的话,那么软件的演进方向就可以认为是:“难用、清晰”到“好用、复杂”的转变。
从简单到复杂
复杂的软件也可以有很优秀的设计。各种大型项目本身也是有先例的。但是为了支撑起复杂的业务,必然的需要使用各种设计模式以及架构原则对系统进行抽象提取。但反过来说对一个 add()
方法中的a+b
直接进行抽取,把符号运算与加数和被加数都进行抽象(实际上正交设计正是如此),不是说这种设计方式不可以,但是如果对于当前可预知的内容外做太多额外的逻辑设计的话,不免有过度设计之嫌。
过度设计的问题在于:通过经验或者参考过往方案进行的提前设计,如果预期未能兑现,则会造成额外的成本损失。
所以,在第一小节中第一次的支付需求设计的流程并无问题。简单的问题应该简单设计,复杂的问题再进行复杂抽象。设计本身没有问题,那么问题来自后续的迭代。
先说一个显而易见的结论:我们应该随着程序业务逻辑变得复杂时逐步地调整程序结构,让其从简单程序结果变成复杂程序结构。
但实际上这个并不这么容易实现,就如同一开始的支付逻辑一样。因为在实际业务中没有一个明确的边界告诉你:你的软件已经足够复杂了,需要调整程序结构了。那么在各种工期时间的安排下,人们自然而然地就更倾向于在原有简单结构的代码基础上进行业务逻辑调整。而这样最后的后果就是:当发现程序已经足够复杂的时候却发现为时已晚。
这个时候我们就可以发现,软件退化的根源并不是开头说的:需求变更。需求变更只是表象的诱因,而根据上文我们也可以发现,软件退化的根源是:
在业务逻辑变得复杂的同时,没有根据业务重新调整程序结构。
所以,解决软件退化也就呼之欲出:
要保持软件设计质量不退化,必须要在每次需求变更的时候,根据变更点调整原有程序的设计结构。
如何调整
实际上,如果对于一段业务逻辑的设计,我们有各种不同的设计原则、策略模式可以参考。如果让你单独的新设计一个需要支持 5、6 中优惠的业务逻辑的话,我们可以直接套用策略模式来对业务进行封装。这样就可以让这部分的业务块满足开发-封闭原则(OCP)即:可以扩展新的优惠模式,同时又可以保证每次调整业务代码只涉及其中的一个类文件。
新的逻辑我们容易想得明白,但是如果对于已有逻辑的改造又该如何来呢。直接在原有逻辑上修改,就又无法满足 OCP 原则了,难道 OCP 原则在这种情况不适用吗?
按咱们习惯地先给出结论:
对于程序结构改造的业务逻辑,应该分为两步走:
在不添加新业务逻辑的基础上,对原来程序结构进行调整,以便支持新业务的扩展性
实现新功能
这样我们就可以发现,在第一步的调整时候,由于是原有业务,所以并不涉及新功能的扩展,可以单纯地调整程序结构。而当进行第二步的时候,只需要在扩展的基础上实现单独的业务就可以了,那么就满足 OCP 原则。
由于进行了两步走,第一步的时候实际上是与原有逻辑一致的。此时可以复用原来业务中的所有测试用例,以及单元测试,用很低的成本就可以保证代码的正确性。
而这样就也可以避免在早期开发的时候为了预留扩展功能,对系统添加了所以的各种功能支持而带来的过度设计。两步走的方案可以让我们在早期时候不必关心与后续的扩展性,只用“刚好”地完成业务就可以了,而让新的需求出现的时候,才是我们根据已有业务进行代码结构调整的时候。
最后
软件不退化的关键是保证每次需求变更的时候,做出正确的设计,才能保证软件以一种良性循环的方式不断维护下去。尽管原则如此,在实际执行的过程中也是十分困难,而难点正是保持良好的设计。就如同前文说的,每一次需求变更都意味着引入了更加复杂的客观世界逻辑,那么如何对这些越来越复杂的客观世界逻辑进行设计,也是我们的重要课题。
版权声明: 本文为 InfoQ 作者【蜜糖的代码注释】的原创文章。
原文链接:【http://xie.infoq.cn/article/1bfad2a7afa790aef15c5ff29】。文章转载请联系作者。
评论