管理复杂度的最重要的方法是每一次只需要面对一个小模块,这种方法被称为模块化设计。
模块化设计
整个系统被拆分成多个相对独立的模块。模块可以是类、子系统、服务。在理想情况下,模块间完全独立,开发某个模块时不需要感知到其他模块(不现实),一个系统的复杂度取决于这个系统中最复杂模块的复杂度。
不幸的是,模块间总有联系,一个模块总要调用别的模块,模块化设计的目标是尽可能的减少模块间的依赖性。
模块分成两个部分:接口和实现,接口是模块间需要感知的东西,接口定义了一个模块做什么以及如何使用它,实现保证了接口,编码一个模块时,需要知道这个模块的接口、实现以及其他模块的接口,但是不需要了解其他模块的实现。
只要有接口和现实的部分就是模块,一个好的工程实践是模块的接口要简单,实现可以复杂,这样有两个好处:调用者使用起来简单;设计者修改这些模块时,可以避免更改接口。
什么是接口
接口包含两部分:正式和非正式
正式:代码定义的,包括参数、返回值等
非正式:接口的功能,比如DeleteFile(const std::string& file_name),表示删除一个文件,甚至用注释说明这部分
抽象
抽象与模块化设计有关,抽象隐蔽细节,抽象以模块接口的方式展现,从使用者的角度去看,接口最重要,实现不重要,在设计接口时,需要站在使用者的角度去看。
抽象有两个错误表现:1、展现了不必要的细节,导致使用者认知放大;2、隐蔽了重要的细节,导致使用者晦涩难懂。所以抽象设计起来并不是越简单越好,需要深刻理解这个抽象,然后仅仅包含重要的信息,使得使用者仅仅看到抽象之后,就能或者到这个模块的所有需要获知的信息。
例子:文件系统,如何在磁盘上布局数据,不需要对使用者暴露,但是page cache是需要对用户暴露的,所以提供了flush接口
深度模块
最好的模块时提供很好的功能,但是保持简单的接口。(最简单的,从代码量上看,实现的代码很多,但是接口,包括注释的行数很少)
浅度模块
方法类(一个方法一个类?)
类要深,不一定要小,也不是说类越多越好,比如建议:一个方法如果超过多少行,就要被拆分,这个建议不一定对,可能会增加系统的复杂度。(要根据类或者模块要提供的功能来实现,要切切实实的提供功能相关的接口,至于功能的实现,只要隐蔽的好,不会增加复杂度)
例子:Java和Unix IO对比
java的编码风格提供类越多越好,比如打开文件序列化对象需要三步,违反了对于常见用法提供的接口越简单越好。
好的设计应该和Unix IO一样。