重构 —— 代码的实用性与艺术性
MF的《重构》一书算是程序设计书籍的经典了。其中对于重构的认识和剖析深入浅出,提纲挈领。对于有一定编程经验的人来说更是如虎添翼的帮助。下面我尽量在不贬损原意的基础上,用自己的思路和语言进行适当的总结。
序 & 前言:重构的再认识
开篇名义,还未进入正文,书从序和前言开始,便不自觉间流露着真知灼见:
- 重构是不改变软件可观察行为的前提下改善其内部结构。
- 重构需要你维护一份“坏味道”和重构手段的对应
- 设计前期使用模式通常会导致过度工程
- 代码总将随着设计者的经验成长而进化
样例:感受重构
任何一个傻瓜都能写出计算机理解的代码。但唯有优秀的程序员才能写出人类能理解的代码
代码被阅读和修改的次数远多于被编写的次数。尽管代码在机器中运行时,机器并不会嫌弃代码丑陋。但是代码总是要修改的,当我们打算修改系统时,就涉及到了人。人在乎这些。差劲的系统很难维护,如果很难找到修改点,程序员就可能犯错,从而引入bug。如果你发现你需要为程序增加特性,但是当前的代码结构让你不能方便达成目标时,先重构那个程序,再方便地添加特性。
当然,重构前一定要确认,自己有没有一套可靠的测试机制,因为你需要它来保证重构的基础要素:不修改已有功能。重构中,最好能以微小的步伐前进(这样能及时回滚)。在本章样例的重构中,体现了下面一些“好味道”:
- 代码块越小,代码功能就越好管理
- 好的代码应该能够清楚表达自己的功能,变量名也是代码清晰的关键
- 用多态取代条件判断逻辑
- 结构化风格相比过程化风格更易扩展也更好维护
原则
本章介绍了重构的一些原则和基础性认识。
- 何为重构:不改变软件可观察特性的前提下,通过修改内部结构,提高其可理解性。通常情况下和性能优化相对应
- 两顶帽子:添加新功能和重构应该属于两种截然不同的行为,它们应该分开交替进行
- 重构的好处
- 改进软件设计,整理代码让后续的修改更容易
- 让软件更好理解,准确说出我想要的
- 帮忙找到bug
- 提高未来的编程速度
- 何时重构
- 事不过三,第一次只管去做,第二次产生反感但还是去做,第三次做类似的事情就去重构
- 修改bug时重构
- review代码时重构
- 间接层和重构:中间层能够允许逻辑共享和意图的分开解释,同时隔离变化和解耦。
- 提前设计好中间层不如先直接做再重构
- 少数情况下,中间层只会带来冗余
- 重构的难题
- 修改已有API:建议维护新旧两个接口,让用户做出反应后,再迁移。这期间,旧接口应该要调用新接口实现
- 代码已经无法正常运行时,重写比重构更省事
- 重构和性能优化:大多数的性能优化集中在小部分代码上。先写出风格良好的代码,再使用性能工具实测数据,对瓶颈处单独优化性能。好的重构也会让性能优化更容易进行
坏味道
在遇到下面一些“味道”时,可能你就需要重构了。
- 重复代码
- 函数过长,每当你需要用注释说明点什么时,可以把需要说明的东西写到一个独立函数中
- 太长的类
- 函数入参过多
- 发散式变化:一个类因为多个原因发生不同的变化
- 霰弹式变化:一个原因引起一个类的多个变化
- 特性依恋:函数对某个类的兴趣高于自己所在的类
- 数据泥团:喜欢聚合在一起的零散数据字段
- 基础类型偏执:对于基础类型如字符串、整型不愿意使用简单类来封装
- swtich语句
- 冗余类
- 夸夸其谈未来性:过度为未来设计
- 令人迷惑的暂时字段
- 过度耦合的链式调用,如
a.b.c().d()
,链上任意类做修改都会影响整个调用 - 两个类的狎昵关系
- 异曲同工的类
- 幼稚的数据类:只有最简单的getter和setter
- 子类拒绝继承超类的函数或数据
- 过多的注释
测试体系:重构的保证
前面已经提到数次,重构的前提是不对已经已有行为做改动,这需要测试的帮助。本章对建立测试给了一些简单的介绍。
- 编写测试代码最有用时机是编程之前
- 编写一个测试case时,可以先让测试失败,再通过成功验证程序功能
- 遇到bug时,先添加一个单元测试复现这个bug
- 测试不能保证程序没有bug,编写测试样例也遵循82原则,当样例已经很多时,它带来的边际效果就没那么好了。应该更多考虑容易出错的边界条件,积极思考如何“破坏代码”。
重构列表
下面分几大方向介绍具体的重构手段。每个手段会分场景、思路、动机、做法来展开。
组织函数
日常工作中,非常容易坏味道中的过长函数,下面的一些重构方式可以帮我们优化这一点。
提炼函数
- 场景:有一段相对独立的代码可以被组织并独立出来
- 思路:将这段代码放到一个独立函数中,用函数名解释该函数的用途
- 动机:有时会遇到过长函数中有一段需要注释才能看明白的代码。将这样相对独立的逻辑拆分成表意的短小函数后,可以让高层函数读起来就像一系列注释。如果提炼可以提高代码清晰度,就算函数名比函数体长都无所谓
- 做法:用做什么而不是怎么做来为函数命名(如果你想不出一个更有意义的名称,就别动了)。检查是否有临时变量,如果有读取,可以作为入参传递给函数;如果对临时变量甚至有再赋值,那可能还要让函数返回临时变量修改后的值
内联函数
- 场景:函数本体和名称一样清晰易懂
- 思路:在函数调用点插入函数本体,然后移除函数
- 动机:如果函数本体足够简单,且表意清晰,同时调用点有限,不具备多态性。那么出于减少无用中间层的考虑,可以直接使用函数体
- 做法:注意检查是否有多态性
内联临时变量
- 场景:一个临时变量只被简单表达式赋值一次,同时妨碍了其他重构手法
- 思路:将对变量的引用动作,替换成给它赋值的表达式本身
- 动机:过多的临时变量会妨碍你重构长函数
- 做法:注意确保表达式没有副作用
以查询替代临时变量
- 场景:程序中有个临时变量保存了某个表达式的运算结果,同时被多处引用
- 思路:将表达式提炼成独立函数,在独立变量的所有引用点替换成对新函数的调用
- 动机:替换成函数后,整个类都可以获得这份信息,同时会减少对该变量的频繁引用带来的重构困难
- 做法:寻找只被赋值一次的临时变量,对于赋值多次的临时变量使用“分解临时变量”方法先重构,保证提炼出来的函数没有副作用。先不要担心性能问题,等到出现了优化也会比较简单
引入解释性变量
- 场景:有个复杂的表达式,表意不够清晰
- 思路:将表达式的值放进一个临时变量,用变量名表意
- 动机:表达式不如变量名更好阅读。如果临时变量在整个类都有意义,建议直接使用“提炼函数”方法
- 做法:先判断是否使用“提炼函数”的做法
分解临时变量
- 场景:某个临时变量被多次赋值,且每次赋值意义不一样
- 思路:针对每次不同意义的赋值使用不一样的临时变量
- 动机:临时变量的多义性会增大理解成本
- 做法:寻找被多次赋值且有多义性的变量,不同的意义使用新的不同临时变量
移除对函数入参的赋值
- 场景:对函数入参赋值
- 思路:用新的临时变量取代入参
- 动机:对入参赋值会混淆按值传递和按引用传递的传参方式
- 做法:略
用函数对象取代函数
- 场景:大型函数中代码过于复杂,无法使用“提炼函数”
- 思路:直接将函数放在单独对象中,将复杂的局部变量变成对象字段,从而可以轻松地在对象中分解这个大型函数到多个小型函数
- 动机:略
- 做法
- 建立一个新类,用函数用途给这类命名
- 在新类中创建final字段保存大型函数所在的对象,即“源对象”
- 新类的构造函数使用原函数入参作为入参
- 新类中建立
computed()
函数 - 赋值原代码到
computed()
中 - 在原函数位置,创建这个新类的一个对象,并调用这个对象的
computed()
函数 - 继续重构新类中的
computed()
函数
替换算法
- 场景:某个算法有更清晰的算法替代
- 思路:直接更换函数本体
- 动机:略
- 做法:略
对象间的特性搬移
类应该承担清晰且明确的责任。不论是承担责任过多还是“不怎么负责任”,都需要进行重构。
搬移函数
- 场景:有函数和所在类以外的其他类反而有更多交流,如调用或被调用
- 思路:在和函数交流更多的类中建立一个有类似行为的新函数,改造旧函数为新函数的委托函数,或者直接移除旧函数
- 动机:略
- 做法:
- 检查和搬移函数关联的字段或函数,判断是否要一起搬移
- 检查子类和超类有无其他声明,检查有无多态性
- 如果目标函数需要太多源类特性,就需要进一步分解后再搬移
搬移字段
- 场景:某个字段和所在类以外的其他类有更多交流
- 思路:在目标类新建字段,修改源字段的所有使用者,令它们使用新字段
- 动机:略
- 做法:如果字段的访问级别是
public
,需要先用“封装字段”手段制造一个委托中间层
提炼类
- 场景:某个类做了两个类的事情
- 思路:建立新类,搬移函数和字段
- 动机:一个类应该是清楚的抽象,即可以使用清晰的命名
- 做法:拆分类,建立两个类之间的单向或双向连接,搬移底层函数,搬移高层函数
内联化类
- 场景:某个类没做什么事情
- 思路:将这个类的特性搬移到其他类,然后移除原类
- 动机:通常会由于此前的重构动作移走了这个类的责任
- 做法:选择和这个类关系最近的类进行合并,可以先在目标类中使用委托,然后再通过搬移函数的方式完成重构
隐藏委托关系
- 场景:使用者通过委托类来调用对象
- 思路:在提供服务的类上直接建立使用者所需的所有函数,隐藏委托关系
- 动机:隐藏调用关系可以减少实现细节暴露从而减少耦合
- 做法:在发起请求的类中,直接实现功能的接口,移除使用者的委托代码
移除中间人
- 场景:类做了过多简单委托的动作
- 思路:让使用者直接调用受托类
- 动机:当“隐藏委托关系”使用过多时,封装会很痛苦,这个时候不如直接让使用者通过链式调用用中间受托类实现功能
- 做法:刚好是“隐藏委托关系”的反向过程
引入外加函数
- 场景:需要为提供服务的类新增函数,但是你无法修改这个类(通常是库代码)
- 思路:在使用者类中建立一个函数,并用第一参数的方式传入服务类实例
- 动机:尽管可以在不修改服务类代码的情况下,自行添加新函数,但还是建议当外加函数较多时,使用“引入本地扩展”的方式,或直接推动服务类升级
- 做法:在客户类中建立函数,这个函数不调用客户类特性,只是转发请求到服务类
引入本地扩展
- 场景:需要为服务类添加一些额外函数,但你无法修改这个类
- 思路:建立一个新类,使其包含这些额外函数,让这个扩展类成为源类的子类或包装类
- 动机:子类工作量较少,但是必须在对象创建期接管创建过程;包装类只是单纯转发请求,没有这个限制,但是转发过程都需要自己实现
- 做法:略
重新组织数据
自封装字段
- 场景:直接访问一个字段的方式给你的重构带来了麻烦,或是引入了麻烦的耦合关系
- 思路:用取值/设值函数替代直接访问字段
- 动机:这种方式让字段更为灵活,但是根据奥卡姆剃刀法则,等需要的时候再用
- 做法:有的字段可能需要一个初始化函数
用对象取代数据值
- 场景:数据项需要和行为合在一起使用才有价值
- 思路:把简单的数据项封装成对象
- 动机:开发初期的简单数据,可能在迭代后会加上特殊行为,如果不及时处理,就会出现特性依恋或重复代码
- 做法:略
将值对象改为引用对象
- 场景:从一个类会衍生出多个实例,实例间只是一个实体的多种状态
- 思路:将值对象改为引用对象
- 动机:值对象通过
equals()
或hashCode()
判断,如日期;引用对象则直接可以用相等操作符==
判断,如顾客、账户等概念 - 做法:你可能需要一个静态字段或提前创建好多个新对象作为访问点
将引用对象改为值对象
- 场景:你的引用对象很小且不可变,同时不易管理
- 思路:将引用对象改为值对象
- 动机:引用对象不好控制,值对象的不可变特性在某些场景很好用。
- 做法:只有不可变对象才能被重构
以对象取代数组
- 场景:有个数组,其中的元素类型不一,代表不同的东西
- 思路:用对象替代数组,用字段表示不同意义的元素
- 动机:数组的作用是以某种顺序存储一组相似对象,不要让位置具有特殊意义
- 做法:略
复制被监视数据
- 场景:有些领域数据被放在了GUI部分代码里
- 思路:将数据复制到领域对象中,建立Observer模式,剥离UI和逻辑
- 动机:分层良好的系统,用户界面和业务逻辑代码是分开的,这样也更好维护
- 做法:略
将单向关联改成双向关联
- 场景:两个类都需要对方特性,但目前只有单向连接
- 思路:增加一个反向指针,同时修改函数能够同时更新两条链接
- 动机:略
- 做法:注意删除过程移除指针的顺序
将双向关联改为单向关联
- 场景:两个类有双向关联,但是一个类已经不需要另一个类的特性
- 思路:去除不必要连接
- 动机:维护双向连接带来便利的同时,也会增加维护的复杂度
- 做法:略
用常量取代魔法数
- 场景:有个字面量数值,具有特殊含义,但是不能一眼看明白
- 思路:创造一个常量,用命名说明字面数值的意义
- 动机:魔法数是类型码时,要使用“以类取代类型码”
- 做法:略
封装字段
- 场景:类中有public字段
- 思路:声明改为private,提供相应的访问函数
- 动机:暴露public会降低函数的模块化程度,数据应该和行为集中在一起,不应被直接修改
- 做法:略
封装集合
- 场景:函数返回一个集合
- 思路:返回集合的只读副本,并在类中提供添加/移除集合元素的函数
- 动机:类似“封装字段”,返回的集合一样可能被修改
- 做法:使用Collection,或返回一个副本
用数据类取代记录
- 场景:面对传统编程中的记录结构
- 思路:创建“哑”数据对象
- 动机:要将记录型结构转成面向对象的程序中
- 做法:创建private字段,创建读写函数并提供
以类取代类型码
- 场景:类中有个数值类型码,但是不影响类行为
- 思路:用新的类替换数值类型码
- 动机:略
- 做法:略
以子类取代类型码
- 场景:类中有个不可变数值类型码,同时影响类行为
- 思路:用宿主的子类替换类型码
- 动机:可以用子类的多态性取代switch语句,不过,如果类型码会发生改变,或者宿主类已经有子类则不能用此方法
- 做法:略
以状态/策略取代类型码
- 场景:类中有个数值类型码,会影响类行为,同时不能通过继承来消除
- 思路:以状态对象取代替换数值类型码
- 动机:略
- 做法:创建一个新的类,用类型码的用途为它命名,这就是一个状态对象。所有的新类继承自超类,返回不同的状态码
以字段取代字段
- 场景:子类的查边只在返回常量数据的函数上
- 思路:修改函数,让它们返回超类的新增字段,然后销毁子类
- 动机:这样可以避免继承带来的额外复杂性
- 做法:略
简化条件表达式
条件逻辑会增加理解的层级,处理不好时,很容易配合长代码造成理解困难。
分解条件表达式
- 场景:有一个复杂的条件语句
- 思路:为if、then、else语句段落提炼独立函数
- 动机:条件逻辑通常会使代码更难阅读
- 做法:使用表意的函数名说明条件语句意思
合并条件表达式
- 场景:有一系列的条件逻辑,都得到相同结果
- 思路:合并成一个条件表达式,并将之提炼成一个独立函数
- 动机:有时候这么做能把“做什么”的语句转换成“为什么”的含义,前提是这些检查并非彼此独立
- 做法:注意确认条件语句都没有副作用,有些条件表达式甚至可以简化成三元表达式
合并重复的条件片段
- 场景:条件表达式的每个分支都有相同的一段代码
- 思路:将代码提取到条件表达式之外
- 动机:减少重复语句
- 做法:略
移除控制标记
- 场景:在一系列布尔表达式中,某变量具有控制标记的作用
- 思路:用
break
或return
替代 - 动机:有时候为了可读性和可维护性,可以牺牲单一出口的做法
- 做法:略
用“卫语句”替代嵌套条件表达式
- 场景:嵌套的条件逻辑过多,难以看清正常执行路径
- 思路:用“卫语句”枚举出所有特殊情况,减少嵌套层数
- 动机:当特殊case多于正常case时,提前处理每种特殊情况,可以有效减少嵌套层数
- 做法:注意“卫语句”要么就从函数返回,要么就抛出异常,反正要跳出当前执行流
用多态取代条件表达式
- 场景:你手上有个条件表达式,根据对象类型不同选择不同行为
- 思路:将条件表达式的每个分支放在子类的重载函数中,然后将父类的原始函数声明为抽象函数
- 动机:面向对象程序中,更少出现switch语句也是得益于多态这个工具
- 做法:略
引入Null对象
- 场景:在很多地方检查对象是否为
null
- 思路:用一个特殊的Null对象取代
null
值 - 动机:空对象对外就像是特殊的空的对象(Go笑而不语),而不是什么都没有,有利于保证函数行为的一致性
- 做法:空对象一定是单例的
引入断言
- 场景:某段代码需要对程序状态做出假设
- 思路:用断言表示这种假设
- 动机:有些时候,只有某个条件为真,代码才能正常运行,这个时候用断言明确这些假设。
- 做法:注意不要滥用断言,只用来检查“一定为真”的条件,而不要去检查“应该为真”的条件
优化函数调用
我们在前面提到了函数体本身的优化,这一章我们主要介绍函数调用的优化
函数改名
- 场景:函数名没能说明函数用途
- 思路:修改函数名
- 动机:优化函数名,让它达到注释的效果,重新安排参数顺序,提高代码清晰度
- 做法:对于旧函数,可以标注
deprecated
,说明其不建议使用
添加参数
- 场景:函数需要从调用端得到更多信息
- 思路:为函数添加新的对象参数
- 动机:如果有其他重构的方法,只要可能,基本都比添加参数要好
- 做法:略
移除参数
- 场景:函数本体不需要某个参数
- 思路:去除该参数
- 动机:暂时不要考虑未来是否能用到
- 做法:略
分离查询和修改
- 场景:一个函数即返回对象状态,同时又有副作用
- 思路:将查询和修改分离出两个参数
- 动机:任何有返回值的函数,最好都不要有看得见的副作用
- 做法:先分离查询,再分离修改
让函数携带参数
- 场景:若干函数做了类似的操作,仅仅因为某些值表现不同
- 思路:用一个单一函数表示,用参数来表示那些不同的值
- 动机:减少重复代码
- 做法:略
用明确函数取代参数
- 场景:有一个函数,其中完全取决于参数表现出不同行为
- 思路:针对参数的不同值,建立一个独立函数
- 动机:函数内大多以条件表达式检查这些参数值,并作出不同行为;有时也可以用多态实现
- 做法:略
保持对象完整
- 场景:你从对象中取了若干字段,将它们作为函数调用的一些参数
- 思路:改为传递整个对象
- 动机:如果传递整个对象会让你的依赖结构恶化,那么就不该用这个方法
- 做法:略
用函数取代参数
- 场景:对象调用某个函数,用其结果做参数传递给另一个函数,然而接受改参数的函数本身也能调用到前一个函数
- 思路:让参数接受函数直接去调用前一个函数,然后去除该参数
- 动机:如果函数有其他途径获得参数值,就不该通过参数获得
- 做法:略
引入参数对象
- 场景:某些函数入参总是在一起出现
- 思路:直接用一个对象取代这些参数
- 动机:略
- 做法:略
移除设值函数
- 场景:类的某个字段在创建时设值,然后就不再改变
- 思路:去掉字段的设值函数
- 动机:提供设值字段就表示可能被改变
- 做法:略
隐藏函数
- 场景:有函数从未被其他类用到
- 思路:将函数改为private
- 动机:减少无谓的API暴露
- 做法:可以利用lint工具帮忙检查
用工厂函数替代构造函数
- 场景:希望创建对象时不仅做简单的构建动作
- 思路:使用工厂函数
- 动机:这个方法也可以用来通过类型码创建类对象
- 做法:结合
Class.forName()
可以不用写switch语句
封装向下转型
- 场景:函数返回的对象需要由调用者向下转型
- 思路:将向下转型放在函数中进行
- 动机:略
- 做法:略
用异常取代错误码
- 场景:函数返回特性的代码表示错误情况
- 思路:改用异常
- 动机:异常能够区分出正常情况和异常处理
- 做法:需要决定抛出受控异常或者非受控异常
用测试取代异常
- 场景:对于一个调用者可以预先检查的条件,抛出了异常
- 思路:修改调用者,改在调用前进行检查
- 动机:能够提前检查的情况,就不算是异常
- 做法:略
处理继承关系
字段上移
- 场景:两个子类有相同字段
- 思路:将字段移至超类
- 动机:归纳重复特性
- 做法:略
函数上移
- 场景:两个子类有相同作用的函数
- 思路:将函数移至超类
- 动机:归纳重复特性。子类的函数覆写超类函数,但是做相同工作时,也要使用函数上移
- 做法:略
构造函数上移
- 场景:子类的构造函数几乎完全一致
- 思路:在超类中新建构造函数,再在子类构造函数中调用它
- 动机:如果重构过程过于复杂,可以考虑使用“用工厂函数替代构造函数”
- 做法:略
函数下移
- 场景:超类的某函数只和部分子类有关
- 思路:将函数移到相关的子类中去
- 动机:和“函数上移”恰恰相反
- 做法:略
字段下移
- 场景:超类的字段只被部分子类用到
- 思路:将字段移到真正需要的子类中去
- 动机:和“字段上移”恰恰相反
- 做法:略
提炼子类
- 场景:类的特性只被部分实例对象用到
- 思路:新建一个子类,将未被用到的特性转移到子类中
- 动机:上述的差异行为有时也可能通过类型码区分,这个时候可以由“以子类取代类型码”或“以状态/策略取代类型码”方法来重构
- 做法:略
提炼超类
- 场景:两个类有相似特性
- 思路:为两个类建立超类,将相似特性移到超类中
- 动机:两个类用相同方式做类似事情往往意味着重复代码
- 做法:略
提炼接口
- 场景:若干客户端使用类中的同一子集,或者两个类有部分相同点
- 思路:将相同的子集提炼到独立接口中
- 动机:接口有助于系统的责任划分和能力声明(鸭子类型)。在单继承的语言中,接口扮演了组合功能代码的角色。尤其某个类在不同环境表现不同时,使用接口是个好主意
- 做法:接口命名通常由
-able
结尾
折叠继承关系
- 场景:超类和子类几乎无法区分
- 思路:将它们合为一体
- 动机:往往在过度设计时出现
- 做法:略
构造模板函数
- 场景:有一些子类,细节上有所区别,但是整个流程上操作类似
- 思路:提炼出操作流程,上移至超类,将具体细节操作放在独立函数中,让它们有相同的签名,然后实现超类的抽象函数
- 动机:这样抽离出来的流程函数也叫模板函数,模板上插槽接口固定,然而提供插槽的模板函数是一致的
- 做法:后续新增的类,只需实现超类抽象函数就可以完成扩展
用委托取代继承
- 场景:子类只使用超类接口的一部分,或者直接不需要继承来的数据
- 思路:在子类中新建字段保存超类,然后调整子类函数,让它委托超类,然后去掉两者的继承关系
- 动机:略
- 做法:略
用继承取代委托
- 场景:两个类的委托关系过多,且委托函数都很简单
- 思路:让委托类继承受托类
- 动机:如果你没有使用所有受托类函数,那么就不要用这个重构方法,继续保持委托关系,使用其他重构方法;另外受托对象可变时,也要注意
- 做法:略
大型重构
Kent Beck和作者所写
本章介绍了4个大型重构的思路,也是大型程序容易遇到的4个问题
- 梳理和分析继承体系:往往因为某个继承体系承担的两个甚至更多责任,有一个特征是,某一层级的所有类,子类都以相同形容词开始。可以通过委托的形式,对继承体系做正交化
- 过程化设计转化为对象设计:往往出现在过程化风格传统语言中。可以将数据记录变为对象,拆分大块行为为小块,然后将行为转移到相关对象中。
- 分离领域和UI:出现在有GUI的场景中。传统的MVC设计模式就是将领域逻辑分离出来,用接口的方式和UI部分代码对接
- 提炼继承体系:有的类做了太多工作,里面经常有较多的条件表达式。对于这种,可以借助面向对象中的子类和多态或者策略模式实现
重构与现实
重构在某些角度和技术演进很像。技术的接纳过程类似一条钟形曲线。前段包括先行者和早期接受者,中部大量人群包括早期消费者和晚期消费者,最后则是行动迟缓者。不同人有不同的消费动机。先行者和早期接受者感兴趣的是新技术,“范式转移和突破性思想”的愿景;早期和晚期消费者则关心成熟度、成本、支持程度,以及这种新思想/新产品是否被和他们相似的其他人成功使用。
尾声
- 重构工具能节省你的重构时间
- 永远记住“两顶帽子”,重构时保持代码功能不变