如果大家觉得文章有错误内容,欢迎留言或者私信讨论~
软件开发的难度无非两点。第一点是技术难,就是说,代码量不一定多,但要解决的问题比较难,需要用到一些比较深的技术解决方案或者算法,不是靠“堆人”就可以解决的,比如自动驾驶、图像识别、高性能消息队列等。第二点则是技术不难,但项目很庞大,业务复杂,代码量多,参与开发的人员多。比如物流系统、财务系统等。今天需要了解的是第二点。
简单的"hello world"谁都可以写出来,几千行的代码谁都能维护。但是当代码量开始上涨——几万行、十几万行甚至几十万行、百万行代码的时候,软件的复杂度就会呈指数型上涨。这种情况下,我们不仅仅要求程序运行得了,运行得正确,还要求代码看得懂、维护得了。实际上,复杂度不仅仅体现在代码本身,还体现在协作研发上,如何管理庞大的团队,来进行有条不紊地协作开发,也是一个很复杂的难题。
如何应对复杂的软件开发?Unix 开源项目就是一个值得学习的例子。希望你能够从这三点出发:
话不多说,让我们开始吧。
在 Unix、Linux 系统中,有一句经典的话,“Everything is a file”,翻译成中文就是“一切皆文件”。在 Unix、Linux 系统中很多比如 Socket、驱动、硬盘、系统信息等都使用文件系统的路径作为统一的命名空间,使用统一的 read、write 标准函数来访问。
比如,我们要查看 CPU 的信息,在 Linux 系统中,我们只需要使用 Vim、Gedit 等编辑器或者 cat 命令,像打开其他文件一样,打开 /proc/cpuinfo,就能查看到相应的信息。除此之外,我们还可以通过查看 /proc/uptime 文件,了解系统运行了多久,查看 /proc/version 了解系统的内核版本等。
实际上“一切皆文件”就体现了封装和抽象的设计思想。
装了不同类型设备的访问细节,抽象为统一的文件访问方式,更高层的代码就能基于统一的访问方式,来访问底层不同类型的设备。这样隔离底层设备访问的复杂性,统一的访问方式能够使得上层代码的编写变得简单;并且抽象和封装还能有效控制代码复杂性的蔓延,提供简单、统一的访问接口,让其他模块来使用,其他模块基于抽象的接口而非具体的实现编程,代码会更加稳定。
对于像 Unix 这样的复杂系统,没有人能掌控所有的细节。之所以我们能开发出如此复杂的系统,并且能维护得了,最主要的原因就是将系统划分成各个独立的模块,比如进程调度、进程通信、内存管理、虚拟文件系统、网络接口等模块。不同的模块之间通过接口来进行通信,模块之间耦合很小,每个小的团队聚焦于一个独立的高内聚模块来开发,最终像搭积木一样,将各个模块组装起来,构建成一个超级复杂的系统。
Unix、Linux 等大型系统之所以能做到几百、上千人有条不紊地协作开发,也归功于模块化做得好。不同的团队负责不同的模块开发,这样即便在不了解全部细节的情况下,管理者也能协调各个模块,让整个系统有效运转。
除了模块化,分层也是我们常用来架构复杂系统的方法。
我们常说,计算机领域的任何问题都可以通过增加一个间接的中间层来解决,这本身就体现了分层的重要性。比如,Unix 系统也是基于分层开发的,它可以大致上分为三层,分别是内核、系统调用、应用层。每一层都对上层封装实现细节,暴露抽象的接口来调用。而且,任意一层都可以被重新实现,不会影响到其他层的代码。
面对复杂系统的开发,我们要善于应用分层技术,把容易复用、跟具体业务关系不大的代码,尽量下沉到下层,把容易变动、跟具体业务强相关的代码,尽量上移到上层。
上面提到分层、模块化,不同层、不同模块之间该如何通信呢——通过接口来实现。在设计模块或者层要暴露接口的实现,我们要学会隐藏,接口从命名到定义都要抽象一些,尽量少涉及具体的实现细节。
比如,Unix 系统提供的 open() 文件操作函数,底层实现非常复杂,涉及权限控制、并发控制、物理存储,但我们用起来却非常简单。并且我们在改动 open() 函数的底层实现的时候,并不需要改动依赖它的上层代码。
高内聚、松耦合是一个比较通用的设计思想,内聚性好、耦合少的代码,能让我们在修改或者阅读代码的时候,聚集到在一个小范围的模块或者类中,不需要了解太多其他模块或类的代码,让我们的焦点不至于太发散,也就降低了阅读和修改代码的难度。
简单来说就是依赖关系简单,耦合小,修改代码不会牵一发而动全身,代码改动比较集中,引入 bug 的风险也就减少很多。
越是复杂的项目,越是需要在前期设计上多花点时间。提前思考项目中未来有哪些功能需要拓展,提前预留好拓展点(实际上我觉得这非常依赖经验
),以便在未来需求变更时,在不改动代码整体结构的情况下,轻松添加新功能。
做到代码可扩展,需要代码满足开闭原则。特别是像 Unix 这样的开源项目,有 n 多人参与开发,任何人都可以提交代码到代码库中。代码满足开闭原则,基于扩展而非修改来添加新功能,最小化、集中化代码改动,避免新代码影响到老代码,降低引入 bug 的风险。
除了满足开闭原则,做到代码可扩展,我们前面也提到很多方法,比如封装和抽象,基于接口编程等。识别出代码可变部分和不可变部分,将可变部分封装起来,隔离变化,提供抽象化的不可变接口,供上层系统使用。当具体的实现发生变化的时候,我们只需要基于相同的抽象接口,扩展一个新的实现,替换掉老的实现即可,上游系统的代码几乎不需要修改。
简单清晰、可读性好,是任何大型软件开发要遵循的首要原则。只要可读性好,即便扩展性不好,顶多就是多花点时间、多改动几行代码的事情。但是,如果可读性不好,连看都看不懂,那就不是多花时间可以解决得了的了。如果你对现有代码的逻辑似懂非懂,抱着尝试的心态去修改代码,引入 bug 的可能性就会很大。
不管是自己还是团队,在参与大型项目开发的时候,要尽量避免过度设计、过早优化,在扩展性和可读性有冲突的时候,或者在两者之间权衡,模棱两可的时候,应该选择遵循 KISS原则,首选可读性。
今天讲解的实际上就是设计模式的设计原则,能够做到以上的实际上是比较困难的,需要开发人员或者管理人员都有较高的素质。