系统复杂度 Part I

系统复杂度 Part I

关键词

系统复杂度、重构、设计模式、测试

0x1 Introduction

当你接手一个不熟悉的系统的时候,你可能想找一下系统文档(但是已经很久没更新了);你可能想看一下有没有单元测试(很多根本跑不起来了);这时候,除了问同事,只能自己从几千几万行代码中开始摸索。

当你开发一个新系统的时候,由于某些原因需要调整局部的逻辑,因为没有相应的单元测试,结果在移交给测试后才发现各种顾此失彼的bug出现了。

当你的系统性能已经无法再满足时,又或者系统的迭代成本异常大时,此时就会可能需要重写或重构。此时,由于没有单元测试,自动化集成测试,你不得不拉上更多的同事来参与此次系统优化,以保证系统的可靠性。

当你的系统需要有新的客户接入时,因为没有最新的系统文档或使用说明书,你只能电话、口头地来教别人怎么接入你的系统。

上述的几个场景,都是因为系统的配套设施(文档、测试)没有同步完善,引起接入、迭代、重构的成本显著增加。这不是我们想看到的。相对地,系统的快速迭代的开发模式已经受到了广泛的认同,我们怎么在这个过程能够一直保持高效、可靠的输出呢?对于这个问题,本文尝试从系统复杂度、重构、设计模式、测试这几个角度来讨论这个问题,希望能给大家一些新的思考。

插曲,在招联面试中,关于软件工程回答中,我的答案是说,它与结构工程一样,都需要在有限的资源条件下,用尽可能优化的设计,来实现降低成本地完成目标。经过了近2年的打磨,我对这个问题有了更多的理解。迭代,变化性,设计依据。

0x2 系统复杂度

首先,算法有复杂度一说,在所有的算法课程基本都会在前1-2章围绕这个话题展开详细的定义与分析讨论。算法复杂度一般分为时间复杂度与空间复杂度,可以说是该算法解决问题的时间与空间成本。

在工作上,我们维护的往往是一个系统,一个工程。一个系统的复杂度定义,似乎并不是那么容易。本章节大多数内容来源于_A Philosiphy of Software Designing_。这本书中对于系统的复杂度定义非常的实际,软件系统结构中让其变得难以理解且难以变更的部分组成了系统的复杂性。回顾本文引子部分,其实都是系统难以理解、难以修改导致的结果。

这个定义相对于算法复杂度而言,显得非常主观,没有客观的评估准则,各个人的理解也会大相径庭。因此,书中进一步展开了几种复杂性症状。

  1. 变化放大(change amplication):当你需要改动某一个逻辑时,会涉及到多处代码变更。
  2. 认知负担(cognitive load):有时尽管只写了很少的代码,但是对于他人来说异常难以理解,请尝试将它改写成容易理解的方式。
  3. 不可知(unknown unknowns):不知道哪里有坑存在,就算BUG出现了。

总的来说,控制住整个系统的复杂度,将是保障我们高效、可靠工作的核心。低复杂度的系统,大大降低了开发过程中的心智压力。

0x3 测试

为什么要把测试放在靠前的位置呢?测试是将一直伴随在生产代码演进的,是软件的可靠性保证的重要工具。

拿建筑工程做一个类比,设计一幢房屋时,对于设计人员,最终产出的是施工图。那么,它的设计依据是什么呢?《规范》与模型的承载能力分析。前者,描述了什么类型的结构需要怎么样的设计,这种属于基本要求。后者,通过基于物理建模软件进行专门的结构性能分析,来确保本次设计的可靠性

软件工程相比建筑工程,非常年轻,一方面没有条条框框地规则(P3C也仅仅是一些基础性条约);另一方面,因为大部分软件工程不涉及生命财产,也可以通过快速更新来解决问题,所以导致大多数人对可靠性的关注度远不如建筑工程。先不说这样的态度容易使生产问题频发,另外也会让你的工作开展体验感相对差劲,总是在返工。

因此,我们需要在注重软件的可靠性。我们的第一个工具就是(自动化)测试。好的测试,就能够有效地模拟真实地业务场景,来校验代码的正确性。好的测试,能够让新加入的开发人员更容易理解系统是如何使用的。好的测试,能够促进系统架构的合理化——高内聚低耦合。但是,好的测试代码并不容易写,因为测试代码的思考方式与一般的coding并不一样,它更像是生产代码的前置条件、运行场景。

测试通常分为四个层次:单元测试(Unit Testing)、集成测试(Integration Testing)、系统测试(System Testing)、验收测试(Acceptance Testing)。其中离开发最近的是单元测试,一般覆盖的是函数以及类的功能正确性。集成测试则是对系统内部的各模块之间的协作是否正确,通常按内部接口进行覆盖测试。系统测试则是验证对外接口能否在各种情况下能够正常地运行(性能测试也属于这一层次)。验收测试属于辅助性的测试,范围也相对更广,不同的系统也会需要不同的验收测试,例如安全测试、易用性测试、兼容性测试等等。

这里我们主要谈一下单元测试,因为这是所有测试的基本面,也是开发人员所需要特别关注的地方。先考虑一下是先写生产代码还是先写测试代码。第一个角度是,当然是先写生产代码了,没有生产代码怎么测试呢?第二个角度是,先写测试代码再写生产代码,因为测试代码是功能的运行场景,只有先把这些前置条件搞清楚了,才能写出正确的生产代码。也许你会说,我可以先大脑过一下基本场景,然后再写生产代码,再写测试代码,这应该是大部分人目前的方式,但是不知道你有没有发现,这种方式写出来的测试代码往往会因为前期的假设导致覆盖率很低。

在2003年,Kent Beck提出了Test-Driven development(TDD)这样一种开发模式。

  1. 添加一个测试
  2. 运行所有测试,是否有失败
  3. 一旦有失败的测试,就开始写生产代码,直到该测试通过为止。
  4. 所有测试通过后,就必须开始写测试代码,直到有一个测试无法通过。

可能,你会觉得这样非常耗时,但是你得明确,你在一个功能开发的耗时并不仅仅是写了那一堆生产代码而已,还包括测试、代码修复等等。所以,这种方式只是将一些顺序进行了调换,却能为最终的正确性铺垫很多。

0x4 重构

重构与重写都是试图优化系统复杂度的方式,在不同的场景(代码状况、工期等)会选择不同的方式,但是大多时候会采用重构的方式来实现。

先来看一下wikipedia对重构的定义:

In computer programming and software design, code refactoring is the process of restructuring existing computer code—changing the factoring*—without changing its external behavior. Refactoring is intended to improve the design, structure, and/or implementation of the software (its *non-functional attributes), while preserving its functionality. Potential advantages of refactoring may include improved code readability and reduced complexity; these can improve the source code’s maintainability and create a simpler, cleaner, or more expressive internal architecture or object model to improve extensibility.

在系统复杂度膨胀到一定程度后,添加功能异常困难、系统变得难以理解、修改的时候,重构往往就需要开始执行,从而降低系统的复杂度,让我们能够更高效地开发、修改代码。所以重构并不是某一个开发阶段,而是一种开发工具,帮助我们提升效率。

在《重构》这本书中,提到很多地坏味道,本质就是一些让系统复杂度变高的地方,你可以发现它与第二节中的那些症状很类似。

这里特别想说的是,重构的过程中,其实会采用不少设计模式,但是并不能说设计模式就是万能药。设计模式是我们需要了解的,但是我们需要记住的是它们出现的目的是什么,我这里可以列举几个:

  1. 减少继承,多采用组合的方式来丰富类的功能。
  2. 面对各种功能的变更,能够将变化(影响)的范围尽可能的缩小到1个或者少量的类中。
  3. 面向接口编程。

0x5 结论

  1. 除了写好生产代码以外,我们要多多关注一些周边的工具:自动化测试、持续继承、文档等。
  2. 对于一个持续迭代的系统,系统复杂度是需要重点关注的。
  3. 尝试把你的测试代码前置。
  4. 重构能够帮助你降低系统复杂度。
  5. 三次原则:第一次做某件事时只管去做;第二次做类似的事会产生反感(蹩脚),但无论如何还是可以去做;第三次再做类似的事,你就应该重构/反思。

站在巨人的肩膀上

  1. https://en.wikipedia.org/wiki/Software_testing#Testing_levels
  2. https://en.wikipedia.org/wiki/Code_refactoring
  3. Ousterhout, John. A Philosophy of Software Design. Yaknyam Press, 2018.
  4. Fowler, Martin. Refactoring: improving the design of existing code. Addison-Wesley Professional, 2018.
  5. Martin, Robert C. The clean coder: a code of conduct for professional programmers. Pearson Education, 2011.

最后修改于 2020-06-06