OverRainbow

Design principles of software development

☕️ 1 min read

你可以从本文了解到

本文是对《软件设计的 201 个原则》的第4章——软件开发的设计原则的学习。

软件设计是什么

设计包括以下活动:

  • (1) 定义满足需求的软件架构(architecture);
  • (2) 为架构中的各个软件组件指定算法。

架构包括:

  • 软件中所有模块的定义;
  • 它们之间如何提供接口;
  • 它们之间如何组装;
  • 组件的生命周期。

设计的最终产出是设计规格说明(Design Specification)。

061 从需求到设计的转换并不容易

设计的目的是实现从需求规格到设计规格、从外部视角到内部最优的转换。

设计的第一步,是综合形成一个理想的软件架构。

不能直接将需求规格说明中的"架构"作为软件架构,原因:

  1. 在需求分析阶段,完全没有考虑选择最优设计。在这种情况下,不能接受将需求阶段的设计作为最终设计。

  2. 在需求分析阶段,列出各种可选设计,并进行分析及选优。

    在确定需求基线、做出自己开发或购买的决策和进行开发成本估算之前,组织无法负担的起去做彻底的设计(通常占开发总成本的 30% 到 40%)。

  3. 该方法假设某种软件架构对所有软件都是最理想的。这显然是不可能的。

062 将设计追溯至需求

设计软件时,设计者必须知道,哪些需求能被每个组件满足。

在不同阶段都需要能对组件和需求进行追溯定位。

  • [选择软件架构]时,重要的是所有需求都能被覆盖。
  • 软件[部署]后,当检测到故障时,维护人员需要快速分离出那些最有可能包含故障原因的软件组件。
  • [维护]期间,当一个软件组件被修复时,维护人员需要知道哪些需求可能会受到不利的影响。

可以通过创建一个大的二维表格来满足,它的行对应所有的软件组件,它的列对应需求规格说明中的每个需求。任何位置的 1 表示此设计组件有助于满足此需求。

063 评估备选方案

在所有工程学科中,一个重要思想是:详细列出多种方法,在这些方法之间权衡分析,并最终采用一种。

064 没有文档的设计不是设计

设计,是在纸或其他媒介上,对恰当的体系结构和算法的选择、抽象和记录

065 封装

信息隐藏,有助于隔离错误。

066 不要重复造轮子

软件工程师经常一次又一次地重新发明组件。他们很少修补已有的软件组件。

有趣的是,软件业称这种罕见的实践为“重用”,而不是“工程”。

067 保持简单

托尼·霍尔(Tony Hoare) 说过:构建软件设计有两种方法。一种方法是使它简单到明显没有缺陷,另一种方法是使它复杂到没有明显的缺陷。

记住 KISS 原则(Keep it Simple and Stupid)

当你将软件分解成子组件时,记住一个人很难同时理解超过 7 (+- 2)个事物。

068 避免大量的特殊案例

每一个特殊案例都会使你更难调试,并使其他人更难修改、维护和增加功能。如果你发现太多的特殊案例,你可能设计了一个不合适的算法。应重新思考并重新设计算法。

069 缩小智力距离

艾兹格·迪科斯彻(Edsger Dijkstra)将智力距离(Intellectual Distance)定义为,现实问题和对此的计算机解决方案之间的距离。

理查德·费莱(Richard Fairley) 认为,智力距离越小,维护软件就越容易。

软件的结构应该尽可能接近的模仿现实世界的结构

面向对象设计和杰克逊系统方法(Jackson System)等设计方法,将最小的智力距离作为主要的设计驱动。

070 保持设计在智力可控范围内

如果设计是以能使其创建者和维护者完全理解的方式创建和记录的,那么这个设计就是在智力可控范围内的。

这种设计的一个基本属性是,它是分层构建的和多视图的

层次结构使读者能够抽象的理解整个系统,并在向更深层次移动时,理解越来越多的细节。

在每个层次上,组件应该仅从外部视角描述(原则 80)。

(在层次结构中的任何级别)任何单个组件都应该展现出简单和优雅。

071 保持概念一致

概念一致是高质量设计的一个特点。

它意味着,使用有限数量的设计“形式”,且使用方式要统一。

设计形式包括:模块如何向调用方通知错误,软件如何向用户通知错误,数据结构如何组织,模块通信机制,文档标准,等等。

当设计完成后,它应该看起来都是一个人做的,尽管它其实是很多参与者的产出。

在设计过程中,经常会有偏离既定形式的诱惑。

对这样的诱惑,有些是可以让步的,比如理由是提升系统的一致性、优雅性、简单性或性能。

有些则不能让步,比如仅仅为了确保某个设计者在设计中留下自己的印记。

概念一致性比自我满足更重要。

072 概念错误比语法错误更严重

在开发的各个阶段,问自己一些关键的问题。来发现概念错误:

在需求阶段,问自己,“这是客户想要的吗?”。

在设计阶段,问自己,“这个架构在压力下可以正常工作吗?”,或者,“这个算法真的适用于各种场景吗?”。

在编码阶段,问自己,“这段代码的执行和我想的一样吗?”,或者,“这段代码是否正确实现了这个算法?”。

在测试阶段,问自己,“执行这段测试能让我确信什么吗?”。

073 使用耦合和内聚

我们要追求的是低耦合和高内聚。

074 为变化而设计

为了适应变化,设计需要做到:

  • 模块化,即应该由独立的部分组成,每一部分可以很容易地升级或替换,而对其他部分造成最小的影响(查看相关原则 65,70,73,80)。

  • 可移植性,即应该很容易修改以适应新的硬件和操作系统。

  • 可塑性,即可以灵活地适应预期外的新需求。

  • 最小智力距离(原则 69)。

  • 在智力可控范围内(原则 70)。

  • 这样它就表现出 概念一致性(原则 71)。

075 为维护而设计

对于软件产品,设计后的最大成本风险是维护。

从对可维护性的影响来说,架构选择比算法或代码更加重要。

076 为防备错误而设计

你的设计决策应该尽可能做到以下优化:

1.不引入错误。

2.引入的错误容易被检测。

3.部署后软件中遗留的错误要么是不危险的,要么在执行时有补偿措施,这样错误不会造成灾难。

077 在软件中植入通用性

当把一个系统拆分成子组件时,要注意其潜在的通用性。

当多个地方都需要一个相似的功能时,只需构建一个通用功能组件,而非多个相似功能组件。

在开发只在一个地方需要的功能时,要尽可能植入通用性,以便日后扩展。

078 在软件中植入灵活性

一个软件组件的灵活性体现在,它很容易被修改,以在不同的场景下执行其功能(或者相似功能)。

079 使用高效的算法

了解算法复杂度理论是成为一名优秀设计者的绝对前提。

“算法分析”理论让我们知道,如何区分本来速度就慢的算法(不管编码如何优秀)和速度快几个数量级的算法。

080 模块规格说明只提供用户需要的所有信息

设计过程中一个关键部分,是系统中每个软件组件的精确定义。

它必须包含用户(这里的“用户”是指,另一个软件组件,或另一个组件的开发者)需要的全部内容,如:用途,名字,调用方法,如何同所在环境通信的细节。

任何用户不需要的内容,都要明确排除在外。

在大部分情况下,应该排除使用的算法和内部数据结构。

081 设计是多维的

一份完整的软件设计至少需要包括:

1.打包方案(Packaging)。通常用层次图的形式给出,用于说明“什么是什么的一部分?”。它通常隐含说明了数据可见性。它还能体现封装性,如对象内包含的数据和方法。

2.依赖层次(Needs Hierarchy)。用于说明“谁需要谁”。以组件网状图的形式表达,其中箭头的指向表明组件间的依赖关系。依赖可能是数据、逻辑或者其它信息。

3.调用关系(Invocation)。用于说明“谁调用谁”。以组件网状图的形式表达,其中箭头的指向表明组件间的调用、中断、消息传递关系。

4.进程组织(Processes)。一批组件被组织在一起,成为异步处理的进程。这是与其它进程同时运行的组件副本。零个、一个或多个副本可能同时存在。另外,还需要说明进程创建、执行、停止或销毁的条件。

082 优秀的设计出自优秀的设计师

无论如何,真正优秀的设计,是真正优秀设计者的智慧结晶。

优秀设计的特征是:简洁(Clean)、简单(Simple)、优雅(Elegant)、快速(Fast)、可维护(Maintainable)、易于实现(Easy to Implement)

优秀的设计源于灵感和洞察力,而不仅是努力工作或按部就班的设计方法。

对于最好的设计者要重点支持。他们才是未来。

083 了解应用场景

无论需求文档写得多好,架构和算法的最优选择,主要应基于对应用场景特质的理解。

084 你可以做到低成本的重用

问身边的人你是否开发过做这个功能的组件?如果有,拿来用。

085 “错进,错出”是不对的

要考虑异常的输入,并且保持系统正常运行。不要造成“多米诺效应”。

086 可靠性可以通过冗余来实现

在硬件设计中可以通过:1. 并行方式。2. 冷备方式。

这样的设计成本会少量增长。可靠性是指数级提升。

在软件系统中,相同的软件做两份拷贝,并不会增加软件的可靠性。其中一个失败,另一个也会失败。

可行的方案是:根据相同的需求规格说明,设计出两套软件系统,然后并行部署。

这样在软件系统中,设计成本会翻倍。

软件的超高可靠性是非常昂贵的。(原则 4)