Skip to main content

One post tagged with "软件设计"

View All Tags

· 19 min read
天山飞猫

用一句话概括《A Philosophy of Software Design》,软件设计的核心在于降低复杂性

Chapter 1 Introduction(It’s All About Complexity)

  • 随着时间的流逝,复杂性不断累积,程序员在修改系统时将所有相关因素牢记在心中变得越来越难。这会减慢开发速度并导致错误,从而进一步延缓开发速度并增加成本。在任何程序的生命周期中,复杂性都会不可避免地增加。程序越大,工作的人越多,管理复杂性就越困难。
  • 有两种解决复杂性的通用方法。
    1. 通过使代码更简单和更明显来消除复杂性。
    2. 封装它,以便程序员可以在系统上工作而不会立即暴露其所有复杂性。这种方法称为模块化设计。
  • 为什么采用诸如敏捷开发之类的增量方法:
    1. 通过以这种方式扩展设计,可以在系统仍然很小的情况下解决初始设计的问题。较新的功能受益于较早功能的实施过程中获得的经验,因此问题较少。
    2. 增量方法适用于软件,因为软件具有足够的延展性,可以在实施过程中进行重大的设计更改。
    3. 增量开发意味着永远不会完成软件设计。设计在系统的整个生命周期中不断发生。增量开发还意味着不断的重新设计。
  • 如果软件开发人员应始终考虑设计问题,而降低复杂性是软件设计中最重要的要素,则软件开发人员应始终考虑复杂性。

Chapter 2 The Nature of Complexity

2.1 Complexity defined

  • 如果一个软件系统难以理解和修改,那就很复杂。如果很容易理解和修改,那就很简单。
  • 在复杂的系统中,要实施甚至很小的改进都需要大量的工作。在一个简单的系统中,可以用更少的精力实现更大的改进。
    C=pcptpC=\sum_{p}c_{p}t_{p}
  • 系统的总体复杂度(C)由每个部分的复杂度(cp)乘以开发人员在该部分上花费的时间(tp)加权。在一个永远不会被看到的地方隔离复杂性几乎和完全消除复杂性一样好。
  • 避免主观偏见:读者比作家更容易理解复杂性。作为开发人员,不仅要创建可以轻松使用的代码,而且还要创建其他人也可以轻松使用的代码。

2.2 Symptoms of complexity

  1. 变更放大:复杂性的第一个征兆是,看似简单的变更需要在许多不同地方进行代码修改。
  2. 认知负荷:复杂性的第二个症状是认知负荷,这是指开发人员需要多少知识才能完成一项任务。 系统设计人员有时会假设可以通过代码行来衡量复杂性。他们认为,如果一个实现比另一个实现短,那么它必须更简单;如果只需要几行代码就可以进行更改,那么更改必须很容易。但是,这种观点忽略了与认知负荷相关的成本。我已经看到了仅允许使用几行代码编写应用程序的框架,但是要弄清楚这些行是什么极其困难。有时,需要更多代码行的方法实际上更简单,因为它减少了认知负担。

    另一个要考虑的是性能和可读性的选择,不同的场景下有不同的选择。

  3. 未知的未知:复杂性的第三个症状是,必须修改哪些代码才能完成任务,或者开发人员必须获得哪些信息才能成功地执行任务,这些都是不明显的。

2.3 Causes of complexity

复杂性是由两件事引起的:依赖性和模糊性。

  1. 依赖关系是软件的基本组成部分,不能完全消除。但是,软件设计的目标之一是减少依赖关系的数量,并使依赖关系保持尽可能简单和明显。
  2. 减少模糊性的最佳方法是简化系统设计。

2.4 Complexity is incremental

复杂性不是由单个灾难性错误引起的;它是积少成多的一个过程。复杂性的增量性质使其难以控制。

这也是为什么重构要发生在任何时候,小的重构即安全又可以减少复杂性。

2.5 Conclusion

Chapter 3 Working Code Isn’t Enough(Strategic vs. Tactical Programming)

3.1 Tactical programming

几乎每个软件开发组织都有至少一个将战术编程发挥到极致的开发人员:战术龙卷风。战术龙卷风是一位多产的程序员,他抽出代码的速度比其他人快得多,但完全以战术方式工作。实施快速功能时,没有人能比战术龙卷风更快地完成任务。在某些组织中,管理层将战术龙卷风视为英雄。但是,战术龙卷风留下了毁灭的痕迹。他们很少被将来必须使用其代码的工程师视为英雄。通常,其他工程师必须清理战术龙卷风留下的混乱局面,这使得那些工程师(他们是真正的英雄)的进步似乎比战术龙卷风慢。 那从管理的角度来看,这也属于技术债务管理的范畴,如何保持清醒的头脑面对捷径和债务是最重要的。

3.2 Strategic programming

成为一名优秀的软件设计师的第一步是要意识到仅工作代码是不够的。引入不必要的复杂性以更快地完成当前任务是不可接受的。最重要的是系统的长期结构。任何系统中的大多数代码都是通过扩展现有代码库编写的,因此,作为开发人员,最重要的工作就是促进这些将来的扩展。因此,尽管您的代码当然必须工作,但您不应将“工作代码”视为主要目标。您的主要目标必须是制作出出色的设计,并且这种设计也会起作用。这是战略计划。 这也是为什么代码Review显得如此重要的原因了,当然怎么进行CR那就是另一个大的话题了

3.3 How much to invest?

作者给了一个拍脑袋的 10%-20% 的时间。 以我的个人经验来看,作为架构师,我的时间差不多有50%都会投入在与此相关的活动中;作为开发者的话很可能也会高于20%,因为我不太能容忍大量的重复和非显式的依赖。

3.4 Startups and investment

3.5 Conclusion

Chapter 4 Modules Should Be Deep

不幸的是,深度类的价值在今天并未得到广泛认可。编程中的传统观点是,类应该小而不是深。经常告诉学生,类设计中最重要的事情是将较大的类分成较小的类。 正是这些设计的观点造成的不便,在java世界中充满了各种 xxxUtil 或 xxxHelper 的公共方法类去对相关的冗长的吟唱式的调用模型进行了简单的封装。 我更倾向于短小的方法和内容自洽的类。

Chapter 5 Information Hiding (and Leakage)

在时间分解中,执行顺序反映在代码结构中:在不同时间发生的操作在不同的方法或类中。如果在执行的不同点使用相同的知识,则会在多个位置对其进行编码,从而导致信息泄漏。 这种现象太常见了,大概因为人类的认知本能决定的吧。

Chapter 6 General-Purpose Modules are Deeper

这个方向值得注意,是我既往工作中忽视的那一部分

Chapter 7 Different Layer, Different Abstraction

  1. 当相邻的层具有相似的抽象时,问题通常以直通方法的形式表现出来。 在常见的分层结构的java项目中很常见,从dao层到service层再到controller层,恨不能都是一样的,给前端暴露的接口像是一个专有化的操作表的接口。
  2. 慎用装饰器设计模式

Chapter 8 Pull Complexity Downwards

尽量在自己的范围内处理好自己能做的,而不是简答的将一切都交给其他人。不这光在软件设计上是这样,产品设计上更是这样,千万别把自己该干的推给用户,还美其名曰是提供了足够的个性化。

Chapter 9 Better Together Or Better Apart?

考量点有以下:

  1. 信息共享
  2. ​简化接口
  3. 消除重复

Chapter 10 Define Errors Out Of Existence

最近的一项研究发现,分布式数据密集型系统中超过 90%的灾难性故障是由错误的错误处理引起的。当异常处理代码失败时,很难调试该问题,因为它很少发生。 减少异常处理程序数量的四种技术:

  1. 设计不存在异常的api
  2. 异常屏蔽 在更底层的处理层把异常消化掉,很多工具类都在做类似的事情。Apache-Commons、Guava、Hutool
  3. 异常聚合
  4. 使应用程序崩溃 最常见的就是springboot里面的 fail-fast 了吧。

Chapter 11 Design it Twice

可以翻译成三思而行,成功的人们很容易满足在既有的成功经验中,而不停重复让他们成功的方式。但残酷的现实是,没有思考过优缺点而做出的决定很可能在环境变化后产生非预期的结果。

Chapter 12 Why Write Comments? The Four Excuses

核心原因是两点,没见过好的注释,不会写好的注释。在团队文化中需要好的范例和每次CodeReview的过程潜移默化的来影响团队成员。

Chapter 13 Comments Should Describe Things that Aren’t Obvious from the Code

在注释变量声明(例如类实例变量,方法参数和返回值)时,精度最有用。变量声明中的名称和类型通常不是很精确。注释可以填写缺少的详细信息,例如:

  • 此变量的单位是什么?
  • 边界条件是包容性还是排他性?
  • 如果允许使用空值,则意味着什么?
  • 如果变量引用了最终必须释放或关闭的资源,那么谁负责释放或关闭该资源?
  • 是否存在某些对于变量始终不变的属性(不变量),例如“此列表始终包含至少一个条目”? 方法的接口注释包括抽象层面的高级信息和精确性的低级细节:
  • 注释通常以一两个句子开头,描述调用者感知到的方法的行为。这是更高层次的抽象。
  • 注释必须描述每个参数和返回值(如果有)。这些注释必须非常精确,并且必须描述对参数值的任何约束以及参数之间的依赖关系。
  • 如果该方法有任何副作用,则必须在接口注释中记录这些副作用。副作用是该方法的任何结果都会影响系统的未来行为,但不属于结果的一部分。例如,如果该方法将一个值添加到内部数据结构中,可以通过将来的方法调用来检索该值,则这是副作用。写入文件系统也是一个副作用。
  • 方法的接口注释必须描述该方法可能产生的任何异常。
  • 如果在调用某个方法之前必须满足任何前提条件,则必须对其进行描述(也许必须先调用其他方法;对于二进制搜索方法,必须对要搜索的列表进行排序)。尽量减少前提条件是一个好主意,但是任何保留的条件都必须记录在案。

Chapter 14 Choosing Names

这是一个特别容易被忽视的点,应该在项目文档中就定义好命名的规范,并充分考虑到项目及团队的具体情况。如果能保证团队所有人都在utf-8的环境下工作,我不反对直接使用中文来命名变量和方法名,毕竟他们总好过用拼音首字母或拼音来命名。

Chapter 15 Write The Comments First(Use Comments As Part Of The Design Process)

  • 对于新类,我首先编写类接口注释。
  • 接下来,我为最重要的公共方法编写接口注释和签名,但将方法主体保留为空。
  • 我对这些注释进行了迭代,直到基本结构感觉正确为止。
  • 在这一点上,我为类中最重要的类实例变量编写了声明和注释。
  • 最后,我填写方法的主体,并根据需要添加实现注释。
  • 在编写方法主体时,我通常会发现需要其他方法和实例变量。对于每个新方法,我在方法主体之前编写接口注释。例如变量,我在编写变量声明的同时填写了注释。 有点TDD的意思,从我的经验来说,这是一个靠谱的方法。 而且尽早的写注释,还能在重构时需要重新设计时,提供当初的决策原因。

Chapter 16 Modifying Existing Code

Chapter 17 Consistency

可以通过 IDE的统一格式化标准 -> 代码静态检查工具 -> CodeReview 来保证。

Chapter 18 Code Should be Obvious

我喜欢引用的一个设计原则 KISS -> "Keep It Simple! Stupid!!!" 我更喜欢强调Stupid这个单词,减轻别人的认知障碍,是提高沟通效率的重要手段。

Chapter 19 Software Trends

Chapter 20 Designing for Performance

需要识别性能的瓶颈,解决关键节点的性能问题。当然也有一种情况在老系统中比较常见,在经历了几个不负责任的工程师后,在补丁上打补丁的情况会出现,这时,就需要去识别清楚调用链的情况去重构或重写这部分逻辑了。

Chapter 21 Decide What Matten

不同的项目的关注点是不同的,甚至一些一次性的嵌入式设备最关注的是使用寿命,那么省电就变得极其重要了。

Conclusion

软件设计的核心在于降低复杂性