MF的《重构》一书算是程序设计书籍的经典了。其中对于重构的认识和剖析深入浅出,提纲挈领。对于有一定编程经验的人来说更是如虎添翼的帮助。下面我尽量在不贬损原意的基础上,用自己的思路和语言进行适当的总结。

序 & 前言:重构的再认识

开篇名义,还未进入正文,书从序和前言开始,便不自觉间流露着真知灼见:

  • 重构是不改变软件可观察行为的前提下改善其内部结构
  • 重构需要你维护一份“坏味道”和重构手段的对应
  • 设计前期使用模式通常会导致过度工程
  • 代码总将随着设计者的经验成长而进化

样例:感受重构

任何一个傻瓜都能写出计算机理解的代码。但唯有优秀的程序员才能写出人类能理解的代码

代码被阅读和修改的次数远多于被编写的次数。尽管代码在机器中运行时,机器并不会嫌弃代码丑陋。但是代码总是要修改的,当我们打算修改系统时,就涉及到了人。人在乎这些。差劲的系统很难维护,如果很难找到修改点,程序员就可能犯错,从而引入bug。如果你发现你需要为程序增加特性,但是当前的代码结构让你不能方便达成目标时,先重构那个程序,再方便地添加特性。

当然,重构前一定要确认,自己有没有一套可靠的测试机制,因为你需要它来保证重构的基础要素:不修改已有功能。重构中,最好能以微小的步伐前进(这样能及时回滚)。在本章样例的重构中,体现了下面一些“好味道”:

  • 代码块越小,代码功能就越好管理
  • 好的代码应该能够清楚表达自己的功能,变量名也是代码清晰的关键
  • 用多态取代条件判断逻辑
  • 结构化风格相比过程化风格更易扩展也更好维护

原则

本章介绍了重构的一些原则和基础性认识。

  • 何为重构:不改变软件可观察特性的前提下,通过修改内部结构,提高其可理解性。通常情况下和性能优化相对应
    • 两顶帽子:添加新功能和重构应该属于两种截然不同的行为,它们应该分开交替进行
  • 重构的好处
    • 改进软件设计,整理代码让后续的修改更容易
    • 让软件更好理解,准确说出我想要的
    • 帮忙找到bug
    • 提高未来的编程速度
  • 何时重构
    • 事不过三,第一次只管去做,第二次产生反感但还是去做,第三次做类似的事情就去重构
    • 修改bug时重构
    • review代码时重构
  • 间接层和重构:中间层能够允许逻辑共享和意图的分开解释,同时隔离变化和解耦。
    • 提前设计好中间层不如先直接做再重构
    • 少数情况下,中间层只会带来冗余
  • 重构的难题
    • 修改已有API:建议维护新旧两个接口,让用户做出反应后,再迁移。这期间,旧接口应该要调用新接口实现
    • 代码已经无法正常运行时,重写比重构更省事
  • 重构和性能优化:大多数的性能优化集中在小部分代码上。先写出风格良好的代码,再使用性能工具实测数据,对瓶颈处单独优化性能。好的重构也会让性能优化更容易进行

坏味道

在遇到下面一些“味道”时,可能你就需要重构了。

  • 重复代码
  • 函数过长,每当你需要用注释说明点什么时,可以把需要说明的东西写到一个独立函数中
  • 太长的类
  • 函数入参过多
  • 发散式变化:一个类因为多个原因发生不同的变化
  • 霰弹式变化:一个原因引起一个类的多个变化
  • 特性依恋:函数对某个类的兴趣高于自己所在的类
  • 数据泥团:喜欢聚合在一起的零散数据字段
  • 基础类型偏执:对于基础类型如字符串、整型不愿意使用简单类来封装
  • swtich语句
  • 冗余类
  • 夸夸其谈未来性:过度为未来设计
  • 令人迷惑的暂时字段
  • 过度耦合的链式调用,如a.b.c().d(),链上任意类做修改都会影响整个调用
  • 两个类的狎昵关系
  • 异曲同工的类
  • 幼稚的数据类:只有最简单的getter和setter
  • 子类拒绝继承超类的函数或数据
  • 过多的注释

测试体系:重构的保证

前面已经提到数次,重构的前提是不对已经已有行为做改动,这需要测试的帮助。本章对建立测试给了一些简单的介绍。

  • 编写测试代码最有用时机是编程之前
  • 编写一个测试case时,可以先让测试失败,再通过成功验证程序功能
  • 遇到bug时,先添加一个单元测试复现这个bug
  • 测试不能保证程序没有bug,编写测试样例也遵循82原则,当样例已经很多时,它带来的边际效果就没那么好了。应该更多考虑容易出错的边界条件,积极思考如何“破坏代码”。

重构列表

下面分几大方向介绍具体的重构手段。每个手段会分场景、思路、动机、做法来展开。

组织函数

日常工作中,非常容易坏味道中的过长函数,下面的一些重构方式可以帮我们优化这一点。

提炼函数

  • 场景:有一段相对独立的代码可以被组织并独立出来
  • 思路:将这段代码放到一个独立函数中,用函数名解释该函数的用途
  • 动机:有时会遇到过长函数中有一段需要注释才能看明白的代码。将这样相对独立的逻辑拆分成表意的短小函数后,可以让高层函数读起来就像一系列注释。如果提炼可以提高代码清晰度,就算函数名比函数体长都无所谓
  • 做法:用做什么而不是怎么做来为函数命名(如果你想不出一个更有意义的名称,就别动了)。检查是否有临时变量,如果有读取,可以作为入参传递给函数;如果对临时变量甚至有再赋值,那可能还要让函数返回临时变量修改后的值

内联函数

  • 场景:函数本体和名称一样清晰易懂
  • 思路:在函数调用点插入函数本体,然后移除函数
  • 动机:如果函数本体足够简单,且表意清晰,同时调用点有限,不具备多态性。那么出于减少无用中间层的考虑,可以直接使用函数体
  • 做法:注意检查是否有多态性

内联临时变量

  • 场景:一个临时变量只被简单表达式赋值一次,同时妨碍了其他重构手法
  • 思路:将对变量的引用动作,替换成给它赋值的表达式本身
  • 动机:过多的临时变量会妨碍你重构长函数
  • 做法:注意确保表达式没有副作用

以查询替代临时变量

  • 场景:程序中有个临时变量保存了某个表达式的运算结果,同时被多处引用
  • 思路:将表达式提炼成独立函数,在独立变量的所有引用点替换成对新函数的调用
  • 动机:替换成函数后,整个类都可以获得这份信息,同时会减少对该变量的频繁引用带来的重构困难
  • 做法:寻找只被赋值一次的临时变量,对于赋值多次的临时变量使用“分解临时变量”方法先重构,保证提炼出来的函数没有副作用。先不要担心性能问题,等到出现了优化也会比较简单

引入解释性变量

  • 场景:有个复杂的表达式,表意不够清晰
  • 思路:将表达式的值放进一个临时变量,用变量名表意
  • 动机:表达式不如变量名更好阅读。如果临时变量在整个类都有意义,建议直接使用“提炼函数”方法
  • 做法:先判断是否使用“提炼函数”的做法

分解临时变量

  • 场景:某个临时变量被多次赋值,且每次赋值意义不一样
  • 思路:针对每次不同意义的赋值使用不一样的临时变量
  • 动机:临时变量的多义性会增大理解成本
  • 做法:寻找被多次赋值且有多义性的变量,不同的意义使用新的不同临时变量

移除对函数入参的赋值

  • 场景:对函数入参赋值
  • 思路:用新的临时变量取代入参
  • 动机:对入参赋值会混淆按值传递和按引用传递的传参方式
  • 做法:略

用函数对象取代函数

  • 场景:大型函数中代码过于复杂,无法使用“提炼函数”
  • 思路:直接将函数放在单独对象中,将复杂的局部变量变成对象字段,从而可以轻松地在对象中分解这个大型函数到多个小型函数
  • 动机:略
  • 做法
    1. 建立一个新类,用函数用途给这类命名
    2. 在新类中创建final字段保存大型函数所在的对象,即“源对象”
    3. 新类的构造函数使用原函数入参作为入参
    4. 新类中建立computed()函数
    5. 赋值原代码到computed()
    6. 在原函数位置,创建这个新类的一个对象,并调用这个对象的computed()函数
    7. 继续重构新类中的computed()函数

替换算法

  • 场景:某个算法有更清晰的算法替代
  • 思路:直接更换函数本体
  • 动机:略
  • 做法:略

对象间的特性搬移

类应该承担清晰且明确的责任。不论是承担责任过多还是“不怎么负责任”,都需要进行重构。

搬移函数

  • 场景:有函数和所在类以外的其他类反而有更多交流,如调用或被调用
  • 思路:在和函数交流更多的类中建立一个有类似行为的新函数,改造旧函数为新函数的委托函数,或者直接移除旧函数
  • 动机:略
  • 做法:
    • 检查和搬移函数关联的字段或函数,判断是否要一起搬移
    • 检查子类和超类有无其他声明,检查有无多态性
    • 如果目标函数需要太多源类特性,就需要进一步分解后再搬移

搬移字段

  • 场景:某个字段和所在类以外的其他类有更多交流
  • 思路:在目标类新建字段,修改源字段的所有使用者,令它们使用新字段
  • 动机:略
  • 做法:如果字段的访问级别是public,需要先用“封装字段”手段制造一个委托中间层

提炼类

  • 场景:某个类做了两个类的事情
  • 思路:建立新类,搬移函数和字段
  • 动机:一个类应该是清楚的抽象,即可以使用清晰的命名
  • 做法:拆分类,建立两个类之间的单向或双向连接,搬移底层函数,搬移高层函数

内联化类

  • 场景:某个类没做什么事情
  • 思路:将这个类的特性搬移到其他类,然后移除原类
  • 动机:通常会由于此前的重构动作移走了这个类的责任
  • 做法:选择和这个类关系最近的类进行合并,可以先在目标类中使用委托,然后再通过搬移函数的方式完成重构

隐藏委托关系

  • 场景:使用者通过委托类来调用对象
  • 思路:在提供服务的类上直接建立使用者所需的所有函数,隐藏委托关系
  • 动机:隐藏调用关系可以减少实现细节暴露从而减少耦合
  • 做法:在发起请求的类中,直接实现功能的接口,移除使用者的委托代码

移除中间人

  • 场景:类做了过多简单委托的动作
  • 思路:让使用者直接调用受托类
  • 动机:当“隐藏委托关系”使用过多时,封装会很痛苦,这个时候不如直接让使用者通过链式调用用中间受托类实现功能
  • 做法:刚好是“隐藏委托关系”的反向过程

引入外加函数

  • 场景:需要为提供服务的类新增函数,但是你无法修改这个类(通常是库代码)
  • 思路:在使用者类中建立一个函数,并用第一参数的方式传入服务类实例
  • 动机:尽管可以在不修改服务类代码的情况下,自行添加新函数,但还是建议当外加函数较多时,使用“引入本地扩展”的方式,或直接推动服务类升级
  • 做法:在客户类中建立函数,这个函数不调用客户类特性,只是转发请求到服务类

引入本地扩展

  • 场景:需要为服务类添加一些额外函数,但你无法修改这个类
  • 思路:建立一个新类,使其包含这些额外函数,让这个扩展类成为源类的子类或包装类
  • 动机:子类工作量较少,但是必须在对象创建期接管创建过程;包装类只是单纯转发请求,没有这个限制,但是转发过程都需要自己实现
  • 做法:略

重新组织数据

自封装字段

  • 场景:直接访问一个字段的方式给你的重构带来了麻烦,或是引入了麻烦的耦合关系
  • 思路:用取值/设值函数替代直接访问字段
  • 动机:这种方式让字段更为灵活,但是根据奥卡姆剃刀法则,等需要的时候再用
  • 做法:有的字段可能需要一个初始化函数

用对象取代数据值

  • 场景:数据项需要和行为合在一起使用才有价值
  • 思路:把简单的数据项封装成对象
  • 动机:开发初期的简单数据,可能在迭代后会加上特殊行为,如果不及时处理,就会出现特性依恋或重复代码
  • 做法:略

将值对象改为引用对象

  • 场景:从一个类会衍生出多个实例,实例间只是一个实体的多种状态
  • 思路:将值对象改为引用对象
  • 动机:值对象通过equals()hashCode()判断,如日期;引用对象则直接可以用相等操作符==判断,如顾客、账户等概念
  • 做法:你可能需要一个静态字段或提前创建好多个新对象作为访问点

将引用对象改为值对象

  • 场景:你的引用对象很小且不可变,同时不易管理
  • 思路:将引用对象改为值对象
  • 动机:引用对象不好控制,值对象的不可变特性在某些场景很好用。
  • 做法:只有不可变对象才能被重构

以对象取代数组

  • 场景:有个数组,其中的元素类型不一,代表不同的东西
  • 思路:用对象替代数组,用字段表示不同意义的元素
  • 动机:数组的作用是以某种顺序存储一组相似对象,不要让位置具有特殊意义
  • 做法:略

复制被监视数据

  • 场景:有些领域数据被放在了GUI部分代码里
  • 思路:将数据复制到领域对象中,建立Observer模式,剥离UI和逻辑
  • 动机:分层良好的系统,用户界面和业务逻辑代码是分开的,这样也更好维护
  • 做法:略

将单向关联改成双向关联

  • 场景:两个类都需要对方特性,但目前只有单向连接
  • 思路:增加一个反向指针,同时修改函数能够同时更新两条链接
  • 动机:略
  • 做法:注意删除过程移除指针的顺序

将双向关联改为单向关联

  • 场景:两个类有双向关联,但是一个类已经不需要另一个类的特性
  • 思路:去除不必要连接
  • 动机:维护双向连接带来便利的同时,也会增加维护的复杂度
  • 做法:略

用常量取代魔法数

  • 场景:有个字面量数值,具有特殊含义,但是不能一眼看明白
  • 思路:创造一个常量,用命名说明字面数值的意义
  • 动机:魔法数是类型码时,要使用“以类取代类型码”
  • 做法:略

封装字段

  • 场景:类中有public字段
  • 思路:声明改为private,提供相应的访问函数
  • 动机:暴露public会降低函数的模块化程度,数据应该和行为集中在一起,不应被直接修改
  • 做法:略

封装集合

  • 场景:函数返回一个集合
  • 思路:返回集合的只读副本,并在类中提供添加/移除集合元素的函数
  • 动机:类似“封装字段”,返回的集合一样可能被修改
  • 做法:使用Collection,或返回一个副本

用数据类取代记录

  • 场景:面对传统编程中的记录结构
  • 思路:创建“哑”数据对象
  • 动机:要将记录型结构转成面向对象的程序中
  • 做法:创建private字段,创建读写函数并提供

以类取代类型码

  • 场景:类中有个数值类型码,但是不影响类行为
  • 思路:用新的类替换数值类型码
  • 动机:略
  • 做法:略

以子类取代类型码

  • 场景:类中有个不可变数值类型码,同时影响类行为
  • 思路:用宿主的子类替换类型码
  • 动机:可以用子类的多态性取代switch语句,不过,如果类型码会发生改变,或者宿主类已经有子类则不能用此方法
  • 做法:略

以状态/策略取代类型码

  • 场景:类中有个数值类型码,会影响类行为,同时不能通过继承来消除
  • 思路:以状态对象取代替换数值类型码
  • 动机:略
  • 做法:创建一个新的类,用类型码的用途为它命名,这就是一个状态对象。所有的新类继承自超类,返回不同的状态码

以字段取代字段

  • 场景:子类的查边只在返回常量数据的函数上
  • 思路:修改函数,让它们返回超类的新增字段,然后销毁子类
  • 动机:这样可以避免继承带来的额外复杂性
  • 做法:略

简化条件表达式

条件逻辑会增加理解的层级,处理不好时,很容易配合长代码造成理解困难。

分解条件表达式

  • 场景:有一个复杂的条件语句
  • 思路:为if、then、else语句段落提炼独立函数
  • 动机:条件逻辑通常会使代码更难阅读
  • 做法:使用表意的函数名说明条件语句意思

合并条件表达式

  • 场景:有一系列的条件逻辑,都得到相同结果
  • 思路:合并成一个条件表达式,并将之提炼成一个独立函数
  • 动机:有时候这么做能把“做什么”的语句转换成“为什么”的含义,前提是这些检查并非彼此独立
  • 做法:注意确认条件语句都没有副作用,有些条件表达式甚至可以简化成三元表达式

合并重复的条件片段

  • 场景:条件表达式的每个分支都有相同的一段代码
  • 思路:将代码提取到条件表达式之外
  • 动机:减少重复语句
  • 做法:略

移除控制标记

  • 场景:在一系列布尔表达式中,某变量具有控制标记的作用
  • 思路:用breakreturn替代
  • 动机:有时候为了可读性和可维护性,可以牺牲单一出口的做法
  • 做法:略

用“卫语句”替代嵌套条件表达式

  • 场景:嵌套的条件逻辑过多,难以看清正常执行路径
  • 思路:用“卫语句”枚举出所有特殊情况,减少嵌套层数
  • 动机:当特殊case多于正常case时,提前处理每种特殊情况,可以有效减少嵌套层数
  • 做法:注意“卫语句”要么就从函数返回,要么就抛出异常,反正要跳出当前执行流

用多态取代条件表达式

  • 场景:你手上有个条件表达式,根据对象类型不同选择不同行为
  • 思路:将条件表达式的每个分支放在子类的重载函数中,然后将父类的原始函数声明为抽象函数
  • 动机:面向对象程序中,更少出现switch语句也是得益于多态这个工具
  • 做法:略

引入Null对象

  • 场景:在很多地方检查对象是否为null
  • 思路:用一个特殊的Null对象取代null
  • 动机:空对象对外就像是特殊的空的对象(Go笑而不语),而不是什么都没有,有利于保证函数行为的一致性
  • 做法:空对象一定是单例的

引入断言

  • 场景:某段代码需要对程序状态做出假设
  • 思路:用断言表示这种假设
  • 动机:有些时候,只有某个条件为真,代码才能正常运行,这个时候用断言明确这些假设。
  • 做法:注意不要滥用断言,只用来检查“一定为真”的条件,而不要去检查“应该为真”的条件

优化函数调用

我们在前面提到了函数体本身的优化,这一章我们主要介绍函数调用的优化

函数改名

  • 场景:函数名没能说明函数用途
  • 思路:修改函数名
  • 动机:优化函数名,让它达到注释的效果,重新安排参数顺序,提高代码清晰度
  • 做法:对于旧函数,可以标注deprecated,说明其不建议使用

添加参数

  • 场景:函数需要从调用端得到更多信息
  • 思路:为函数添加新的对象参数
  • 动机:如果有其他重构的方法,只要可能,基本都比添加参数要好
  • 做法:略

移除参数

  • 场景:函数本体不需要某个参数
  • 思路:去除该参数
  • 动机:暂时不要考虑未来是否能用到
  • 做法:略

分离查询和修改

  • 场景:一个函数即返回对象状态,同时又有副作用
  • 思路:将查询和修改分离出两个参数
  • 动机:任何有返回值的函数,最好都不要有看得见的副作用
  • 做法:先分离查询,再分离修改

让函数携带参数

  • 场景:若干函数做了类似的操作,仅仅因为某些值表现不同
  • 思路:用一个单一函数表示,用参数来表示那些不同的值
  • 动机:减少重复代码
  • 做法:略

用明确函数取代参数

  • 场景:有一个函数,其中完全取决于参数表现出不同行为
  • 思路:针对参数的不同值,建立一个独立函数
  • 动机:函数内大多以条件表达式检查这些参数值,并作出不同行为;有时也可以用多态实现
  • 做法:略

保持对象完整

  • 场景:你从对象中取了若干字段,将它们作为函数调用的一些参数
  • 思路:改为传递整个对象
  • 动机:如果传递整个对象会让你的依赖结构恶化,那么就不该用这个方法
  • 做法:略

用函数取代参数

  • 场景:对象调用某个函数,用其结果做参数传递给另一个函数,然而接受改参数的函数本身也能调用到前一个函数
  • 思路:让参数接受函数直接去调用前一个函数,然后去除该参数
  • 动机:如果函数有其他途径获得参数值,就不该通过参数获得
  • 做法:略

引入参数对象

  • 场景:某些函数入参总是在一起出现
  • 思路:直接用一个对象取代这些参数
  • 动机:略
  • 做法:略

移除设值函数

  • 场景:类的某个字段在创建时设值,然后就不再改变
  • 思路:去掉字段的设值函数
  • 动机:提供设值字段就表示可能被改变
  • 做法:略

隐藏函数

  • 场景:有函数从未被其他类用到
  • 思路:将函数改为private
  • 动机:减少无谓的API暴露
  • 做法:可以利用lint工具帮忙检查

用工厂函数替代构造函数

  • 场景:希望创建对象时不仅做简单的构建动作
  • 思路:使用工厂函数
  • 动机:这个方法也可以用来通过类型码创建类对象
  • 做法:结合Class.forName()可以不用写switch语句

封装向下转型

  • 场景:函数返回的对象需要由调用者向下转型
  • 思路:将向下转型放在函数中进行
  • 动机:略
  • 做法:略

用异常取代错误码

  • 场景:函数返回特性的代码表示错误情况
  • 思路:改用异常
  • 动机:异常能够区分出正常情况和异常处理
  • 做法:需要决定抛出受控异常或者非受控异常

用测试取代异常

  • 场景:对于一个调用者可以预先检查的条件,抛出了异常
  • 思路:修改调用者,改在调用前进行检查
  • 动机:能够提前检查的情况,就不算是异常
  • 做法:略

处理继承关系

字段上移

  • 场景:两个子类有相同字段
  • 思路:将字段移至超类
  • 动机:归纳重复特性
  • 做法:略

函数上移

  • 场景:两个子类有相同作用的函数
  • 思路:将函数移至超类
  • 动机:归纳重复特性。子类的函数覆写超类函数,但是做相同工作时,也要使用函数上移
  • 做法:略

构造函数上移

  • 场景:子类的构造函数几乎完全一致
  • 思路:在超类中新建构造函数,再在子类构造函数中调用它
  • 动机:如果重构过程过于复杂,可以考虑使用“用工厂函数替代构造函数”
  • 做法:略

函数下移

  • 场景:超类的某函数只和部分子类有关
  • 思路:将函数移到相关的子类中去
  • 动机:和“函数上移”恰恰相反
  • 做法:略

字段下移

  • 场景:超类的字段只被部分子类用到
  • 思路:将字段移到真正需要的子类中去
  • 动机:和“字段上移”恰恰相反
  • 做法:略

提炼子类

  • 场景:类的特性只被部分实例对象用到
  • 思路:新建一个子类,将未被用到的特性转移到子类中
  • 动机:上述的差异行为有时也可能通过类型码区分,这个时候可以由“以子类取代类型码”或“以状态/策略取代类型码”方法来重构
  • 做法:略

提炼超类

  • 场景:两个类有相似特性
  • 思路:为两个类建立超类,将相似特性移到超类中
  • 动机:两个类用相同方式做类似事情往往意味着重复代码
  • 做法:略

提炼接口

  • 场景:若干客户端使用类中的同一子集,或者两个类有部分相同点
  • 思路:将相同的子集提炼到独立接口中
  • 动机:接口有助于系统的责任划分能力声明(鸭子类型)。在单继承的语言中,接口扮演了组合功能代码的角色。尤其某个类在不同环境表现不同时,使用接口是个好主意
  • 做法:接口命名通常由-able结尾

折叠继承关系

  • 场景:超类和子类几乎无法区分
  • 思路:将它们合为一体
  • 动机:往往在过度设计时出现
  • 做法:略

构造模板函数

  • 场景:有一些子类,细节上有所区别,但是整个流程上操作类似
  • 思路:提炼出操作流程,上移至超类,将具体细节操作放在独立函数中,让它们有相同的签名,然后实现超类的抽象函数
  • 动机:这样抽离出来的流程函数也叫模板函数,模板上插槽接口固定,然而提供插槽的模板函数是一致的
  • 做法:后续新增的类,只需实现超类抽象函数就可以完成扩展

用委托取代继承

  • 场景:子类只使用超类接口的一部分,或者直接不需要继承来的数据
  • 思路:在子类中新建字段保存超类,然后调整子类函数,让它委托超类,然后去掉两者的继承关系
  • 动机:略
  • 做法:略

用继承取代委托

  • 场景:两个类的委托关系过多,且委托函数都很简单
  • 思路:让委托类继承受托类
  • 动机:如果你没有使用所有受托类函数,那么就不要用这个重构方法,继续保持委托关系,使用其他重构方法;另外受托对象可变时,也要注意
  • 做法:略

大型重构

Kent Beck和作者所写

本章介绍了4个大型重构的思路,也是大型程序容易遇到的4个问题

  • 梳理和分析继承体系:往往因为某个继承体系承担的两个甚至更多责任,有一个特征是,某一层级的所有类,子类都以相同形容词开始。可以通过委托的形式,对继承体系做正交化
  • 过程化设计转化为对象设计:往往出现在过程化风格传统语言中。可以将数据记录变为对象,拆分大块行为为小块,然后将行为转移到相关对象中。
  • 分离领域和UI:出现在有GUI的场景中。传统的MVC设计模式就是将领域逻辑分离出来,用接口的方式和UI部分代码对接
  • 提炼继承体系:有的类做了太多工作,里面经常有较多的条件表达式。对于这种,可以借助面向对象中的子类和多态或者策略模式实现

重构与现实

重构在某些角度和技术演进很像。技术的接纳过程类似一条钟形曲线。前段包括先行者和早期接受者,中部大量人群包括早期消费者和晚期消费者,最后则是行动迟缓者。不同人有不同的消费动机。先行者和早期接受者感兴趣的是新技术,“范式转移和突破性思想”的愿景;早期和晚期消费者则关心成熟度、成本、支持程度,以及这种新思想/新产品是否被和他们相似的其他人成功使用

尾声

  • 重构工具能节省你的重构时间
  • 永远记住“两顶帽子”,重构时保持代码功能不变

关于编程相关的书籍已有太多太多,本书相比其他编码相关的技术书籍来说,从技术外的视角来介绍还是挺有意思的。编码无非是求生的一种方式,对于程序员来讲,把生活过好也绝不仅是把代码写好就OK的。书中所写基本是作者对其过去职业和人生经历的一个总结,以tips的形式给出,是“术”而非“道”。是的,本书的介绍思路大概是告诉你一些方法,对你的生活和工作有些帮助的方法,而非构建一个体系,一种思考方式。因此,对于那些三观和做事方法思路基本稳定的人来说,它没法撼动你根本的认识,只能做到具体某个方面的启示和改进。同时,读起来也是相对简单的。不客气地说,这本书应该是最近一年中读到的信息密度最低的书了。不过,其中理财和健身两章,尤其是第55章应该算是全书的精华,对我还是挺有帮助的。

下面分章节,对其中的关键的idea进行摘录。

职业

可能由于作者大多数时间是自由职业者,这一部分介绍没有太多新意

  • 工作是公司的,职业生涯是你自己的
  • 把自己当成公司去思考,你有什么可以卖,相比其他“产品”你的优势是什么
  • 作为程序员,你能提供的基础服务就是创建软件
  • 人际交往不能忽略
  • 通过面试最快捷的方式是让面试官对你产生好感,如果能提前接触就更好了(Really?)
  • 承担更多责任是脱颖而出的一种方式,同时保证自己被注意到
  • 成为自由职业者之后,一定不要忘了自我营销

自我营销

作为自由职业者,作者在自我营销上有些自己的见解

  • 自我营销:打造自我品牌 + 多种媒介 + 持之以恒
  • 一个品牌需要包含:传达的信息(slogan) + 视觉符号(logo) + 一致性 + 曝光率,重点在建立一套预期
  • 不要忘了有效利用社交媒体,为自己积累粉丝
  • 学会演讲
  • 著书有时候不是为了赚钱,而是赢得名声

学习

  • 学习知识最好能将知识用于实践
  • 十步学习法:了解全局、确定范围、定义目标、寻找资源、创建计划、筛选资源、开始学习、动手操作、全面掌握、乐为人师
    • 前6步能让你明确方向和目标,为正式学习做准备
    • 后面4步循环往复,快速迭代,乐为人师能强行提高你的理解程度
  • 有时候导师也很重要
  • 遇到知识短板时,就是你成长的机会

注意力

  • 专注像是一种惯性,是逐渐达到的,不可能一次性达成
  • 番茄工作法:将时间拆分成30分钟的番茄钟(25分钟专心工作 +5分钟休息),通过番茄钟衡量工作量和自己的工作效率(实际工作中太容易被打乱节奏了,比较适合自由职业者)
  • 定额工作法:对于需要定期完成的任务,自我规定周期内需要达到的工作量,承诺然后完成。有时候可以借助大众的监督来坚持
  • 多任务并行通常会因为上下文切换影响工作效率,除非另一项任务不需要花费脑力
  • 电视是时间杀手
  • 为了享受快乐有意识地去做,就不是浪费时间;为了逃避自己应该完成的任务去做,就是浪费时间
  • 分解任务可以有效地减小解决问题的困难程度
  • 任何行动都比不行动要好(主要是行动可以获得反馈)

理财

本篇是全书相对最有信息量的一篇。其中第55章对于自己生涯的描述,甚至比其他所有篇章的介绍都要精华。其他章节可能有粉饰自己的成分,但是第55章“额外馈赠”足够真诚。

  • 资产是使用价值高于维护成本的东西,负债则相反。减少负债,增加资产
  • 自我营销越好,薪酬越好谈
  • 期权是指在未来某个时期前购买一定数量股票的选择权,购买的是期望。买方最大亏损有限,最大盈利无限,有权利没有义务;卖方相反。看涨期权和看跌期权作为买方都需要交期望差额的权利金。到期日时(这是欧式期权的做法,美式期权可以在到期日前任意时间交易),买方可以选择或放弃行权,选择行权时,买方卖出股票,赚取高于权利金的差额;放弃行权时同时放弃权利金。
  • 房地产投资是低风险高负债的投资类型,一方面可以寻求房产出售的机会,另一方面可以通过租金获取稳定收入。当然在租赁房产时,建议选择负责任的物业托管。通过部分租金换取安心。
  • 想要提前退休,需要有资本积累,同时,这个资本还要让你获得足够生活的被动收入。如果有提前退休打算,需要下面几点准备
    • 树立目标,即XX岁前退休
    • 意识到通胀的存在,他会吞噬你赶不上它速度的资产
    • 做有固定收益和被动收入的投资(最好还能赶上通胀)
    • 要想更早退休,自然要比常人花更多精力“开源”和“节流”
  • 真正获得财务成功的唯一方法就是用钱生钱
  • 一边欠债一边存钱是最愚蠢的做法,因为债务利息永远高于存款利息。
    • 存钱之前先把房屋抵押贷款还清(这么做还是有些绝对,还要留些以备急用)
    • 能一次付清就一次付清,除非你能通过提前享受获得超过利息的收益
    • 确保先偿还利息最高的债务
    • 并非所有债务都是不好的,除非你能通过提前享受获得超过利息的收益(如住房贷款和学生贷款)
  • 退休即自由,即可以不以钱作为出发点行事,更形象地说,“从社会中赎回自己的生活”。
  • 买房办理贷款时注意贷款利率是否是固定的
  • 作者最初也是程序员的工作,之后逐渐贷款购置房产用于租赁,通过日常工作还款。之后几次创业都不太成功。但是房产累积越来越多。之后通过个人营销开始创办博客和培训教程,认识大佬后,培训教程逐渐受到欢迎。之后订下退休目标,随着房产带来的被动收入和在线培训(编程 + 健身 + 创业)被动收入逐渐稳定,作者成功上岸退休

健身

  • 同时达到多个健身目标是很难实现的。很难在增肌同时减掉脂肪,同样地,很难在减脂同时增长肌肉
  • 减肥很简单:摄入的卡路里小于燃烧的卡路里。
    • 摄入卡路里通过食物计算,但是烹饪过程对热量也有很大影响
    • 燃烧的卡路里可以根据基础代谢率BMR结合训练消耗计算
  • 增肌需要给肌肉压力,挑选动作的时候注意选择复合动作,如深蹲、硬拉、卧推、杠铃推举等等。注意保持足够的蛋白质摄入
  • 腹肌不是靠增肌得到,而要通过减脂。体脂率下降到特定水平自然能看到腹肌。可以通过高强度间歇式训练(HITT)来减脂
  • 跑步和站立式办公都是简单的燃脂方式

心灵

  • 积极思考不只是外表乐观,而且对健康有益,延年益寿
  • 做事方法的第一步是相信自己有改变的能力,改变不了现状,你至少能改变自己的心态。
  • 爱情不是追逐游戏,你追我逃。更健康的模式是行为上体现出自信,用自然随和且充满自信的态度和别人交往。“我自己感觉很好,我不需要你,但是我觉得你挺有意思的,所以我想更好地了解你”
    • 关键是你要真得能表现出足够的自信,你要对自己足够尊重
    • 做一个绝望的、缺乏自信的人,你会发现自己会真的孤立无援
  • 那些拒绝最终都会把你带到一个想和你在一起的人那里

结束语 & 附录

  • 生活原本比你所厌恶的朝九晚五的工作丰富多彩得多
  • 空头是指,你事先“借”该股票的一些股份并卖出,这会产生空头头寸,最终你需要靠回购这只股票来填补你借入的空头头寸
  • Ⅰ型糖尿病是指自身不能产生胰岛素,Ⅱ型糖尿病是指自身对胰岛素不够敏感
  • 吃垃圾食品不会对健康造成重大影响,但是摄入食品的总量却会影响健康。相比健康食品,垃圾食品带来更高热量的同时,只有较低的饱腹感。因此,达到标准热量所使用的垃圾食品可能会让你长期处于挨饿状态。
  • 水果和蔬菜都是健康食品,热量都不高;高蛋白食物的热量值通常也不怎么高。纤维带来饱腹感的同时有较低的热量
    • 总结来说,未经加工的食品就是最健康的

–END–

前一阵被邀请参与公司新任培训的师兄师姐分享环节,大体是和小组内的新入职校招同学分享工作中的积累和收获。分享内容没有要求,但是大家提的比较多的问题是入职后有没有什么成长的tips或者坑。我干脆提前做了些相关的准备,也一并在这里记录下来吧。

我回忆了下过去2年多以来,在公司遇到的之前未曾预期的种种,大致可以总结为下面两点:

  • 了解业务
  • 以人为本

先说了解业务吧。我们在日常工作中遇到的需求无非两类:业务需求技术需求。这两类工作都要求对业务的了解。在业务需求中,首先了解业务才有可能完好地还原prd的需求,避免产生不符合预期的情况,避免被产品或QA打回。然后,我们每个人都不是需求实现的机器,了解业务能让你知道,你写的每一行代码都是有意义的,都是真真正正为人服务的,而不是机械地完成任务。同时,对业务的熟悉程度也能让你的视野上个台阶,从更高的角度考虑问题,看到更远的可能。实际上,服务端同学相对来说,由于要设计数据库表,实现底层的业务逻辑,需要对整个业务理解更深入,所以在大多数团队,leader也都是服务端同学担当。

在技术需求中,同样离不开对业务的理解。可能有些刚入职的同学不会意识到:我们其实是工程师,而不是科学家。我们需要把技术应用到实际工作中,而不是单纯地指出某个技术的可行性。失去业务土壤的技术需求是无法带来真正价值的。业务需要需要能够从业务中挖掘,并在最后真正应用到业务中产生价值。举个反例,在我刚入职时实现过一个流程图工具,想法很单纯,用技术实现工具是个很酷的事。然而没有考虑过如何应用到业务中,最后半途而废。类似地,在入职1年多的时候,和同事开发了能够托管UI稿和prd稿的平台,但是我们无视了市面上已经有太多成熟好用的产品,最终也没能推动业务使用。一个比较好的技术需求应该怎么做呢:了解业务需要、使用技术赋能,然后保证实现落地。这三步中,使用技术赋能反而是最简单的一步。理解业务需要和推动业务使用是比想象中要困难的。

第二点叫做以人为本。这也是刚加入工作时可能注意不到的一点,入职前你可能以为你每天很浪漫地和代码泡在一起,和机器打交道。真正工作一段时间后,你会发现,每天至少50%的时间是在和人打交道的。人和机器是不一样的,机器是可靠的,可预期的,可以根据在学校里学习的知识推测的;而人是不可靠的,需要技巧,需要将心比心。这种不一样的思维方式会给可能过于理性的你带来麻烦。不然你会发现每天70%~80%的烦恼都是人而非机器带来的。

说几个例子,展开聊一下。先说代码,代码是写给人看,写给人理解的,然后才是交给机器去执行。看似你是在写代码,实际上你是通过代码在和未来的你或者接手你代码的人交流思路。所以代码的风格、可读性可能比你想象中要重要。一个糟糕的代码风格、可读性会让未来的你或者其他同事想要骂人,想要通过git blame找到这一坨shi一样的代码究竟是哪一个如此没品味的人写出来的。相反,一个好的风格、可读性会让未来的你和你的同事接手代码时心情愉悦,如清风拂面。类似地,可扩展性和设计良好与否也能起到上面的效果。可扩展性强和设计良好的代码可以极高地提高修改代码的愉悦程度和生产效率。

作为前端,界面的设计也一样重要。不要只是单纯地去实现PM或者UI的设计,可以站在终端用户的角度换位思考。如果是你遇到这种交互,它是否符合你的直觉,使用起来是否够简单,是否能达到目的。遇到有疑问的地方,随时可以找PM和UI讨论。

当然,这里说以人为本也不要矫枉过正。各位在日常工作中,还是以做事为主,公司内也还是看大家工作成果如何,而不是人际关系搞的如何。这里只是提个醒,希望大家在踏实努力的同时,也不要忘记了“人”这个角色的重要性。

总结上来看,就是上面这些,没别的了。各位既然来参加今天的培训,肯定也是从学校的环境刚进入工作环境没多久。上面这段分享听完之后,如果能对有些人有启示,能让大家意识到思维方式的不同,就达到我的目的了。

谢谢!

正式版HTTP/2发布于2015年5月,距今已经快5年了,相比老一辈HTTP 1.1有了许多改进。在具体讨论这些改进前,我们先简单回顾下HTTP这一路以来的历程。

历史回顾

HTTP在1991年发布了最初的HTTP0.9版本,主要用于学术交流,目的也只是用来在网络之间传递HTML超文本的内容。HTTP0.9基于TCP,只有一个GET请求类型,请求和文档响应都是ASCII字符流,响应数据类型只有HTML类型,在响应结束后立即断开连接。

随着互联网发展,1994年底出现了拨号上网,网景推出浏览器后,WWW已经不仅局限于学术交流,浏览器中除了承载HTML以外,还包括了JavaScript、CSS、图片、音视频等资源。HTTP1.0在这个背景下于1996年推出(RFC1945),它支持了状态码、方法、头部的概念,响应内容不局限于超文本文件,编码类型也不仅限于ASCII。但是TCP连接在响应返回后依旧会断开连接。

在浏览器等技术发展,HTTP请求更多也更复杂,HTTP1.0的已有问题暴露得越来越明显。TCP频繁建立连接的时延,缓存协商机制的不完整、大文件下载的支持等问题需要解决。于是HTTP1.1在1999年推出(RFC2616),这是个很庞大的协议,全文长达176页,在后续IETF对该规范进行更新时,则被拆分成了总页数更多的六个文档(即RFC7230协议族)。HTTP1.1包含了太多细节和可选的部分,包含不常用的功能和不合适的优化,因此几乎没有任何实现包含完整的协议功能。总的来看,HTTP1.1做了一些尝试:

  • TCP持久连接(keep-alive),即在一个TCP连接上发起HTTP请求
  • 支持范围请求(Accept-Ranges)
  • 更强大的缓存机制(协商缓存和强缓存,以及相关的头部)
  • 提出了HTTP pipeline,尝试改善串行HTTP请求引起的线头阻塞(Head-of-line blocking)问题
  • 更多的错误相关状态码
  • Host头处理

之后HTTP1.1便被一直使用至今,随着互联网页面请求资源的数量和体积增大,HTTP1.1中没能妥善解决的问题越来越明显。HTTPbis工作组在2007年夏天便着手于HTTP1.1标准的更新,并最终在2014年初形成上面提到的RFC7230系列协议族。

SPDY(SPeeDY)是由Google牵头开发的开源协议,意图在TLS和HTTP中间插入中间层,解决HTTP协议的问题。大约在2012年提出,也被大多数主流浏览器支持。最终在2015年HTTP/2协议发布后,逐步放弃支持。实际上HTTP/2也是在SPDY/3草案的基础上形成的协议初稿。

历史问题

传输资源与延迟

当今的互联网环境和20世纪末那会儿已经有了很大的不同,现在的Web页面更像一个应用的概念,而非一个简单的页面(SPA)。从HTTPArchive一个页面下请求的资源数已经上百,请求的资源体积也接近2M。

http-transfer-size

随着这些年网络硬件条件的迅猛发展,带宽已经不再是影响人们体验的因素,而网络延迟仍然没有太好的改善。高延迟的网络上(比如移动设备),即使拥有高连接速率,也很难获得优质快速的网络体验。页面从访问到打开的近70%时间都发生在网络上。

请求-响应模型

HTTP1.1是无状态协议,需要以客户端请求开始,然后才能响应。一个TCP上同时只能有一个请求/响应。TCP协议的能力并没有被充分利用。在HTTP1.1启用keep-alive后,TCP连接重复建立的问题被解决。但是请求还是需要排队一个一个发送,TCP的RTT(round-trip time)还是比较可观。后续的HTTP1.1提出了HTTP管线化(pipeline),即将多个HTTP请求合并成一个,一起发送,这样的确提高了服务器的资源利用率,但是也会带来线头阻塞(head-of-line blocking)问题,即一个比较耗时的请求会影响后续的所有请求。另外,它会给重试带来麻烦,需要网络中间节点的支持。所以这个特性并没有得到浏览器和服务器认可,实现也并不普及。目前大多数桌面浏览器也是默认关闭这个特性的。

http-pipeline

那些年,我们一起克服延迟的办法

针对上面的困难,智慧的开发者们自然也是有了许多应对办法。

  • 雪碧图:将小图片整合成一张大图。
  • 内联:将高优先级资源或小资源通过script标签或style标签或dataUrl的形式直接内嵌在页面里
  • 分片(sharding)与域名散列:将图片或者其他资源分发到不同主机。最初的HTTP1.1规范(RFC2616)提到一个客户端最多只能对同一主机建立两个TCP连接。后来,两个连接的限制被取消了(RFC7230),现在的浏览器一般允许每个域名主机建立6-8个连接。根据httparchive.org的记录显示,在Top30万个URL中平均使用40个TCP连接来显示页面

http-sharding

除此之外,为了减少请求数,前端会将代码合并并打包,这也是webpack这样的工具诞生的背景。

冗长的头部

HTTP1.1中1000+字节的头部都是常见的且体积较大的,如Cookie。头部信息有许多多余信息。这也让许多大请求建立连接的过程变得很慢。

总结来看,HTTP1.1遗留了下面几个问题:

  • 对TCP利用较差,同时只能有一个请求/响应
    • 目前应对方法:开多个TCP连接(分片),减少请求数(合并资源);这些方法多少会遇到TCP慢启动、DNS开销等问题
  • HTTP头部没有压缩,占用较大空间
    • 目前应对方法:减少请求数、使用cookie-less域名
  • 固有的请求-响应模式,重要资源无法优先推送
    • 目前应对方法:内联资源

HTTP/2对于上面这些问题自然是重拳出击。

HTTP/2概述

“HTTP/2 enables a more efficient use of network resources and a reduced perception of latency by introducing header field compression and allowing multiple concurrent exchanges on the same connection. It also introduces unsolicited push of representations from servers to clients.”

根据RFC7540的摘要,简明扼要地点出了HTTP/2带来的几个重要特性:

  • 多路复用的二进制协议;一个TCP连接上不再只有1个请求/响应,同时采用二进制而非文本传输数据
  • 头部压缩;用二进制分帧配合专门设计的头部压缩算法(HPACK)大大减少头部体积,HPACK有专门的RFC7541来规范。
  • 服务器推送;在客户端发送请求前,主动将资源推送给客户端

整个HTTP/2实际上还是在HTTP的框架下的,对HTTP1.1也是完全兼容的,这意味着你可以像以前一样使用HTTP的API、方法、头部、状态码这些:

  • HTTP/2必须维持HTTP的范式。它只是一个让客户端发送请求到服务器的基于TCP的协议
  • 不能改变http://https://这样的URL,也不能对其添加新的结构。使用这类URL的网站太多了,没法指望他们全部改变。
  • HTTP1.1的服务器和客户端依然会存在很久,所以必须提供HTTP1.1到HTTP/2服务器的代理
  • 不再使用小版本号。服务器和客户端都必须确定自己是否完整兼容http2或者彻底不兼容

协商

SPDY依赖于TLS,不过从SPDY中诞生的HTTP/2却可以选择是否基于TLS。由此带来2种HTTP/2协商机制。对于普通的HTTP1.1,通过给服务器发送一个带升级头部的报文。如果服务器支持HTTP/2,它将以“101 Switching”作为回复的状态码,并从此开始在该连接上使用HTTP/2。这种连接方式也被称为h2c(HTTP/2 cleartext),此时HTTP/2在TCP之上运行。出于安全性考虑。几乎所有的主流浏览器都不支持这种协商实现(curl可以支持)。

对于在TLS之上的https,Next Protocol Negotiation (NPN)是一个用来在TLS服务器上协商SPDY的协议。IETF将这个非正式标准进行规范化,从而演变成了ALPN(Application Layer Protocol Negotiation)。ALPN会伴随HTTP/2中的使用而推广,考虑到SPDY会使用NPN,而许多服务器又会同时提供SPDY以及HTTP/2,所以在这些服务器上同时支持ALPN以及NPN显然会成为最理所当然的选择。ALPN和NPN的主要区别在于,ALPN中由服务端最终决定通信协议,NPN中由客户端最终决定。

HTTP/2特性

在HTTP/2的介绍中提到,协议通过定义一个优化的基础连接的HTTP语义映射来解决HTTP1.1的问题。具体地,它允许在同一连接交错地建立请求和响应消息,并使用高效率编码的HTTP报头字段。它还允许请求的优先级,让更多的重要的请求更快速的完成,进一步提升了性能。最终协议设计为对网络更友好,因为它相对HTTP/1.x减少了TCP连接。最后,这种封装也通过使用二进制消息帧使信息处理更具扩展性。

里面加粗的部分即HTTP/2带来的几个新特性:

  • 单一TCP连接
  • 二进制分帧
    • 请求优先级
    • 服务端推送
    • 流量控制
  • 多路复用
  • 头部压缩(HPACK)

二进制分帧“层”

首先,HTTP/2是个二进制协议。它的请求和响应都是流的形式,它基本的协议单位是帧。每个帧都有不同的类型和用途。HTTP/2所有性能增强的核心也在于这个新的二进制分帧层,它定义了如何封装 HTTP 消息并在客户端与服务器之间传输。

http2-framing-layer

从图中能看到,在TLS之上,HTTP/2之下新增了一个二进制分帧层。这里所谓的“层”,指的是位于套接字接口与应用可见的高级HTTP API之间一个经过优化的新编码机制:HTTP的语义(包括各种动词、方法、标头)都不受影响,不同的是传输期间对它们的编码方式变了。不同于HTTP1.x里面用换行符作为分隔,HTTP/2中将信息分割成帧,并进行二进制编码。整个分帧过程由客户端和服务端替我们完成。

数据流和帧

上面这种二进制分帧机制改变了客户端与服务器之间交换数据的方式,也带来了流的概念。

  • 流(Stream):一个双向字节帧流穿过HTTP/2连接中的虚拟通道,可以承载一条或多条消息。
  • 消息:与逻辑请求或响应消息对应的完整的一系列帧。

流的生存周期包含idle、reserved(local)、reserved(remote)、open、half closed(local)、half closed(remote)、closed多个阶段。状态间通过特定的帧类型流转。在不同状态下对应着不同的能力,对于状态规范描述以外的操作请求都会给出协议错误(PROTOCOL_ERROR)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
                         +--------+
send PP | | recv PP
,--------| idle |--------.
/ | | \
v +--------+ v
+----------+ | +----------+
| | | send H / | |
,------| reserved | | recv H | reserved |------.
| | (local) | | | (remote) | |
| +----------+ v +----------+ |
| | +--------+ | |
| | recv ES | | send ES | |
| send H | ,-------| open |-------. | recv H |
| | / | | \ | |
| v v +--------+ v v |
| +----------+ | +----------+ |
| | half | | | half | |
| | closed | | send R / | closed | |
| | (remote) | | recv R | (local) | |
| +----------+ | +----------+ |
| | | | |
| | send ES / | recv ES / | |
| | send R / v send R / | |
| | recv R +--------+ recv R | |
| send R / `----------->| |<-----------' send R / |
| recv R | closed | recv R |
`----------------------->| |<----------------------'
+--------+

send: endpoint sends this frame
recv: endpoint receives this frame

H: HEADERS frame (with implied CONTINUATIONs)
PP: PUSH_PROMISE frame (with implied CONTINUATIONs)
ES: END_STREAM flag
R: RST_STREAM frame

流和帧的关系是:

  • 所有通信都在一个TCP连接上完成,这个连接可以承载任意数量的流
  • 每个流上面都有唯一标识符和可选的优先级信息,里面会承载要传递的消息
  • 每条消息都是一条逻辑HTTP信息(如请求或相应),有完整的HTTP语义,其中可能有一个或多个帧
  • 帧是最小的通信单位,承载着特定类型的数据,例如HTTP 标头、消息负载等等。帧可以交错发送,然后再根据帧头的数据流标识符进行组装

http2-frame

所有的帧以8字节的报头开始并且跟着0到16383字节长度的主体。帧格式如下:

1
2
3
4
5
6
7
8
9
 0                   1                   2                   3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| R | Length (14) | Type (8) | Flags (8) |
+-+-+-----------+---------------+-------------------------------+
|R| Stream Identifier (31) |
+=+=============================================================+
| Frame Payload (0...) ...
+---------------------------------------------------------------+

其中:

  • R:保留字段
  • Length:14位无符号整数的帧主体长度
  • Type:帧类型,它描述了剩余的帧报头和帧主体将如何被解释
  • Flags:为帧类型保留的8位布尔类型字段,根据不同帧类型赋予不同语义
  • Stream Identifier:31字节的流标识符(见StreamIdentifiers)。0是保留的,标明帧是与连接相关作为一个整体而不是一个单独的流。

请求与响应复用

在HTTP1.1中,客户端要想发起多个并行请求以提升性能,则必须使用多个TCP连接,这种对TCP效率低下的利用,在HTTP/2中得到改善。二进制分帧层将HTTP消息分解为互不依赖的帧,然后交错发送,最后再在另一端把它们重新组装起来。

http2-reusing

在上图中,流1和流3交错在同一个TCP连接上并行运行。这种机制带来了下面一些具体的提升:

  • 请求之间交错且互不影响
  • 响应之间交错且互不影响
  • 可以使用一个连接并行发送多个请求和响应
  • 不必再为了优化HTTP1.1性能做雪碧图、分片等骚操作
  • 一定程度上解决了线头阻塞问题

流控制

HTTP/2的流控制类似TCP,但是更为精细和更靠近应用层。借助HTTP/2流控制,可以实现在用户暂停一个大型视频流后,减少或阻塞视频里带来的HTTP流量,又或者中间代理匹配上下游流量速率。HTTP/2流控制提出了一些规则,但并没有指出特定算法,目标在于允许不需要协议改动的情况下改进流量控制算法。

  • 流量控制是逐跳的,而不是头尾端点的
  • 流量控制是基于窗口更新帧的。接收端广播自己准备在流及整个连接过程中接收的字节大小。这是一个信用为基础的方案。
  • 流量控制是有方向性的,由接收端全权掌握。
  • 流量控制窗口初始值是65,535字节,不过接收方可以设置一个更大的值
  • 帧类型决定了是否适用流量控制规则。目前只有DATA帧受流量控制
  • 不能被禁用
  • 通过使用WINDOW_UPDATE帧类型来实现

流优先级

HTTP/2标准允许每个数据流都有一个关联的权重和依赖关系:

  • 可以向每个数据流分配一个介于1至256之间的整数。
  • 每个数据流与其他数据流之间可以存在显式依赖关系。

优先级的目的是让客户端可以构建和传递“优先级树”,表明它倾向于如何接收响应。反过来,服务器可以使用此信息通过控制CPU、内存和其他资源的分配设定数据流处理的优先级。

http2-priority

数据流依赖关系通过将另一个数据流的唯一标识符作为父项引用进行声明;如果忽略标识符,相应数据流将依赖于“根数据流”。分配时,会尽可能先向父数据流分配资源,然后再向其依赖项分配资源。共享相同父项的数据流按其权重比例分配资源。在上图中,数据流B获得的资源是数据流A所获资源的三分之一。

新建流的终端可以在报头帧中包含优先级信息来对流标记优先级。对于已存在的流,优先级帧可以用来改变流优先级。

更详细规范参考RFC

帧类型

DATA

数据帧,类型0x0,传递和流关联的任意变量值长度的字节数据。例如,一个或多个数据帧被用来携带HTTP请求或者响应的载体。数据帧定义了以下标记:

  • END_STREAM (0x1) :用来表示当前帧是确定的流发送的最后一帧。设置这个标记时流进入到一种半封闭状态或者关闭状态。
  • END_SEGMENT (0x2) :表示是当前端的最后一帧。代理端绝对不能跨越多个端的边界来合并帧,转发帧的时候代理端必须保持片段的边界。
  • PADDED (0x8) : 位4用来表示Pad Length字段是可见的。

数据帧绝对需要与流相关联,且遵从流量控制。

HEADERS

报头帧,类型0x1,用来打开一个流,并携带头部片段。能在流打开或者半封闭(远程)的状态下发送。

1
2
3
4
5
6
7
8
9
10
11
+---------------+
|Pad Length? (8)|
+-+-------------+-----------------------------------------------+
|E| Stream Dependency? (31) |
+-+-------------+-----------------------------------------------+
| Weight? (8) |
+-+-------------+-----------------------------------------------+
| Header Block Fragment (*) ...
+---------------------------------------------------------------+
| Padding (*) ...
+---------------------------------------------------------------+

它有以下标记:

  • END_STREAM (0x1) :用来标识这是发送端对确定的流发送的最后报头区块。设置这个标记将使流进入一种半封闭状态。后面伴随带有END_STREAM标记的延续帧的报头帧表示流的终止。延续帧不用来终止流。
  • END_SEGMENT (0x2) :表示这是当前端的最后一帧。中介端绝对不能跨片段来合并帧,且在转发帧的时候必须保持片段的边界。
  • END_HEADERS (0x4) :表示帧包含了整个的报头块,且后面没有延续帧。不带有END_HEADERS标记的报头帧在同个流上后面必须跟着延续帧。
  • PADDED (0x8) :表示Pad Length字段会呈现。
  • PRIORITY (0x8) :设置指示专用标记(E),流依赖及权重字段将会呈现

PRIORITY

优先级帧,类型0x2。明确了发送者建议的流的优先级,它可以在任意时间的流中发送。优先级帧不包含任何标记(flag)。

1
2
3
4
5
6
7
 0                   1                   2                   3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|E| Stream Dependency (31) |
+-+-------------+-----------------------------------------------+
| Weight (8) |
+-+-------------+

RST_STREAM

类型0x3,允许流的立即终止。通常用来取消一个流,或表示有错误发生。绝不应该在idle状态下发出。

1
2
3
4
5
0                   1                   2                   3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Error Code (32) |
+---------------------------------------------------------------+

SETTINGS

设置帧,类型0x4。包含影响如何与终端通信的设置参数,并且用来确认这些参数的接收。设置帧必须由两个终端在连接开始的时候发送,并且可以由各个终端在连接生存期的任意时间发送。

PUSH_PROMISE

推送承诺帧,类型0x5。用来在流发送者准备发送流之前告知对等端。包含了终端准备创建的长流的31位无符号标记以及提供附加上下文的报头的集合。通常在服务器中使用。推送承诺的接收端可以选择给推送承诺的发送端返回一个与被承诺的流标识符相关的RST_STREAM标记来拒绝接收承诺流。

1
2
3
4
5
6
7
8
9
10
11
0                   1                   2                   3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| [Pad High(8)] | [Pad Low (8)] |X| Promised Stream ID (31) ...
+---------------+---------------+-+-----------------------------+
... Promised Stream ID | Header Block Fragment (*) ...
+-------------------------------+-------------------------------+
| Header Block Fragment (*) ...
+---------------------------------------------------------------+
| Padding (*) ...
+---------------------------------------------------------------+

PING

类型0x6。从发送端测量最小的RTT时间的机制,同样也是一种检测连接是否可用的方法。PING帧可以被任何终端发送,且必须在载体中包含一个8字节长度的任意数据。

GOAWAY

超时帧,类型0x7。通知远端对等端不要在这个连接上建立新流。超时帧可以由客户端或者服务端发送。发送后,可以针对新的流创建一个新的连接。这个帧的目的是允许终端优雅的停止接收新的流,但仍可以继续完成之前已经建立的流的处理。

1
2
3
4
5
6
7
8
9
0                   1                   2                   3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|X| Last-Stream-ID (31) |
+-+-------------------------------------------------------------+
| Error Code (32) |
+---------------------------------------------------------------+
| Additional Debug Data (*) |
+---------------------------------------------------------------+

32位的错误码中包含了关闭连接的原因。

WINDOW_UPDATE

窗口更新帧,类型0x8。用来实现流控制。

1
2
3
4
5
0                   1                   2                   3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|X| Window Size Increment (31) |
+-+-------------------------------------------------------------+

CONTINUATION

延续帧,类型0x9,用来延续一个报头区块。在END_HEADERS标记前,可以在HEADERS帧、PUSH_PROMISE帧以及CONTINUATION帧后接续任意数量的CONTINUATION帧。它包含一个flag:

  • END_HEADERS (0x4) : 设置指示这个帧的报头区块的终止

延续帧必须与流相关联。如果延续帧的相关流表示字段是0x0,终端必须响应一个类型为协议错误的连接错误。

服务器推送

http2-server-push

HTTP/2中的服务器推送打破了原来HTTP中的请求-响应语义(对原有语义也做了改进),支持服务器可以对一个客户端请求发送多个响应。在原先的HTTP1.1中我们可能会将重要资源内联到网页中,减少网络延迟,这实际上等同于HTTP/2中的强制推送。在HTTP/2中的服务器推送还有下面一些功能:

  • 推送的资源能被客户端缓存(服务器也只能推送可被缓存的资源)
  • 在不同页面之间可以重用
  • 可以由服务器设定优先级
  • 可以被客户端拒绝

服务器推送数据流由PUSH_PROMISE帧发起,需要先于请求推送资源的响应数据传输。实现上的策略是先于父响应(即,DATA 帧)发送所有PUSH_PROMISE帧,其中包含所承诺资源的HTTP头部。客户端接收到PUSH_PROMISE帧后,它可以根据自身情况选择接受拒绝(通过RST_STREAM帧)数据流。(例如,如果资源已经位于缓存中)

客户端完全掌控服务器推送的使用方式。客户端可以限制并行推送的数据流数量;调整初始的流控制窗口以控制在数据流首次打开时推送的数据量;或完全停用服务器推送。这些在HTTP/2连接开始时通过SETTINGS帧传输,可能随时更新。

头部压缩

在HTTP1.1中,头部数据使用以纯文本的形式传输,所占空间较大,在使用HTTP Cookie后,更是会达到上千字节。为了减少此开销和提升性能,HTTP/2使用专门设计的HPACK压缩格式压缩请求和响应头部,这种格式通过静态霍夫曼编码对传输的头部字段进行编码。HPACK要求客户端和服务器同时维护和更新一个包含之前见过的标头字段的索引列表,利用之前传输值的索引列表,我们可以通过传输索引值的方式对重复值进行编码,索引值可用于有效查询和重构完整的头部键值对。

hpack

作为一种进一步优化方式,HPACK压缩上下文包含一个静态表和一个动态表:静态表在规范中定义,并提供了一个所有连接都可能使用的常用HTTP头部字段列表;动态表最初为空,将根据在特定连接内交换的值进行更新。

早期版本的HTTP/2和SPDY使用的DEFLATE对头部进行压缩,但是在2012年夏天出现了CRIME这种安全攻击。因此,之后HTTP/2的头部压缩采用了专门设计的HPACK方案。在使用HPACK后,初次访问后的压缩率能达到70%~80%,甚至90%+。

支持度与调试

支持度上,主流浏览器服务器程序都已支持。你也可以访问这个网址体验HTTP/2和HTTP1.1在load大量图片时的延迟对比。

如果你想确认当前网页中的哪些请求是使用的HTTP/2,可以在chrome devTools下的network选项卡里查看“Protocol”列(未发现此列的可以在表头右键找到并勾选显示),其中HTTP/2将显示为h2。正如之前所说,支持HTTP/2的浏览器会和服务器使用特定协议协商,对于不支持HTTP/2的情况,会自动会退到HTTP1.1版本。

尽管HTTP/2使用二进制传输数据,然而浏览器为我们掩盖了实现细节。如果想要深入查看甚至是调试二进制分帧层的功能,如何去debug呢?

  • Wireshark
  • curl
  • h2i,不过这个好像已经不维护了

TLS调优

目前各大浏览器只在https://的基础上支持HTTP/2,即在TLS层之上的HTTP/2。多出的TLS的层也是会增加时延和成本的,具体涉及到的TLS握手、会话和加密套件协商过程还有优化空间,如减少证书层级、减少证书大小等。其余优化方向可以查看参考中一些文章介绍。

之前的优化还应该继续用么

继续保持的

  • 减少DNS查询
    • 减少域名
    • 启用预读:dns-prefetch
  • 使用CDN
  • 避免重定向
  • 资源压缩
    • 代码压缩(JS、HTML、CSS)
    • 资源压缩(图片、字体、音频、视频)
    • 文本压缩(Gzip)
  • 使用缓存

不再需要的

  • 分片与域名散列:HTTP/2对于一个域名只使用一个TCP连接,分片反而会浪费资源,同时也会影响流控制、头部压缩的表现。
  • 资源打包合并:HTTP/2支持多路复用,资源合并会降低缓存利用率,且会让开发流程更复杂。(snowpack了解一下)
  • 资源内联:可以由服务器推送解决这类需求,资源内联一方面无法缓存,另一方面会让页面代码更大

常见问题

  • Q: 既然HTTP/2是在SPDY工作基础上设计的,那HTTP/2推出后,SPDY还使用吗
  • A: Google公开声明了他们会在2016年移除Chrome里对SPDY和NPN的支持,并且极力推动服务器迁移至HTTP/2。2016年2月他们声明了SPDY和NPN会在Chrome 51之后被移除。
  • Q: 这个协议是否只对大型网站有效
  • A: 由于缺乏内容分发网络,小网站的网络延迟往往较高,而多路复用的能力可以极大的改善在高网络延迟下的体验。
  • Q: 基于TLS让速度变得更慢
  • A: 正如上一节提到的,TLS的握手确实增加了额外的开销,也有越来越多的方案提出来减少TLS往返的时间。TLS同时也会更多消耗CPU等资源,更多例子可见istlsfastyet.com。不过一方面HTTP/2并不强制要求基于TLS,另一方面HTTP/2带来的性能提升使得即使基于TLS,通常也会比HTTP1.1更快
  • Q: 为什么不使用文本传输
  • A: 的确,如果可以直接读出协议内容,那么调试和追踪都会变得更为简单。但是二进制带来的灵活度更高,何况浏览器会自动帮你解析。
  • Q: 看起来,HTTP1.1中的一些短板并没有改彻底
  • A: 实际上,设计HTTP/2之初的目标就包括向前兼容HTTP/1.1的范式,来保证升级协议也不用重写底层的很多东西。老的HTTP功能,如协议头、状态码、可怕的Cookie,这些都保留了。性能优化更多通过增加了一个中间分帧层解决的。
  • Q: 目前使用的广泛程度如何
  • A: 在2015年年底大多数浏览器就已经支持HTTP/2,目前约96%的浏览器支持HTTP/2,同时约46%的网站支持HTTP/2。

后续

  • 官网在给出HTTP/2规范时,也曾预计要10年时间和HTTP1.1并存,在这个期间,Web优化的思路也可能会有调整
  • TLS1.3。2020年2月的RFC8740中给出了基于TLS1.3的HTTP/2的实现建议。
  • 既然TCP容易遇到线头阻塞问题,那么能不用使用UDP呢?Google提出的QUIC(Quick UDP Internet Connection)它在很大程度上继承了SPDY的衣钵。QUIC可以理解成TCP + TLS + HTTP/2替代实现。
  • 2018年10月,互联网工程任务组HTTP及QUIC工作小组正式将基于QUIC协议的HTTP(英语:HTTP over QUIC)重命名为HTTP/3以为确立下一代规范做准备。

参考

正文:《经济学原理》 - 宏观经济学原理笔记

宏观经济学的数据

一国收入的衡量

  • 微观经济学:研究家庭和企业如何做出决策,以及它们如何在市场上相互影响
  • 宏观经济学:研究整体经济现象,包括通货膨胀、失业和经济增长
  • 国内生产总值(gross domestic product,GDP):在某一既定时期一个国家内生产的所有最终物品和服务的市场价值
  • 消费:家庭处购买新住房外用于物品和服务的支出
  • 投资:用于资本设备、存活、建筑物的支出
  • 净出口:外国人对国内生产物品的支出减国内居民对外国物品的支出
  • 名义GDP:按现期价格评价的物品和服务的生产
  • 真实GDP:按不变价格评价的物品和服务的生产
  • GDP平减指数:用名义GDP与真实GDP的比率乘以100得到的物价水平衡量指标

生活费用的衡量

  • 消费物价指数(CPI):普通消费者所购买的物品和服务的总费用的衡量指标
  • 通货膨胀率:从前一个时期以来,物价指数变动的百分比
  • 生产物价指数:企业所购买的一揽子物品与服务的费用的衡量指标
  • 指数化:根据法律或合同按照通货膨胀的影响对货币数量的自动调整
  • 名义利率:通常公布的、为根据通货膨胀影响校正的利率
  • 真实利率:根据通货膨胀影响校正过的利率

长期中的真实经济

生产与增长

  • 生产率:每单位劳动投入所生产的物品和服务的数量
  • 收益递减:随着投入量的增加,每一单位额外投入得到的收益减少的特性
  • 追赶效应:开始贫穷的国家倾向于比开始富裕的国家增长更快的特征

储蓄、投资和金融体系

  • 金融体系:经济中促使一个人的储蓄和另一个人的投资相匹配的一组机构
  • 金融市场:储蓄者可以借以直接向借款者提供资金的金融机构
  • 债券:一种债务证明书
  • 股票:企业部分所有权的索取权
  • 金融中介机构:储蓄者借以间接向借款者提供资金的金融机构
  • 共同基金:向公共出售股份,并用收入来购买股票和债券资产组合的机构
  • 国民储蓄(储蓄),用于消费和政府购买后剩下的一个经济中的总收入
  • 私人储蓄:家庭支付税收和消费后剩下的收入
  • 公共储蓄:政府在支付支出后剩下的税收收入
  • 预算盈余:税收收入大于政府支出的余额
  • 预算赤字:政府支出引起的税收收入短缺
  • 可贷资金市场:想储蓄的人借以提供资金、想借钱投资的人借以借贷资金的市场
  • 挤出:政府借款引起的投资减少

金融学基本工具

  • 金融学:研究人们如何在某时期内做出关于配置资源和应对风险的决策的学科
  • 现值:用现行利率产生一定量未来货币所需要的现在货币量
  • 终值:现行利率既定时,现在货币量将带来的未来货币量
  • 复利:货币量的累积,赚得的利润停留在账户中赚取更多利息
  • 风险厌恶:不喜欢不确定性
  • 多元化:通过用大量不相关的小风险代替一种风险来降低风险
  • 企业特有风险:只影响单独公司的风险
  • 市场风险:影响股市上所有公司的风险
  • 基本面分析:为决定一家公司的价值而对其会计报表和未来前景进行的研究
  • 有效市场假说:认为资产价格反映了关于一种资产价值的所有公开、可获得的信息的理论
  • 信息有效:以理性方式反映所有可获得信息的关于资产价格的描述

失业

  • 劳动力:就业者和失业者的工人总数
  • 失业率:劳动力中失业者所占比例
  • 劳动力参工率:劳动力占成年人口的百分比
  • 自然失业率:失业率曲线围绕波动的正常失业率
  • 周期性失业:失业率对自然失业率的周期性背离
  • 丧失信心的工人:想工作但已放弃寻找工作的人
  • 摩擦性失业:由于工人寻找符合自己嗜好和技能的工作需要时间带来的失业
  • 结构性失业:由于劳动市场提供的工作岗位不足或工资高于均衡工资引起的失业
  • 失业保险:工人失业时,位置提供部分收入保障的政府计划
  • 工会:与雇主就工资、津贴、工作条件进行谈判的工人协会
  • 集体谈判:工会和企业就就业条件达成一致的过程
  • 效率工资:企业为了提供工人生产率而支付的高于均衡水平的工资

长期中的货币与物价

货币制度

  • 货币:经济中人们经常用于向其他人购买物品和服务的一组资产
  • 交互媒介:卖者在购买物品和服务时给予卖者的东西
  • 计价单位:人们用来表示价格和记录债务的标准
  • 价值储藏手段:人们用来把现在的购买力转变为未来购买力的东西
  • 流动性:一种资产兑换为经济中交换媒介的容易程度
  • 商品货币:以由内在价值的商品为形式的货币
  • 法定货币:没有内在价值、由政府法令确定通货使用的货币
  • 通货:公众手中持有的纸币钞票和铸币
  • 中央银行:为了监管银行体系和调节经济中货币量设计的机构
  • 准备金:银行得到但是没有贷出去的存款
  • 部分准备金银行:只把部分存款作为准备金的银行
  • 准备金率:作为准备金持有的存款比例
  • 货币乘数:银行体系有1单位准备金产生的货币量
  • 银行资本:银行的所有者投入机构的资源
  • 杠杆:将借到的货币追加到投资的现有资金上
  • 杠杆率:资产和银行资本的比率
  • 资本需要量:政府管制确定的最低银行资本量
  • 公开市场操作:美联储买卖美国政府债券
  • 贴现率:美联储向银行发放贷款的利率
  • 法定准备金:银行必须根据其存款持有的最低准备金量的规定
  • 联邦基准利率:银行向另一家银行进行隔夜贷款时的利率

货币增长和通货膨胀

  • 货币数量论:一种认为可得到的货币量决定物价水平,可得到货币量增长率决定通货膨胀率的理论
  • 名义变量:按货币单位衡量的变量
  • 真实变量:按实物单位衡量的变量
  • 古典二分法:名义变量和真实变量的理论区分
  • 货币流通速度:货币易手的速度
  • 通货膨胀税:政府通过创造货币而筹集的收入
  • 费雪效应:名义利率对通货膨胀率所进行的一对一的调整
  • 皮鞋成本:当通货膨胀鼓励人们减少货币持有量时所浪费的资源
  • 菜单成本:改变价格的成本

面向开放经济的宏观经济学

基本概念

  • 封闭经济:不与世界上其他经济交易的经济
  • 开放经济:与其他经济交易的经济
  • 出口:在国内生产国外销售的物品和服务
  • 进口:在国外生产国内销售的物品和服务
  • 净出口:一国出口值减进口值,又称贸易余额
  • 贸易赤字:进口大于出口
  • 资本净流出:本国居民购买的外国资产减去外国人购买的本国资产
  • 名义汇率:用一国通货交换另一国通货的比率
  • 升值:能购买到外国通货量衡量的一国通货的价值增加
  • 贬值:能购买到外国通货量衡量的一国通货的价值降低
  • 购买力平价:一种认为任何一单位通货应该能在所有国家买到等量物品的汇率理论

宏观经济理论

  • 贸易政策:直接影响一国进口或出口的物品与服务数量的政府政策
  • 资本外逃:一国资产需求大量且突然地减少

短期经济波动

解释短期经济波动

  • 衰退:真实收入下降和失业增加的时期
  • 萧条:严重的衰退
  • 自然产出水平:一个经济在长期中当失业处于正常时达到的物品与服务的生成水平
  • 滞涨:产量减少而物价上升的时期

货币政策和财政政策对总需求的影响

  • 流动性偏好理论:凯恩斯认为,利率的调整使货币供给与需求平衡
  • 财政政策:政府决策者对政府支出和税收水平的确定
  • 乘数效应:扩张性财政政策增加收入的同时增加消费支出引起总需求的额外变动
  • 挤出效应:扩张性财政政策引起利率上升,从而减少投资支出引起的总需求减少
  • 自动稳定器:经济进入衰退时,决策者不必采取任何行动就可以刺激总需求的财政政策变动(如税制、政府支出)

通货膨胀和失业间的短期权衡

  • 菲利普斯曲线:表示通货膨胀和失业间短期关系的曲线
  • 自然率假说:认为无论通货膨胀如何,失业都要回到正常率或自然率的观点
  • 牺牲率:通货膨胀降低一个百分比过程中每年产量损失的百分比数
  • 理性预期:人们能充分利用他们可以获取到的全部信息,如政策信息等,用来预测未来
0%