代码精进之路:从码农到工匠

张建飞

序二

  • 近几年的银弹是微服务,但是微服务需要更强的业务建模能力和技术管理能力,否则实现和维护微服务系统只能是难上加难。
  • 真正的技术Leader是能够创建并且演进架构,在架构层面上帮助大家比较容易地写出好代码的人;是能够创建良好的技术氛围,以写好代码为荣,以写坏代码为耻,促使大家不停学习的人。
  • 即使我们的工作是在维护“屎山”(Shit Mountain),也请不要忘记时时仰望星空……

前言

  • 我有一个梦想,我写的代码,可以像诗歌一样优美。我有一个梦想,我做的设计,能恰到好处,既不过度,也无不足。
  • 不管你们有多敬业,加多少班,在面对烂系统时,你仍然会寸步难行,因为你大部分的精力不是在应对开发需求,而是在应对混乱。
  • “软件的复杂性是一个基本特征,而不是偶然如此。”问题域有其复杂性,而软件在实现过程中又有很大的灵活性和抽象性,导致软件具有天然的复杂性。
  • 整洁面向对象分层架构(Clean Object-oriented and LayeredArchitecture,COLA)
  • 一个程序员的“美德”就在于他是否能为后人留下一段看得懂、可维护性好的代码。
  • 需要我们对自己的思维习惯、学习方法和工程实践进行彻底的反省和重构。
  • 任何不区分上下文和情景的教条都有可能在实施过程中遭遇惨败。
  • 软件工程就是一个对现实世界的问题进行分析、抽象、建模,然后转换成计算机可以理解的语言,解释执行,实现特定业务逻辑的过程。
  • 如果说思想是务虚的最高境界,那么实践就是务实的最低要求。
  • 种一棵树最好的时间是十年前,其次是现在
  • 管理者的一个很重要的使命就是帮助团队成长,包括制定规范和技术传承。

第一部分 技艺

  • 名为万物之始,万物始于无名,道生一,一生二,二生三,三生万物。
  • 一个名字虽然并不影响程序的执行,但是却对代码的表达力和可读性有着重要的影响。

1.2 命名其实很难

  • 命名为什么难呢?因为命名的过程本身就是一个抽象和思考的过程,在工作中,当我们不能给一个模块、一个对象、一个函数,甚至一个变量找到合适的名称的时候,往往说明我们对问题的理解还不够透彻,需要重新去挖掘问题的本质,对问题域进行重新分析和抽象,有时还要调整设计和重构代码。因此,好的命名是我们写出好代码的基础。
  • 在计算机科学中有两件难事:缓存失效和命名

1.3 有意义的命名

  • 代码即文档,可读性好的代码应该有一定的自明性,也就是不借助注释和文档,代码本身就能显性化地表达开发者的意图。
  • 变量名应该是名词,能够正确地描述业务,有表达力。
  • 类似的还有魔术数,数字86400应该用常量SECONDS_PER_DAY来表达;每页显示10行记录的,10应该用PAGE_SIZE来表达。
  • 函数命名要具体,空泛的命名没有意义。
  • 函数的命名要体现做什么,而不是怎么做
  • 对于一个应用系统,我们可以将类分为两大类:实体类和辅助类。
  • 对于辅助类,尽量不要用Helper、Util之类的后缀,因为其含义太过笼统,容易破坏SRP(单一职责原则)。比如对于处理CSV,可以这样写:
  • 包(Package)代表了一组有关系的类的集合,起到分类组合和命名空间的作用。
  • 包名应该能够反映一组类在更高抽象层次上的联系。
  • 包的命名要适中,不能太抽象,也不能太具体。

1.4 保持一致性

  • 保持命名的一致性,可以提高代码的可读性,从而简化复杂度
  • 每个概念对应一个词,并且一以贯之。
  • 如果你要用类似Total、Sum、Average、Max、Min这样的限定词来修改某个命名,那么记住把限定词加到名字的最后,并在项目中贯彻执行,保持命名风格的一致性。
  • 为了避免Num带来的麻烦,我建议用Count或者Total来表示总数,用Id表示序号。
  • 统一语言就是要确保团队在内部的所有交流、模型、代码和文档中都要使用同一种编程语言。实际上,统一语言(Ubiquitous Language)也是领域驱动设计(Domain Driven Design,DDD)中的重要概念

1.5 自明的代码

  • 我们可以通过添加中间变量让代码变得更加自明,即将计算过程打散成多个步骤,并用有意义的变量名来命名中间变量,从而把隐藏的计算过程以显性化的方式表达出来。
  • 只要把计算过程打散成一系列良好命名的中间值,不透明的语义自然会变得透明。
  • 如果注释是为了阐述代码背后的意图,那么这个注释是有用的;如果注释是为了复述代码功能,那么就要小心了,这样的注释往往意味着“坏味道”
  • 注释要能够解释代码背后的意图,而不是对功能的简单重复
  • 这里的注释和没写是一样的,因为它只是对sleep的简单复述。正确的做法应该是阐述sleep背后的原因,比如改写成如下形式就会好很多。

1.6 命名工具

  • 作者一般会安装一个叫作OnlineSearch的插件,插件里自带了像SearchCode这样的代码搜索工具,也可以自己配置像Codelf这样的代码搜索工具。

1.7 本章小结

  • 命名的重要性不仅体现在提升代码的可读性上,有意义的命名更能够引导我们更加深入地理解问题域,理清关键业务概念,进行合理的业务抽象,从而设计出更加符合业务语义、易于理解的系统。
  • 了解如何给软件制品(Artifact,包括Module、Package、Class、Function和Variable)命名,如何写注释,如何让代码自明地表达自己,以及如何保持命名风格的一致性。

第2章 规范

  • 离娄之明,公输子之巧,不以规矩,不能成方圆。
  • 事物的复杂程度在很大程度上取决于其有序程度,减少无序能在一定程度上降低复杂度,这正是规范的价值所在。通过规范,把无序的混沌控制在一个能够理解的范围内,从而帮助我们减少认知成本,降低对事物认知的复杂度。

2.1 认知成本

  • 所谓认知,是指人们获得知识或应用知识的过程。获得知识是要学习的,在学习过程中,我们要交的学费叫作认知成本。
  • 发现共同抽象和机制可以在很大程度上帮助我们理解复杂系统。

2.2 混乱的代价

  • 认知是有成本的,而混乱的代价在于让我们对事物无法形成有效的记忆和认知,导致我们每次面对的问题都是新问题,每次面临的场景都是新场景,又要重新理解一遍。
  • 规范的缺失会导致工程师不知道应用中有哪些制品(Artifact)、如何给类命名、一个类应该放在哪个包(Package)或哪个模块(Module)里比较合适、错误码应该怎样去写、什么时候该打印日志、选用哪个日志级别。
  • 我们有限的精力用在刀刃上,而不是用来疲于应对各种不一致和随心所欲的混乱。

2.3 代码规范

  • 一个简单的原则就是将概念相关的代码放在一起:相关性越强,彼此之间的距离应该越短。
  • 开发人员应在一开始就养成良好的撰写日志的习惯,并在实际的开发工作中为写日志预留足够的时间。
  • 我认为比较有用的4个级别依次是ERROR、WARN、INFO和DEBUG。通常这4个级别就能够很好地满足我们的需求了。
  • ERROR表示不能自己恢复的错误,需要立即被关注和解决
  • 对于可预知的业务问题,最好不要用ERROR输出日志,以免污染报警系统。例如,参数校验不通过、没有访问权限等业务异常,就不应该用ERROR输出。
  • INFO用于记录系统的基本运行过程和运行状态。
  • 为了防止日志量过大,我们可以采用分布式配置工具来实现基于requestId判断的日志过滤,从而只打印我们所需请求的DEBUG日志。
  • 针对以上问题,我建议在业务系统中设定两个异常,分别是BizException(业务异常)和SysException(系统异常),而且这两个异常都应该是Unchecked Exception。
  • 最后,针对业务异常和系统异常要做统一的异常处理,类似于AOP,在应用处理请求的切面上进行异常处理收敛
  • 千万不要在业务处理内部到处使用try/catch打印错误日志,这样会使功能代码和业务代码缠绕在一起,让代码显得很凌乱,并且影响代码的可读性。
  • 错误码非常重要,一定要在系统搭建之初就制定好相应的规范,否则当系统上线后,系统的错误码已经对前端或者外部系统进行了透出,再重构的可能性就很小了。
  • 我们可以将错误码定义成3个部分:类型+场景+自定义标识。每个部分之间用下划线连接,内容以大驼峰的方式书写。

2.4 埋点规范

  • 有一句话叫“业务数据化、数据业务化”,即业务要沉淀数据、数据要反哺业务。
  • 在阿里巴巴有一个超级位置模型(Super Position Model,SPM)的埋点规范,用于统计分析各种场景的用户行为数据。

2.5 架构规范

  • 规范对于架构来说至关重要。从某种意义上来说,架构就是一组约束,遵从了这些约束,才能符合架构要求;反之,架构将失去意义。

2.6 防止破窗

  • 环境中的不良现象如果被放任存在,就会诱使人们仿效,甚至变本加厉
  • 从“破窗效应”中我们可以得到这样一个道理:任何一种已存在的不良现象都在传递着一种信息,会导致不良现象无限扩展,同时必须高度警觉那些看起来是偶然的、个别的、轻微的“过错”,如果对“过错”不闻不问、熟视无睹、反应迟钝或纠正不力,就会纵容更多的人“去打烂更多的窗户”,极有可能演变成“千里之堤,溃于蚁穴”的恶果。
  • 首先,我们要有一套规范,并尽量遵守规范,不要做“打破第一扇窗”的人;其次,发现有“破窗”,要及时地修复,不要让事情进一步恶化。整洁的代码需要每个人的精心呵护,需要整个团队都具备一些工匠精神。

2.7 本章小结

  • 留给公司一个方便维护、整洁优雅的代码库,是我们技术人员的最高技术使命,也是我们对公司做出的最大技术贡献。

第3章 函数

  • 把简单的事情做到极致,功到自然成,最终“止于至善”。

3.2 软件中的函数

  • 在英语中,Function一般代表函数式语言中的函数,而Method代表面向对象语言中的函数。

3.3 封装判断

  • 如果把解释条件意图作为函数抽离出来,用函数名把判断条件的语义显性化地表达出来,就能立即提升代码的可读性和可理解性。

3.4 函数参数

  • 最理想的参数数量是零(零参数函数),其次是一(一元函数),再次是二(二元函数),应尽量避免三(三元函数)
  • 如果函数需要3个以上参数,就说明其中一些参数应该封装为类了。

3.5 短小的函数

  • 如果是Java语言,我建议一个方法不要超过20行代码

3.6 职责单一

  • 一个方法只做一件事情,也就是函数级别的单一职责原则(Single Responsibility Principle,SRP)。

3.7 精简辅助代码

  • 所谓的辅助代码(Assistant Code),是程序运行中必不可少的代码,但又不是处理业务逻辑的核心代码,比如判空、打印日志、鉴权、降级和缓存检查等。
  • Java 8引入了一个很有趣的特性——Optional类。Optional类主要解决的问题是“臭名昭著”的空指针异常。Optional类是一个包含可选值的包装类,意味着Optional类既可以含有对象,也可以为空。使用Java 8的这个新特性和新语法,我们可以用Optional来代替冗长的null检查:
  • 对于那些对可用性要求非常高的场景,有必要制定一个服务降级的策略,以便当其中一个服务不可用时,我们仍然能够对外提供服务。

3.8 组合函数模式

  • 组合函数要求所有的公有函数(入口函数)读起来像一系列执行步骤的概要,而这些步骤的真正实现细节是在私有函数里面。组合函数有助于代码保持精炼并易于复用。阅读这样的代码就像在看一本书,入口函数是目录,目录的内容指向各自的私有函数,而具体的内容是在私有函数里实现的。
  • 而且并不会涉及什么高深的思想,只要我们愿意,很多时候只需要多做一点点,就可以写出更好的代码,这也是“工匠精神”的一种体现
  • 我不是一个伟大的程序员,只是习惯比较好而已。

3.9 SLAP

  • 抽象层次一致性(Single Level of Abstration Principle,SLAP)
  • 在构筑金字塔的过程中,要求金字塔的每一层要属于同一个逻辑范畴、同一个抽象层次。

3.10 函数式编程

  • 在函数式编程中,函数不仅可以调用函数,也可以作为参数被其他函数调用。
  • ·减少冗余代码,让代码更简洁、可读性更好。·函数是“无副作用”的,即没有对共享的可变数据操作,可以利用多核并行处理,而不用担心线程安全问题。

3.11 本章小结

  • 一个系统容易腐化的部分正是函数,不解决函数的复杂性,就很难解决系统的复杂性。

第4章 设计原则

  • 每个人都有义务捍卫、遵守或完善原则。原则可以修正,但是不能肆意妄为。
  • 所谓原则,就是一套前人通过经验总结出来的,可以有效解决问题的指导思想和方法论

4.1 SOLID概览

  • Single Responsibility Principle(SRP):单一职责原则。 ·Open Close Principle(OCP):开闭原则。 ·Liskov Substitution Principle(LSP):里氏替换原则。 ·Interface Segregation Principle(ISP):接口隔离原则。 ·Dependency Inversion Principle(DIP):依赖倒置原则。
  • 开闭原则和里氏代换原则是设计目标;单一职责原则、接口分隔原则和依赖倒置原则是设计方法。

4.2 SRP

  • SRP要求每个软件模块职责要单一,衡量标准是模块是否只有一个被修改的原因。职责越单一,被修改的原因就越少,模块的内聚性(Cohesion)就越高,被复用的可能性就越大,也更容易被理解。

4.3 OCP

  • 软件实体应该对扩展开放,对修改关闭。
  • 装饰者模式,可以在不改变被装饰对象的情况下,通过包装(Wrap)一个新类来扩展功能;策略模式,通过制定一个策略接口,让不同的策略实现成为可能;适配器模式,在不改变原有类的基础上,让其适配(Adapt)新的功能;观察者模式,可以灵活地添加或删除观察者(Listener)来扩展系统的功能。

4.4 LSP

  • 程序中的父类型都应该可以正确地被子类型替换。
  • 根据LSP的定义,如果在程序中出现使用instanceof、强制类型转换或者函数覆盖,很可能意味着是对LSP的破坏。
  • 可以通过提升抽象层次来解决此问题,也就是将子类中的特有函数用一种更抽象、通用的方式在父类中进行声明。
  • 正方形-矩形问题(Square-rectangle Problem)

4.5 ISP

  • 接口隔离原则认为不能强迫用户去依赖那些他们不使用的接口。换句话说,使用多个专门的接口比使用单一的总接口要好。
  • 满足ISP之后,最大的好处是可以将外部依赖减到最少。你只需要依赖你需要的东西,这样可以降低模块之间的耦合(Couple)。

4.6 DIP

  • DIP要求高层模块不应该依赖于低层模块,二者都应该依赖于抽象。抽象不应该依赖细节,细节应该依赖抽象。
  • 依赖倒置,就是要反转依赖的方向,让原来紧耦合的依赖关系得以解耦,这样依赖方和被依赖方都有更高的灵活度。

4.7 DRY

  • DRY是Don’t Repeat Yourself的缩写,DRY原则特指在程序设计和计算中避免重复代码,因为这样会降低代码的灵活性和简洁性,并且可能导致代码之间的矛盾。

4.8 YAGNI

  • YAGNI是针对“大设计”(Big Design)提出来的,是“极限编程”提倡的原则,是指你自以为有用的功能,实际上都是用不到的。因此,除了核心的功能之外,其他的功能一概不要提前设计,这样可以大大加快开发进程。它背后的指导思想就是尽可能快、尽可能简单地让软件运行起来。

4.9 Rule of Three

  • Rule of Three也被称为“三次原则”,是指当某个功能第三次出现时,就有必要进行“抽象化”了。

4.10 KISS原则

  • 他认为把事情变复杂很简单,把事情变简单很复杂。好的目标不是越复杂越好,反而是越简洁越好。

4.11 POLA原则

  • POLA(Principle of least astonishment)是最小惊奇原则,写代码不是写侦探小说,要的是简单易懂,而不是时不时冒出个“Surprise”。

4.12 本章小结

  • 软件是一种平衡的艺术。要清楚一点,我们不是为了满足这些原则而工作的,原则只是背后的指导思想。我们的目的是构建可用的软件系统,并尽量减少系统的复杂度。在不能满足所有原则时,要懂得适当取舍。

第5章 设计模式

  • 利用模式,我们可以让一个解决方案重复使用,而不是重复造轮子。
  • 设计模式描述了在软件设计过程中的一些不断重复发生的问题,以及该问题的解决方案,具有一定的普遍性,可以反复使用。其目的是提高代码的可重用性、可读性和可靠性。
  • 设计模式的本质是面向对象设计原则的实际运用,是对类的封装性、继承性和多态性,以及类的关联关系和组合关系的充分理解

5.2 GoF

  • 结构型模式:用于描述如何将类或对象按某种布局组成更大的结构,GoF中提供了代理、适配器、桥接、装饰、外观、享元、组合7种结构型模式。
  • 适配器(Adapter)模式:将一个类的接口转换成客户希望的另一个接口,使得原本由于接口不兼容而不能一起工作的那些类能一起工作。

5.4 插件模式

  • 插件(plug-in)模式扩展方式和普通的对象扩展方式的不同之处在于,普通的扩展发生在软件内部,插件式扩展发生在软件外部
  • 插件模式的难点不在于如何开发插件,而在于如何实现一套完整的插件框架。

5.5 管道模式

  • 一个典型的管道模式,会涉及以下3个主要的角色。 (1)阀门:处理数据的节点。 (2)管道:组织各个阀门。 (3)客户端:构造管道并调用。
  • 翻看JDK源码,你会发现,支撑Stream API背后的原理正是管道模式。在构建Stream时,会调用核心类ReferencePipeline来创建管道,其内部采用双向列表的数据结构对操作(Operation)进行存放,然后包(wrap)成Sink链表等待执行,整个处理是延迟执行的,只有在最后收集(Collect)被调用时才会被执行。

第6章 模型

  • 建模的艺术就是去除实在中与问题无关的部分。
  • 如果把写代码比喻成“施工”,那么架构和建模就是“设计图纸”。

6.1 什么是模型

  • 模型是对现实世界的简化抽象
  • 模型大致可以分为物理模型、概念模型、数学模型和思维模型等。
  • 我们把用简单易懂的图形、符号或者结构化语言等表达人们思考和解决问题的形式,统称为思维模型

6.2 UML

  • 推荐阅读Grady Booch等人的《面向对象分析与设计》和Larman的《UML和模式应用》这两本书。

6.3 类图

  • 类(Class)封装了数据和行为,是面向对象的重要组成部分,是具有相同属性、操作、关系的对象集合的总称。
  • 可见性:表示该属性对于类外的元素而言是否可见,包括公有(public)、私有(private)和受保护(protected),在类图中分别用符号+、-和#表示。
  • 类和类之间的关系主要有关联关系、依赖关系和泛化关系
  • 在UML类图中,用实线连接有关联关系的对象所对应的类。在代码实现上,通常将一个类的对象作为另一个类的成员变量。
  • 类的关联关系也可以是单向的,单向关联用带箭头的实线表示
  • 在系统中可能会存在一些类的属性对象类型为该类本身,这种特殊的关联关系称为自关联。
  • 在用代码实现聚合关系时,成员对象通常作为构造方法、Setter方法或业务方法的参数注入整体对象中
  • 组合(Composition)关系也表示类之间整体和部分的关联关系,但是在组合关系中,整体对象可以控制成员对象的生命周期,一旦整体对象不存在,成员对象也将不存在,成员对象与整体对象之间具有“同生共死”的关系。
  • 依赖(Dependency)关系是一种使用关系,特定事物的改变可能会影响到使用该事物的其他事物,在需要表示一个事物使用另一个事物时,使用依赖关系。
  • 泛化(Generalization)关系也称为继承关系,用于描述父类与子类之间的关系。父类称为基类或超类,子类称为派生类
  • 在UML中,类与接口之间的实现关系通常是用带空心三角形的虚线来表示。

6.4 领域模型

  • 从本质上来说,软件开发过程就是问题空间到解决方案空间的一个映射转化

6.6 广义模型

  • C4模型由Simon Brown提出。C4模型提出使用上下文(Context)、容器(Container)、组件(Component)和代码(Code)等一系列分层的图表,来描述不同缩放级别的软件架构

7.1 什么是DDD

  • 是指通过统一语言、业务抽象、领域划分和领域建模等一系列手段来控制软件复杂度的方法论。

7.2 初步体验DDD

  • 评审完需求,工程师画几张UML图完成设计,就开始像上面这样写业务代码了,这样写基本不用太动脑筋,完全是过程式的代码风格。

7.3 数据驱动和领域驱动

  • 数据驱动以数据库为中心,其中最重要的设计是数据模型,但随着业务的增长和项目的推进,软件开发和维护的难度会急剧增加。
  • 领域模型对应的是业务实体,在程序中主要表现为类、聚合根和值对象,它更加关注业务语义的显性化表达,而不是数据的存储和数据之间的关系
  • 复杂的数据库关系和对象关系之间的差异,其本质是数据模型和领域模型之间的差异,而这种差异的多样性和灵活性是很难通过规则预先定义的,这也是为什么工具的作用会很有限

7.4 DDD的优势

  • 目标是创造可以被业务、技术和代码自身无歧义使用的共同术语,即统一语言
  • 在软件的世界里,任何的方法论如果最终不能落在“减少代码复杂度”这个焦点上,那么都是有待商榷的。
  • 代码复杂度是由业务复杂度和技术复杂度共同组成的

7.5 DDD的核心概念

  • 当这样的行为从领域中被识别出来时,推荐的实践方式是将它声明成一个服务。
  • 识别领域服务,主要看它是否满足以下3个特征。(1)服务执行的操作代表了一个领域概念,这个领域概念无法自然地隶属于一个实体或者值对象。(2)被执行的操作涉及领域中的其他对象。(3)操作是无状态的。
  • 事件是表示发生在过去的事情,所以在命名上推荐使用Domain Name +动词的过去式+ Event,这样可以更准确地表达业务语义。
  • 边界上下文(Bounded Context)的作用是限定模型的应用范围,在同一个上下文中,要保证模型在逻辑上统一,而不用考虑它是不是适用于边界之外的情况。

7.6 领域建模方法

  • 任何的业务事件都会以某种数据的形式留下足迹。
  • 所以企业的业务系统的主要目的之一,就是记录这些足迹,并将这些足迹形成一条有效的追溯链。
  • (1)首先以满足管理和运营的需要为前提,寻找需要追溯的事件,或者称为关键业务时刻。(2)根据这些需要追溯,寻找足迹以及相应的关键业务时刻对象。(3)寻找“关键业务时刻”对象周围的“人-事-物”对象。(4)从“人-事-物”中抽象出角色。(5)把一些描述信息用对象补足。

7.7 模型演化

  • 世界上唯一不变的就是变化,模型和代码一样,也需要不断地重构和演化。

7.8 为什么DDD饱受争议

  • 核心业务逻辑和技术细节相分离

第二部分 思想

  • 若想捉大鱼,就得潜入深渊。深渊里的鱼更有力,也更纯净。硕大而抽象,且非常美丽。

8.2 到底什么是抽象

  • 抽象和具象是相对应的概念,“抽”就是抽离,“象”就是具象。
  • 抽象是指为了某种目的,对一个概念或一种现象包含的信息进行过滤,移除不相关的信息,只保留与某种最终目的相关的信息。
  • 抽象更接近问题的本质。

8.3 抽象是OO的基础

  • 面向对象的思想主要包括3个方面:面向对象的分析(Object Oriented Analysis,OOA)、面向对象的设计(Object Oriented Design,OOD),以及我们经常提到的面向对象的编程(Object Oriented Programming,OOP)。

8.4 抽象的层次性

  • 软件领域的任何问题,都可以通过增加一个间接的中间层来解决。

8.5 如何进行抽象

  • 简单来说,抽象的过程就是合并同类项、归并分类和寻找共性的过程。
  • 在开发工作中,很多时候就需要通过抽象层次的提升来提高代码的可读性和通用性。
  • 因此,每当我们有强制类型转换,或者使用instanceof时,都值得停下来思考一下,是否需要做抽象层次的提升。
  • 要自下而上地思考,总结概括;自上而下地表达,结论先行。

8.6 如何提升抽象思维

  • 为什么阅读书籍比看电视更好呢?因为图像比文字更加具象,阅读的过程可以锻炼我们的抽象能力、想象能力,而看画面时你的大脑会被铺满,较少需要抽象和想象。
  • 小时候,我们可能不理解语文老师为什么总是要求我们总结段落大意、中心思想。现在回想起来,这种思维训练在基础教育中是非常必要的,其实质就是帮助学生提升抽象思维的能力。
  • 现实世界纷繁复杂,只有具备较强的抽象思维能力的人,才能够具备抓住事物本质的能力。

8.7 本章小结

  • 归纳总结,合并同类项是进行抽象活动时最有效的方法

9.1 分治算法

  • 分治法解题的一般步骤如下。(1)分解:将要解决的问题划分成若干规模较小的同类问题。(2)求解:当子问题划分得足够小时,用较简单的方法解决。(3)合并:按原问题的要求,将子问题的解逐层合并,构成原问题的解。

9.2 函数分解

  • 函数过大过长是典型的代码“坏味道”,意味着这个函数可能承载着过多的职责,我们有必要“分治”一下,将大函数分解成多个短小、易读、易维护的小函数。

9.3 写代码的两次创造

  • 优雅的代码很少是一次成形的,大部分情况下要经过两次创造:第一遍实现功能,第二遍重构优化。

9.5 分层设计

  • 分层设计最大的好处是分离关注(Separation of concerns),这样我们就可以通过分层隔离简化一个复杂的问题,让每一层只对上一层负责,从而使每一层的职责变得相对简单。
  • 分层架构的目的是通过分离关注点来降低系统的复杂度,同时满足单一职责、高内聚、低耦合、提高可复用性和降低维护成本,也是一种典型的分治思想。

9.6 横切和竖切

  • 所谓竖切,就是按照领域将单体数据库拆分成多个数据库。

10.1 不教条

  • 软件的第一性原理是“控制软件复杂度”
  • 软件开发的生命周期风格类似一个连续光谱——有从瀑布式到敏捷,以及它们之间的多种可能性。
  • 我们在实际操作中应重视指导原则,弱化方法论。
  • 贫血模式提倡模型对象只包含数据,并提供简单的Getter和Setter;而充血模式提倡数据和行为放在一起,是一种更加面向对象的做法。
  • 问题的核心不在于行为和数据是否在一起,而在于你能否有效地控制复杂度

10.2 批判性思维

  • 批判性思维(Critical Thinking)是一种谨慎运用推理去断定一个断言是否为真的能力。它要求我们保持思考的自主性和逻辑的严密性,不被动地全盘接受,也不刻意地带着偏见去驳斥一个观点。

10.3 成长型思维

  • 她在《终身成长》中提醒我们:我们获得的成功并不是能力和天赋决定的,更多受到我们在追求目标的过程中展现的思维模式的影响。
  • 具有成长型思维的人相信自己可以通过学习来提升自我,相信学习和成长的力量,相信努力可以改变智力和能力

10.4 结构化思维

  • 最清晰和实用的结构化表达是“提出问题,定义问题,分析问题,解决问题,最后展望未来”。如果按照这个逻

10.5 工具化思维

  • (1)最差的境界是“实在懒”,拖延不喜欢的任务。(2)其次是“开明懒”,迅速做完不喜欢的任务,以摆脱之。(3)最高的境界是“智慧懒”,编写某个工具来完成不喜欢的任务,以便再也不用做这样的事情了,从而一劳永逸。
  • 我经常在团队中说,每当你重复同样的工作3次以上,就应该停下来问问自己:我是不是可以通过自动化脚本、配置化,或者小工具来帮助自己提效?

10.6 好奇心

  • 学习的动力不应该来自于外界的强力,而应该来自于内在,来自于我们内心对知识的渴望、对世界的好奇心

10.7 记笔记

  • (1)知识内化:记笔记的过程是一个归纳整理、再理解、再吸收的过程,可以加深我们对知识的理解。(2)形成知识体系:零散的知识很容易被遗忘,而形成知识体系之后,知识之间就能有更强的连接。(3)方便回顾:笔记就像我们的硬盘,当缓存失效后,我们依然可以通过硬盘调回,保证知识不丢失。

10.8 有目标

  • 所有事物都要经过两次创造的原则,第一次为心智上的创造,第二次为实际的创造
  • 在学习之前,我们一定要问自己,这次学习的目标是什么?

10.9 选择的自由

  • 自由并不是想做什么,就做什么。自由是一种价值观,是一种为自己过去、现在及未来的行为负责的价值观。自由是一种责任,是一种敢于做出选择,并愿意为自己的选择承担后果的责任。
  • 处乱世而不惊,临虚空而不惧,喜迎阴晴圆缺,笑傲雨雪风霜

10.10 平和的心态

  • 动机至善,了无私心;用无为的心,做有为的事

11.2 目标管理

  • 不管是KPI的目标设定,还是OKR的KR设定,都需要满足SMART原则。如图11-3所示,S代表Specific,表示指标要具体;M代表Measurable,表示指标要可衡量;A代表Attainable,表示指标是有可能达成的;R代表Relevant,表示KR和O要有一定的相关性;最后,T代表Time bound,表示指标必须具有明确的截止期限。

11.5 Leader和Manager的区别

  • Manager是管理事务,是控制和权威;而Leader是领导人心,是引领和激发
  • 我们不需要这么多‘高高在上’‘指点江山’的技术Manager,而是需要更多能真正深入系统里面,深入代码细节,给团队带来改变的技术Leader。

11.6 视人为人

  • 一群有情有义的人,做一件有意义的事
  • ·对待上级——有胆量。 ·对待平级——有肺腑。 ·对待下级——有心肝。

第三部分 实践

  • 软件的首要技术使命:管理复杂度。
  • 整洁面向对象分层架构(Clean Object-oriented and Layered Architecture,COLA)

12.1 软件架构

  • 软件架构划分成业务架构、应用架构、系统架构、数据架构、物理架构和运维架构

12.2 典型的应用架构

  • 内部不关心外部如何使用端口,从一开始就要假定外部使用者是可替换的。
  • 在我看来,洋葱架构在端口和适配器架构的基础上贯彻了将领域放在应用中心,将驱动机制(用户用例)和基础设施(ORM、搜索引擎、第三方API等)放在外围的思路。