软件设计哲学-读书笔记

148 阅读29分钟

综述

作为最具有创造性的工作,编码确实是最近几年在国内比较受欢迎的工作。自然而然的,设计工作作为编码的前奏,也就更加的富有创造性和不确定性。
而作者通过复杂度这个概念来帮助大家梳理如何来简化设计,构建一个简单高效的系统。
复杂度也是本书通篇在讨论的一个概念,通过梳理依赖性和模糊点来想办法降低设计的复杂度以最终达到简化设计的目的
有一个观点很认同:代码写来是需要让别人也能读懂的;如果一份代码有人认为它不易懂,那它就真的是不易懂。
而想要在设计阶段尽量降低复杂度,一定要具有战略性思维,从根本上扭转“以完成功能为目的”这种工作思维方式;用投资的心态进行系统设计、模块设计,用前期的投入来带来后期的高收益。

第一章:创造性的工作

编码是世界上当前最有创造性的工作,不会被物理定律所局限,可以充分发挥自己的创造力来构建一些虚拟世界 同时这也会导致编码的复杂度,特别是随着时间流逝,不停的迭代,将会导致系统的复杂度越来越高,编码也就越来越容易出错

虽然有很多很牛的开发工具来避免编码时的问题,但如果想要构建更强大的系统,还想简化编码过程,就必须找到一些简化系统的方法
更加简单的设计可以帮助我们在复杂度膨胀到无法处理之前,构建功能更加强大的系统服务
作者认为解决复杂度有两种方式:

  1. 通过编写更加简洁、浅显易懂的代码来降低复杂度,比如通过消除特殊处理,或者以约定好的方式来使用标识符
  2. 封装它,通过模块化设计,将复杂度高的内容进行封装,这样其他编码人员就不用关心这个模块的细节 由于不停迭代,软件设计贯穿了整个软件系统的一生。而软件设计的复杂度相比其他物理设计,要复杂的多,而一个大型系统的设计,也不可能在编码之前,就能完整的进行展现
    这本书就是讲软件设计中,如何来识别复杂度,并在设计过程中来降低复杂度

第二章:复杂度

识别复杂度是一个非常重要的技能。它能帮你在一个问题上投入大量精力之前,帮你识别出该问题,同时能帮你在多个选择之间作出更好选择。
如果能识别出一个系统过于复杂,那你在设计时就应该尝试不同的方式或方法去去简化它
作者从更加实用的角度,来重新定义了复杂度(complexity):复杂度是指那些让系统难以理解或修改的与系统相关的任何事物(Complexity is anything related to the structure of a software system that makes it hard to understand and modify the system)
作者认为如果一个系统比较简单并易于理解,那设计就是简单的,否则就是复杂的;当然复杂度还可以根据成本和收益来进行评估,简单的系统中,较大的改进只需要消耗很少的精力,而复杂系统则相反
复杂度是开发人员在达到特定目标过程中的特定时间点所经历的东西,它不一定和系统的大小或功能有关。如果一个大系统(或者功能很多的系统)很容易的在其中进行开发维护,那按照本书的定义来看,这个系统并不复杂;相反如果一个小系统很难进行维护开发,那它也是复杂的
Complexity is determined by the activities that are most common,复杂度取决于最常见的活动。 复杂度计算公式 c=∑(Cp) * (Tp)。复杂度就是系统中每个模块的复杂度( complexity of each part p) 乘上 开发人员花费在对应模块的时长 ( time developers spend working on that part )再相加的总和。所以把复杂的内容隔离在一个多数人不会去动的模块,也变相等于消除了复杂度

相对来说,读者比作者更容易发现复杂度的存在。对一段代码来说,如果读者觉得比较复杂,那它可能就真的复杂。作为开发人员来说,工作不仅仅是写代码,更是要写一些别人能够易于理解、容易上手维护的代码
复杂度通常有三种方式来展现

  1. 变更放大:一个简单的需求,却需要改动多个地方的代码来实现;
  2. 认知负荷:也即是一个开发人员需要了解学习多少知识才能完成对应任务。负荷高也代表完成任务需要花费更多的时间和精力以便于学习了解更多知识,这也意味着会有更多风险,因为学习过程中可能会遗漏一些重要的知识点 通过代码行数来判定系统是否复杂是不合理的,因为这种方式会忽略认知负荷带来的复杂度。有些功能只需要少量代码就可以实现,但却需要很多时间和精力来学会怎么使用该段代码或接口
  3. 未知的未知(Unknown unknowns):不知道需要修改哪些代码才能完成任务,或者开发人员不知道需要了解哪些信息才能去完成工作。 三个复杂度的表现形式中,相较于变更放大和认知负荷,未知的未知是最糟的一种情况。而良好的设计一定是明朗的,易于理解的;在遇到变更时,开发人员可以只花费很少的时间成本,来明确需要修改哪些代码;

复杂度的来源有两个:依赖性和模糊性(dependencies and obscurity)

所谓依赖性:当一段代码无法独立的进行修改或者隔离于其他代码无法被轻易理解的话,这段代码就存在依赖性。当然模块间的依赖关系是系统的组成部分之一,不可能完全消除。遗憾的时,软件设计的目标就是减少代码的依赖性以使得依赖关系尽可能的简单明了
而模糊性的定义则是指当一些重要信息不明确时,就说明存在模糊性;比如定义的一些过于通用的变量名,可能不会带有单位等关键信息,从而导致认知负荷上升。很多情况下,模糊性多由文档缺失导致。但相对应的是,作者认为如果一个系统需要大量的文档,这也是一个红线警告
依赖性导致变化放大和高认知负荷。晦涩会产生未知的未知数,还会增加认知负担
也因此,随着系统的迭代,复杂度会随着时间逐步增加

第三章:明确思维方式

作者把编码的思维方式分为了战略方式思维战术方式思维
作者把着重于功能运行和维护的思维方式称为战术式思维,他认为这种是比较短视的,不可能产生良好的系统设计。同时为了追求功能的战术编码,还会导致复杂度的极速增加
一个有意思的概念:
战术龙卷风: 是指撸码速度远比其他同事都快,并且高产的开发人员,而且编码过程是完全的战术式思维(works in a totally tactical fashion)。而这带来的后果就想真的龙卷风一样,破坏力惊人!

成为一个好的软件设计人员的第一步就是从战术式编码方式中跳出来,working code is not enough
最重要的事情就是构建一个可以长期维护的系统结构。
作者认为大多数的系统代码都是通过扩展基础代码库来完成的,所以作为开发人员,最重要的事情应该是如何更好的进行代码扩展,也就是产出一个好的设计。而这就是战略规划
战略规划必须要有一个投资心态。相较于快速的完成功能代码,必须投入更多的时间来进行系统设计;短期看这种会拖慢进展,但对长期来看,会相对提高速度。
当然无论前期投入多少时间,设计过程中总会出现错误。发现这些问题时,需要进行修复。战略式编程,其实也是对系统设计的持续改进,这和战术式编码正好相反
投资当然也要考虑成本
前期投资巨量精力,不见得会有效果,比如想着去设计一个完整的系统。随着对系统的深入了解,理想的设计方式将趋向于碎片化。所以最好的方式就是持续的进行少量投资,每次投入10%-20%的开发时间

第四章:依赖性

模块化设计的目标就是降低模块间的依赖
为了管理模块依赖,给每个模块定义成了两个部分:接口和实现。接口来定义模块是做什么的,而不关注如何做;而实现则是指承载了接口所承诺的功能代码组合(The implementation consists of the code that carries out the promises made by the interface.)
模块可大可小,并没有固定的大小指定。面向对象的编程语言中,一个class也可以认为是一个模块。只要有接口以及对应的实现,都可以认为是模块。
好的模块设计,就是指接口比实现简单的多。这样就有两个好处:

  1. 依赖该模块的其他模块复杂度会降低;
  2. 修改了该模块的实现,可以不需要修改模块的接口,减少改动范围*
    模块化编程中,接口就是模块功能的抽象展现。抽象过程中,忽略掉的不重要的细节越多越好。当然这些细节也只能在抽象过程才能忽略
    抽象也可能走偏:
  3. 包含太多不重要的细节,这样会增加不必要的复杂度,增加认知负荷
  4. 忽略了重要的细节!这样会导致模糊性 设计抽象的重要一点就是识别重要性,并在设计过程中,将重要信息的数量尽量减到最少。
    作者认为抽象不仅仅是在编码中才依赖抽象去管理复杂度,生活中到处都是抽象的体现;例如微波炉,我们不关心他们具体的原理,只需要几个按钮就可以加热食物,又比如汽车也是一样的逻辑。
    好的模块当然是那些提供了简单接口,还实现了强大功能的模块.作者引入了Deep来形容这类的模块;模块深度(Depth)可以用来衡量成本收益比。深模块的成本收益比会很高,因为只需要引入少量的复杂度就可以带来巨大的功能
    同时作者认为不应该拆分过多的小类和小方法,这种只会增加复杂度。(The extreme of the “classes should be small” approach is a syndrome I call classitis)
    所以模块设计中,通过将接口和实现隔离开来隐藏相关复杂度;当然模块设计中,最重要的就是提高模块的深度

第五章:信息隐藏

想要增加模块深度最重要的技术就是隐藏信息;基本思想就是每个模块都应该封装一些代表来设计决策的知识。这些隐藏在模块中的信息,通常由某些机制的实现细节组成,例如:

  1. 如何在B树中存储数据并有效访问
  2. 如何实现TCP
  3. 如何解析JSON文档 信息隐藏可以在两方面降低复杂度:
  4. 它简化了模块的接口
  5. 使系统更容易进行优化 设计一个新模块时,一定要仔细考虑要隐藏哪些信息;如果可以隐藏更多信息,你就应该可以简化模块接口,并让模块更深(makes the module deeper.)

信息泄漏:如果同一个设计决策出现在了多个模块中,就出现了信息泄漏(Information leakage occurs when a design decision is reflected in multiple modules)
这将导致模块之间存在依赖,当设计决策发生变更时,所有模块都需要变动。例如模块接口中定义的信息,根据信息泄漏的定义,这些信息就属于信息泄漏(类似接口入参,如果参数发生变化,其他和该接口相关的模块都需要进行变更)。这也是为什么要设计更加简单接口的原因。
同时作者认为,信息泄漏不一定存在接口之间,比如两个处理同一个文件格式的类:一个处理写,一个处理读;如果文件格式发生变化,那么两个文件都需要变动
由此可见,信息泄漏会是软件设计中的红线之一,设计过程中,要对信息泄漏高度敏感

时序分解:作者称这种会导致信息泄漏的设计风格为时序分解(Temporal decomposition)。大概意思是设计中,如果只按照功能提出的时间进行设计,而不通盘考虑,很大可能会调出时序分解的陷阱中。
例如设计一个文件系统,我们需要读,写,修改相关接口。按照功能提出或者优先级来实现的话,可能会分成多个类进行处理,这就导致了信息泄漏,因为每个类都需要对这个格式的文件进行处理
作者认为在时序分解中,执行顺序会反映在代码结构上:不同的时间发生的操作调用会是不同的方法或者不同的类;这些操作中,可能会用到同样的知识(knowledge),这就需要在多个不同的地方进行同样的编码,从而导致信息泄漏 信息隐藏可以应用于不同的系统层级,比如同一个类中,同样可以对复杂度较高的代码进行封装,将相关信息进行隐藏;同时还要尽量减少同一个实例变量被引用的次数
同样的,信息隐藏只有当这些信息不会被模块外部所依赖时才有意义。作为设计人员,目标就是要将尽力降低外表所需要的信息

第六章:通用还是专属

设计过程中,总要面对一个很常见的问题:设计一个更通用的系统(或模块)还是设计一个更加个性化的模块
通用的模块在未来可能可以适用于其他问题,节省一定的开发时间。而通用的设计带来的后果可能就是有些功能永远不会被用到,造成浪费
作者的观点是应该进行部分通用设计:实现部分应该着重于当前功能,但接口应该是通用的模式。这样的话还可以避免过度设计
接口设计的更加通用,也会更好的进行信息隐藏。
软件设计最重要的元素之一就是确定谁需要知道什么以及何时知道。当细节很重要时,最好使它们明确且尽可能明显 作者也列出了一些自问的问题来帮助如何设计一个更加通用简洁的类

  1. 能满足我当前需求的最简单的接口是什么样的?如果能删除一些接口中的参数还不影响接口功能,那就是正在进行一个更加通用的设计
  2. 该接口会在哪些场景中被调用?对于一个特定的接口需要考虑是否可以使用更加通用的方法来进行替换
  3. 对当前的调用情况,API是否易于理解和使用?如果调用方还需要写很多代码来调用对应接口,其实也是红线告警

第七章:对于不同的分层采用不用的抽象

如果系统两个相邻分层中,存在了意义相近的抽象,这就表明系统分层拆解做的不够好,可能存在问题
作者认为这种问题通常是通过透传的方式展现出来(the often manifests itself in the form of pass-through methods.)透传方法是指那些除了调用其他方法外几乎没有自己处理逻辑的方法,接口签名和调用方法也非常类似
透传方法多的话,就说明系统间分层不太明确
透传方法会让类变得更浅,增加了类的接口复杂度,提高了复杂度
透传方法还表明类之间的职责划分存在模糊的地方
作者认为透传方法是很没有用处的东西,没有新增任何功能.当然,不是所有同样签名的方法都属于冗余方法,只要其实现的功能不一样,就没有关系
装饰器模式是常用的一种设计模式,它鼓励对API进行复制;通过继承一个类,对其功能进行扩展,例如Window和ScroolWindow。装饰器的初衷就是通过继承,将个性化的特性和通用的核心进行隔离。但作者却不太认同装饰器模式,认为这一方式很容易造成过度使用,增加过多的透传方法
另外一个导致API重复的形式就是透传参数,就是一个参数通过很长的调用链传到最终使用的方法中。这种方式必然会增加复杂度,因为上层被迫的知道了该参数,增加了认知负荷
而消除参数透传的方式就是通过共享对象或者全局变量来进行,或者通过上下文对象来封装。但使用上下文的方式时,一定要注意不能盲目扩充上下文的变量数量

第八章:降低复杂度

作为开发人员,本能的就会将复杂的内容丢给其他人处理以保证自己相关的模块尽量简单
所以作者认为,开发人员应该提供简洁的接口,增加模块或者方法的深度,提高功能性,减少调用方在调用相关API时的处理逻辑;比如尽量避免随意抛出异常等处理方式,以避免增加复杂度

第九章: 分开还是合并

在设计过程中,要谨慎的选择是分开还是合并,拆分或加入模块的决定应基于复杂度
但无论是合并还是拆解,都要记住首要目标永远都是降低复杂度,并优化提升模块化
作者认为拆分中大量的小系统也许对单个系统来说复杂度会很低,但整体来看,复杂度并没有降低,主要是由于:

  1. 数量太多的话,跟踪会有难度,使用者难以在大量的模块中找到自己所依赖的,而且会增加大量接口,从而提升复杂度
  2. 细分的话需要额外的代码来管理相关组件;而且细分会产生隔离,导致调用难度增加,例如会引入分布式事务等
  3. 细分会导致重复 整体的原则就是:如果他们紧密相关,那将代码合在一起收益比较高;如果各部分无关则分开来,具体如下:
  4. 如果有信息共享,则聚集在一起
  5. 如果合并在一起会使接口更加简单,就合并
  6. 通过合并可以消除重复
  7. 将通用代码和个性化代码(special-purpose code)分开。个人感觉其核心还是消除重复代码,对重复代码进行抽象封装后进行调用

拆分合并同样适用于方法层级,但对方法进行拆分时,一定要注意保持方法的深度;方法拆分的前提一定是拆分后,依然能快速的了解掌握如何进行方法调用,降低认知符负荷 拆分方法一定要更加抽象才有意义

第十章:定义不存在的错误

作者认为异常是软件系统中复杂度最严重的一个来源.由于开发人员在定义异常时往往不考虑上层,导致依赖方处理这种异常情况显然要比处理正常情况要复杂的多
大型的分布式系统中,还需要引入专门的异常处理中间件或者自己通过大量代码来实现对异常的处理,以确保服务运行的正确性。尤其是处理分布式事务,处理不当,极易导致数据不一致
而且处理异常时,还可能增加新的异常,比如关闭IO流。再比如在二阶段提交中,如果由于网络延迟导致失败,但请求最终又发送到了相关节点,处理起来也很复杂
所以作者认为支持异常会导致代码显得笨拙而且冗余
作者举了一个Java读取文件的例子,通过try-with-resourses的方式来进行文件读取时,可能需要处理多个检查型异常,让代码显得难以阅读。(想到这里不得不说,GO处理异常的方式真的很明智,不抛出异常,而是作为结果返回,这样就直接明确的知道异常的信息,不用像Java一样catach的很多分支)
增加过多的异常,只会加剧处理时的复杂度。(对于大多数业务异常,定义一个异常并通过错误业务码进行区分是一个不错的方法)
作者认为随便抛异常是一种不负责的表现,把问题丢给调用者来解决并不能真的减少整个系统的复杂度.而且异常会成为接口的一部分,一个接口添加过多异常的话,也会变相的将class实现变浅。同时多层级的调用下,异常不止会影响直接调用者,异常信息栈可能会影响更上层的业务或模块
而消除异常的方式也很简单,就是不定义异常
作者举了一个很生动的例子:Windows和Unix下分别删除被其他进程打开的文件,Windows会报错,用户就会懵逼,要花费精力去查找具体是谁使用了这个文件;Unix就很优雅,先将文件标为删除状态,新的进程无法打开,等之前的进程停掉后再真正删除
另外一个消除异常的方式就是屏蔽异常,底层自己处理相关异常,并作出相关反应,屏蔽上层的感知;例如TCP的滑动窗口,通过滑动窗口可以屏蔽包丢失对上层的影响
还有一种方式就是将异常聚合在一起,减少数量
最极端的一种情况就是遇到异常就直接crash,例如OOM异常或者StackOverFlow异常

第十一章:设计两次

软件设计是一项很有挑战的工作,仅是考虑如何设计一个模块或系统可能并不能做出一份很好的答案,而设计两次(design it twice)就会是一个不错的方式
所谓设计两次个人理解还是尽量做出不同的方案,即使知道可能只有一个方法是可行的,也尽量思考出另一个方法,也就是说:要多尝试一下不同的方法
也许一个解决方案不可行,但他可能能够发现另外一个方案的弱点,这样就可以相互促进
通过列出所有方案的优缺点,可以很容易的进行抉择,选出一个最好的方案;甚至于通过这种方式的对比,可能还可以进行重新组合,重新设计一个比所有选择都要好的方案
两次设计原则同样的适用于所有分层。经常使用这一原则进行设计的话,作者认为可以提高设计技巧:经常对多种方案进行对比,会更容易发现一些影响设计方案的核心的要素

第十二章:写注释

代码注释可以帮助开发人员更加高效的理解系统,更加有效的进行工作,而且功效不止这些。在抽象中,文档也扮演着重要作用。
作者认为好的文档可以隐藏模块的复杂度,提高改善系统设计;相反的,缺少文档的设计其价值也会降低。而且缺少文档会对开发工作造成不必要的拖累。

作者认为好的代码可以减少所需的注释数量,但想完全达到代码自注释还是一个比较理想化的想法 好的注释可以降低认知负荷,减少未知的未知,并且可以消除模糊性
还有很重要的一点就是同样的一个设计思路,在一个地方编写相关注释就可以了,要避免重复注释,减少注释维护的工作量

第十三章:怎么样写好注释

注释的目的是降低使用者的使用难度,所以注释的原则就是:对代码中模糊的(不容易理解的)地方进行注释
写注释的重要原因之一是为了抽象,注释中可以体现很多代码中细节,例如JDK中的注释;但注释可以提供一种更加简单,更加高级的视图
对于注释,不要对一些很简单的代码进行注释,这样不会有太多帮助;注释时,可以考虑包含一下低层级的信息,这样可以对当前层级有一个很精细的补充
注释应该清晰明了,比如对于一些入参,要注明单位,对于一些范围类的查询,应该表明开闭区间
注释应该尽量从高层级来看当前层级,这样的话,可以对整体有一个更清晰的介绍
想要成为一名优秀的设计师,除了能考虑更多细节并很好的处理之外,还要往后退一步,从更高层次来看系统,这样就可以思考系统中哪些内容更加重要,能够从系统最基本的特性来思考
所谓抽象,其实就是用简单的方式来思考复杂的东西
作者认为如果想要想要对抽象进行详细描述的唯一方式就是添加注释

写注释时,应该注意两项:what,why
what:让读者(或者用户)理解代码在做什么
why:对于一些代码中比较难以理解的部分,要解释为什么这么做

第十四章:选择好的命名

代码自注释永远都不会过时,好的命名就是注释
命名的时候,应该要确保其一致性以及精准性;精准性就是指命名不能过于宽泛,不能模糊不清
作者认为如果一个变量很难进行精准,直观且精简的词来命名,那就说明这个变量的定义可能存在问题,它的作用可能就不是那么清晰

第十五章:先写注释也许是一个好方法

作者认为注释写的质量差的一个很大原因就是写注释往往都是在完成代码和单元测试之后才开始写,他认为整个开发过程完成之后再写注释被过度推迟了。
如果一直推迟写注释文档的话,会导致延后的工作不停堆积,直到写注释文档都变成一份比较耗时的工作。
而且先写注释本身也反应了一个设计的过程,可以帮你理清思路。评论写的早,还有助于发现设计中的缺陷,及时对设计进行调整

第十六章:修改旧代码

软件开发就是不断迭代的过程。大型系统通过一系列迭代来最终成为大型系统,这就意味着,每次迭代我们都会添加一些新的功能,修改一些旧有的代码。
修改旧有代码时,还要注意尽量不要增加复杂度
用战略性编程的思想,来修改老代码,不能只想着改动最少的代码来实现当次需求或者修复bug,不能只担心修改代码引进的风险。要基于本次变更,思考一下是否有更好的方式来重新设计这部分内容。持续性的对代码进行重构是非常有必要的。
同时在修改老代码的时候,一定要注意同时对注释进行必要的维护;只是在提交日志中对修改内容进行描述是不够的

第十七章:一致性

所谓系统一致性是指:相似的东西都是用相似的方式来实现(similar things are done in similar ways),而且不同的事情必须用不同的方式来完成(dissimilar things should be done in different ways)
一致性包含方方面面

  1. 命名:同样的命名需要代表同样或者近似的含义
  2. 编码风格
  3. 接口,一个接口的多个实现所最终呈现的内容要是一致的
  4. 设计模式
  5. 不变性,变量或者数据结构一个默认的属性;比如每行的结尾,总是用一个换行符来表示。个人觉得就是约定俗成的一些东西比如请求成功返回OK的状态,失败返回一个ERROR的状态 系统设计也要考虑约定好的东西,不能随便去更改它以避免造成不必要的麻烦

第十八章:代码应该是易懂的(Code Should be Obvious)

模糊性是导致复杂度的两个重要原因之一,解决方案之一就是用一种更加易懂的方式来写代码
其实就是要代码可阅读,毕竟代码写来是给人读的
通过良好的命名方式,以及保持系统一致性,可以促使代码更加易懂
良好的代码风格(包括注释的格式)可以帮主读者更加轻易的读懂代码

for(int pass=1;pass>=0&&!empty;pass--) {

for (int pass = 1; pass >= 0 && !empty; pass--) { 

例如上面两行代码,很明显,下面的这行读着更加让人愉悦
总之,代码应该是更加让人易读,而不是让自己容易写完相关功能

第十九章:系统设计趋势

面向对象编程的思想是很好的一种对复杂度的封装
继承是面向对象的关键要素之一,包括两种形式:

  1. 通过接口实现的方式
  2. 通过类继承的方式 虽然作者认为类继承可以减少重复代码,减少变更放大,但同样认为这样由于造成了父类和子类之间的依赖,导致了信息泄漏,开发人员也修改相关代码时,就需要扩大自己的认知到上层父类;或者说修改了父类之后,会影响多个子类 所以作者认为是用类继承导致的类的分层会提高复杂度
    敏捷开发模式中,每个迭代都会有设计、开发、测试阶段,和作者的一些想法是一致的,通过不断的迭代来优化系统设计
    但作者同时认为,敏捷开发容易走入战术编程的误区,因为敏捷开发过于强调集中精力于特性开发
    设计模式现在非常流行,但作者认为设计模式的滥用会造成一定的模糊,任何问题都硬套设计模式,并不能改善系统的设计
    对于审核系统设计而言,都要学会从复杂度为出发点来看待问题

第二十章:考虑性能

设计时,保持系统的简单,同时还要考虑性能
围绕着关键路径进行性能优化:
明确满足功能的最少代码,也许这会和实际情况相悖,但依然能提供一个非常好的思路
还有一个就是尽量移除关键路径中的特殊处理

第二十一章:总结

本书主要就是讲述了一件事情:复杂度。 软件设计就是不停和复杂度做斗争的过程。
而本书则指出了复杂度的两个根本性原因:依赖性和模糊性
本书也表明了如何避免一些不必要的复杂度:信息泄漏,不必要的错误处理,命名过于宽泛
设计系统的过程中,要永远保持一个投资的心态
设计是一个解谜的过程:如何用最简单的方式来解决问题
毕竟花时间对系统进行良好设计,比花时间去修复bug要更加有意思和吸引力