浅读-软件设计哲学
浅读-软件设计哲学
软件为何越来越难维护?甚至于不想去维护?
在软件迭代中,不关注软件系统结构,导致软件复杂度累加,软件缺乏系统设计,模块混乱,一旦需要增加、修改或优化,改变的代价无法评估,甚至于为了解决一个bug或优化,引入一个新的bug。【高耦合低内聚,降低复杂度】
解决复杂性的方法有两类:让代码更简单易懂,或者通过模块化设计将其封装起来。软件设计贯穿系统的整个生命周期,大型系统的设计不可能在编码之前就完整完成。
1. 复杂度本质
复杂度与软件系统结构息息相关。复杂度越大,修改或理解系统的难度也越大。
复杂度一般的三种症状:
- 变化放大,需要修改一个地方,却发现改动的点涉及全站,导致难度倍增;
- 认知负荷,开发者需要学习大量知识才能完成任务;
- 未知的未知,开发者在修改代码后,不知道它的实际影响面,不清楚哪些代码需要修改,这是最糟糕的一种。
复杂度的原因:
- 依赖性,模块的相互依赖关系(模块间过度依赖);
- 模糊性,重要信息模糊不清(例如:变量名、系统文档、代码注释等)。
简化模块依赖 和 减少模糊性 可以有效降低软件复杂度
2. 战略编程
在现代商业软件开发中,通常采用增量开发(敏捷开发),不断的迭代开发,增加新的功能,如同堆积木一般。
以最快的速度,完成开发任务,崇尚战术编程思维,进行软件开发编程。由于是以完成任务导向,在开发过程,不会过多考虑软件系统设计,必然导致复杂度累加,迭代难度加大。
那么如何避免?
通过战略编程思维,在每次迭代中,尽量设计通用性的接口,花一定的时间修改或调整系统设计,让后期迭代更加顺畅。(任何系统都不是一蹴而就)
建议将总开发时间的10-20%投入设计投资,这种投入能很快收回成本并长期加速开发
3. 模块设计
深模块设计,简单的接口和复杂的实现分离。
每一个模块都会提供抽象的接口,使用者并不需要知道内部实现,只需要调用接口,传递对应的参数使用即可。
对于复杂的深模块,只需要暴露简易的接口即可;
Unix I/O是典型例子:五个系统调用(open、read、write、lseek、close)隐藏了数十万行复杂实现代码。
即使模块最初服务于特定场景,设计者也应尽量使其接口具备一定的通用性,而非过度特化。通用设计往往促成更简洁、更强大的抽象,减少重复代码和特殊逻辑——更重要的是,通用接口天然隐藏了更多实现细节,帮助达成更深层次的抽象
4. 系统分层
软件系统是分层构成的,高层使用低层提供的方法,每一层应该提供与上下层不同层次的抽象。
例如:TCP/IP协议
不要进行简单的参数传递,方法只作为简单的传参使用,这样反而提升复杂度。
同时分层本身不是目的,创造出不同层次的清晰抽象才是目的。
当某个模块面临无法消除的复杂性时,设计者应该“向下推”——尽量在更低层次的模块中处理这些复杂性,为上层使用者提供更简单干净的接口。这样每个使用者的认知负荷就会显著降低。
5. 如何合并和分离
组件分离的前提是:组件是真正独立的。
如果组件之间依然存在依赖关系,分离是不合适的。
组件模块是否合并,以结果为导向:当两个或多个组件模块被合并时,接口使用更简单、方便。
6. 定义错误
代码中异常问题的兼容,通常需要大量代码支持。
对于这种情况,可以通过合理的异常提示来完成,例如:window系统中,对于正在开启的文件进行删除操作,系统就会提示你,操作异常。
在一般情况下,我们就可以采取以上的方法,避免过多的代码兼容,直接提示异常。不过相对理想的情况就是,尽量减少代码,编写通用的异常处理模块,通过通用性来降低异常处理复杂度。
7. 两次设计
并不是真的要去做两次设计,而是希望,在设计一个模块或组件功能时,仔细的思考一下是否有更好的设计方案。
考虑多种设计,考虑这种设计和对比弱点,将有益的它们具有其他设计的功能。在对备选方案进行粗略设计后,列出每个利弊。
“两次设计”方法不仅可以改善您的设计,而且还可以提高您的设计技能。设计和比较多个项目的过程方法将教您有关使设计更好或更坏的因素
8. 为什么写注释
注释是抽象的基础,抽象的目的是隐藏复杂性:抽象是简化的实体视图,其中保留了基本信息。如果用户必须阅读方法的代码,那么注释可以帮助它了解实体抽象。
说服你注意的三件事:
- 好的注释可以在总体上产生很大的不同软件质量;
- 写好注释并不难;
- 并且(这可能很难相信)写注释实际上很有趣;
文档可以通过为开发人员提供他们所需的信息来减少认知负担进行更改,并使开发人员易于忽略以下不相关的信息。好的文档可以阐明依赖关系,并且它填补了空白以消除晦涩之处。
9. 注释应该描述代码中没有明显的内容
编写注释的原因是,使用编程语言编写的语句,无法捕捉到编写代码的开发人员头脑中的重要信息,而将其写在注释中,即可让后来的开发者更快了解功能代码,也可以提醒现有开发人员。
10. 注释先行
在实现过程中,把接口和注释先准备好。
好注释应当描述抽象、说明理由、记录设计决策层面的背景信息而非逐行解释代码。
11. 命名与一致性
命名和一致性是降低认知负担的基石。清晰、准确且一致的命名能帮助开发者快速理解代码,糟糕的命名则导致混淆与错误。系统范围内的一致性则允许开发者基于对某个部分的已有知识推断其他模块的行为逻辑,大大减少学习成本。
