重构:干掉有坏味道的代码
第一次读重构 - 改善既有代码的设计(Refactoring: Improving the Design of Existing Code)这本书还是在学校的时候,那个时候刚开始写Java代码,师兄推荐了两本书《重构》、《设计模式》。在今日看来,这两本书都是经典好书,得谢谢我的师兄。
最近,打算重新读一下这两本书,先读了重构,感觉还是收获颇多。想来这两本书都是比较偏向实践的,并不是读一遍就可以束之高阁了,而是应该常读常新。
本文地址:https://www.cnblogs.com/xybaby/p/12894470.html
去年重读了代码整洁之道这本书,也写了一篇笔记 什么是整洁的代码。今年重读《重构》的时候,发现书中很多内容都是相同的,作者好像都是叫 Martin 什么的,我还想难道是同一个人?
查了一下,并不是,重构的作者是 Martin Fowler;而clean code的作者是 Robert C. Martin ,江湖人称 "Uncle Bob"。
不过好像两位都在面向对象、敏捷领域有所建树。By the way,重构的第一版写于1999年,(本文也基于第一版的译文),而clean code的第一版写于2009年,且clean code是有参考 “refactoring: Improving the Design of Existing Code”的。
在我看来,重构这本书的核心价值有三部分:
- 指出有“坏味道”的代码
- 对这种代码给出重构的详细步骤,这些步骤保证重构过程是安全的
- 关于引入新技术、新思想的一些思考,如重构、代码复用、TDD
当然,第二部 -- 针对各种有问题的代码的重构步骤 -- 是本书的重点,不过现在的IDE都提供了对重构的支持,大大提升了重构的效率和安全性。
认清重构的事实
书名叫 Refactoring: Improving the Design of Existing Code ,作者也着重强调:重构是在不改变软件可观察行为的前提下改善其内部结构。也就是说,对外的API,以及API的行为不应该被改变,不要在重构的同时修bug,或者添加新功能。
重构是为了改善程序的内部结构,而改善的目的在于增加代码的可读性,让代码更容易维护和修改。
我们也常常为了提升性能而修改代码,不幸的是,为了性能而实施的修改通常让代码变得难以维护,标志就是得加注释说明为什么要这么修改。
重构的前提
不管怎么样,手动还是借助工具,重构还是会修改代码,只要修改代码,就可能引入错误。那么重构给出了就是一套经过验证的、有条不紊整理代码的方法,通过逐步改进、及时测试、出错则回滚的方法来最小化引入bug的概率。
上面提到了逐步验证,这就需要在重构的时候需要有可靠的、自动的测试环境,如果没有靠谱的测试方案,那么最好还是不要重构。
什么时候重构
程序员新学得一个技能, 比如重构,就很容易认为这是解决编程问题的屠龙技,迫不及待想找个环境用起来,但只有在合适的时机使用才能发挥其效用。
- 增加新的功能前
- 修改bug前
- code review时
成功的软件都需要长时间的维护、迭代,那么我们程序员难免就会接受其他程序员的遗产:代码以及bug。如果需要在旧代码上加新功能,但旧代码的混乱程度又让人无从下手,该怎么办呢?
- 重写:既然之前的代码很SB,那我就重新写点NB的代码。但现实很残酷,重写的时间、人力成本是多少?引入的新BUG怎么算?况且,如果贸然动手,新造的轮子还可能不如原来的轮子。
- 复制、修改:看看系统中有没有类似的功能模块,复制过来,改一改,如果恰好能工作,那就万事大吉。但我们知道,重复的代码是不好的,bug、“有坏味道”的代码也被复制和传播。
- 重构:处于重写与复制的中间状态,在不修改程序的外在表现的情况下,改善代码的质量,也让新功能的添加更加容易。这也符合clean code中提到的童子军军规:
让代码越来越好,而不是越来越坏
重构与设计
不管是瀑布流模型开发,还是敏捷开发,都是需要有设计的。过度设计和不做设计都是有问题的,而重构简化了设计:无需过度追求灵活些,合理即可。所谓灵活些,即可应对各种需求变化,但灵活的系统比简单的系统复杂得多,且难以维护。
重构使得修改面向对象的程序设计变的很容易,因为可以重构继承体系,将field、method移动到不同的类中,通过多态移除各种复杂的条件判断。某种程度上,重构可以简化详细设计,但不能替代架构设计,或者说概要设计。
值得注意的是:
- 本书的重构手法只适合单进程单线程程序,而不一定适合多线程、分布式。对于多线程,一个简单的inline就可能导致各种问题。而对于分布式系统的重构,更多的是架构层面的设计。
- 越难重构的地方,越需要精心设计,比如数据库字段,通信协议,对外接口。保持对旧协议的兼容是一件非常麻烦的事情。
有“坏味道”的代码
需要重构的代码往往都散发着“坏味道”,让专业的程序员感受到不舒服。这一部分,罗列了作者总结的“坏味道”。
需要注意的是,本书罗列的坏味道不一定很全面,比如一个变量命名为temp
,大概率就是一个坏味道,但本书中就未提及这种情况。因此,非常建议配合clean code一起阅读。
另外,个人觉得本节还有一个严重问题:那就是缺乏例子。“坏味道”是我们为什么要重构,而后面的具体手法是如何重构,why 比 how 更重要些,所以个人感觉应该在描述"坏味道"的时候给出代码示例。
重复的代码 -- duplicated code
最简单的情况,就是两段代码有相同的表达式语句,处理方法也很明确,那就是extract method
,然后应用这个新的方法。另外一种常见情况,就是这两段相同的代码位于不同的子类 -- 往往是新增子类的时候部分复制了其他子类的代码,这个时候就应该使用pull up method
将公共代码抽取到基类去。
当然,两段代码也可能是相似但不完全相同,那么可以考虑将差异部分子类化,即使用form template method
。或者将差异部分参数化,即通过参数控制不同的逻辑,但需要注意的是,参数会不会导致两种截然不同的行为,即parameterize method
与replace parameter with explicit methods
的区别。
最后,也经常发现两个类之间有相同的重复代码,但是二者之间并没有继承关系(并不是is-a关系),那么可以extract class
将公共部分提取出来,以组合的方式使用,或者使用多继承--Mixin 继承其实现。
过长的函数 -- long method
过长的函数往往冗杂着过多的细节,在什么是整洁的代码一文就曾经中, 代码的组织应该像金字塔一样,“每个函数一个抽象层次,函数中的语句都要在同一个抽象层级,不同的抽象层级不能放在一起”。
对于过长的函数,负责任的代码作者往往会给出一些注释:解释某一小段代码的作用,这其实就暗示着我们可以把这段代码移到一个独立的函数,然后取一个恰当的名字来展现其意图。这个新函数的名字应该体现做什么,而不是怎么做,这样,新函数的名字就可以取代原来的注释。
如果新抽取出来的子函数需要用到原函数中的参数或者临时变量,那么这些都需要参数化到子函数,这可能导致子函数参数列表过长的问题,这个问题及其解决办法在后面阐述。
除了注释,还有什么“味道”暗示应该提取子函数呢,比如 if then else
中有大段的代码,这个时候可以使用Decompose conditional
处理条件表达式。
过大类 -- large class
单个类有太多的实例属性,而且其中某些属性经常独立于其他属性一起使用,那么可以使用extract class
。
比如一个课程信息类 Course
,里面包含了 CourseId、CourseName、TeacherId、TeacherName、TeacherSex
等属性,那么坏味道就是:很多属性名拥有相同的前缀。因此可以通过extrace class
将 CTeacherId、TeacherName、TeacherSex
抽取到新的类 Teacher
。然后就可以去掉这些属性名的前缀,同时Course
类持有 Teacher
即可。
或者一些属性只在某些特殊状态下使用,那么可以考虑extrace subclass
。
过长参数列表 -- long parameter list
过长的参数列表让代码变得难以阅读和理解,要搞清楚每个参数的意义就需要大费周折。
如果某个参数可以从函数内可访问的对象(类属性或者其他参数)获得,那么这个参数就是冗余的,就可以 replace parameter with method
。
另外,传递的若干个参数可能只是某个对象的一堆属性,那么就可以考虑直接传递该对象 preserve whole object
,不过需要注意,preserve whole object
可能会导致非预期的依赖关系,这在静态类型语言(如C++)中又是一个复杂问题。
发散式变化 -- Divergent change
某个类由于不同的原因要在不同的地方进行修改,事实上,这违背了类的单一职责原则(SRP),通常也是过大类。解决的办法就是拆分成不同的类(子类)。extract class
or extract subclass
散弹式修改 -- shotgun surgery
与 Divergent change
恰好相反,为了需要响应一个变化而修改大量的类
依恋情结 -- feature envy
函数对某个类的兴趣高于自己所在的类。如大量使用其它类的数据,常见的是取出其他对象的属性,然后一通计算后再赋值。解决办法,将总是一块儿变化的东西放在一起:数据与对数据的操作。
数据泥团 -- Data clumps
如果某些数据经常一起变化,那么应该将这些数据提取到某个类中,正如之前过大类中的例子。提取出单独的类,减少了属性和参数的个数,而且接下来就可以找出 feature envy
,进一步重构。
基本型别偏执 -- primitive obsession
类似于上一条“数据泥团”,不过更强调基本数据的封装
使用基本类型,比如用两个字段 begin
, end
来表示区域[begin, end)
,仅从可读性上来说肯定不如封装成一个类 range
switch
switch 的问题在于重复,这里需要switch case,那么很可能其他地方也要switch case。如果增加一种case,那就得到处修改,违背OCP原则。
使用多态是常用的解决办法,replace condition with polymorphrsim
,过程是这样子的:
extract_method
move method
replace type code with subclass(strategy、state)
replace condition with polymorphrsim
平行继承体系 -- parallel inheritance hiearachies
这是shotgun surgery
的一种特化,某各类增加了一个子类导致另外一个类也必须增加一个子类,虽然设计模式中可能出现这样的情况,但坏味道可以帮助我们加以区分:某个继承体系的类名前缀和另一个继承体系的类名前缀完全相同
冗余类 -- Lazy class
没有什么价值的类。类中不在有什么实质性工作,可能是因为逻辑变化,可能是因为重构,这个时候可用通过collapse hierarchy
或者 inline class
去掉这样的类。
夸夸其谈未来 -- speculative generality
过度的设计、抽象、泛化,各式各样的钩子和特殊情况处理,越灵活越复杂,越是难以维护。坏味道:函数或类的唯一用户是测试用例
令人迷惑的暂时字段 -- Temporary Field
某个成员变量只是在某些特殊情况才会用到,不用到的时候会导致迷惑,或者某个成员变量的赋值只是为了后续方便某个成员方法的调用,根据不同的情况可以参考一下重构手法:
extract class
将这些特殊的field移到新的类- 使用
null object
避免写出条件分支 - 函数调用时传入这些特殊变量
过度耦合的消息链 -- message chain
对某一个对象不停索求另一个对象,坏味道就是 A.getB().getC().dosth()
,这就是 clean code 中提到的火车失事,违背了德墨忒尔律(The Law of Demeter):模块不应了解他所操作的对象的内部情况
解决的办法是Hide delegate
, 但这样的重构又可能导致下一个问题:middle man
中间人 -- middle man
过分使用委托,如果一个类的多半接口都是委托给其他类,那么可以考虑remove middle man
。这有点类似软件架构模式中提到的污水池反模式(architecture sinkhole anti pattern)
如果middle man也有一些职责,可以考虑 replace delagate with inheritance
让其变成最终对象的子类。
狎昵关系 -- inappropriate intimacy
两个class过于亲密,使用彼此的private。抽取出新的类,或者move filed
不完美的类库 -- incomplete library class
类库是代码复用的绝佳体现,但是类库的作者不可能预料到所有的需求,因此怎么在不改源码的基础上完成想要的工作:
introduce foreign method
introduce local extension
被拒绝的馈赠 -- Refused Bequest
坏味道:子类复用了基类的行为(实现),但却不想支持基类的接口,这违背了LSP原则:子类型必须能够替换它们的基类型。
C++中public继承的其实就是接口,而private继承的则是实现,通过private继承,基类中的所有方法都变成private。更通用的重构手法: replace inheritance with delagate
过多的注释 -- comments
注释是好东西,散发着香味,但你不应该用它来掩盖臭味
使用extract method
或者rename method
来解释注释的行为。对于参数的命令也应该能望文知义
具体的重构手法
找到坏味道之后,就是如何安全的进行重构,书中罗列了各种重构手法的具体的实施步骤,按照这种逐步推进、逐步测试的方法,保证重构没有影响到代码的外在表现。当然,IDE提供的重构工具让部分重构变得更加容易和安全。
重新组织函数
函数总是过长,尤其是在漫长的维护过程中,函数内的代码数量会逐渐膨胀。
Extract method
需要注意:
- 保证函数名称与函数本体之间的语义距离 -- 一个好的函数名
- 对于局部变量和参数的处理:参数
Inline Method
难点:
- 是否是多态
- 得找出所有引用点
Inline temp
临时变量只是被一个简单表达式赋值一次。有助于后续的Extract method
,也可以作为replace temp with query
的一部分使用。
注意:
- 如果表达式较为复杂不应内联,影响可读性与效率
- 多次赋值的话也不能内联
replace temp with query
将一个表达式提取为一个单独的函数,新函数可以被其它函数调用。之中有一段实例代码,用python改写如下:
def calc_price(self):
base_price = self._quality * self._item_price
if base_price > 1000:
return base_price * 0.95
else:
return base_price * 0.98
重构后是这样的
def calc_price(self):
if self.base_price() > 1000:
return self.base_price() * 0.95
else:
return self.base_price() * 0.98
def base_price(self):
return self._quality * self._item_price
个人觉得这个例子并不是很恰当
- 在没有改善可读性的情况下,引入了重复调用带来的开销
- 有时也会有问题,原始的代码
base_price
一旦计算后是不会发生变化的,都提取成query之后就不能保证了
个人认为,即使为了解决temp只在函数内部生效而无法复用的问题,也应该改成:
def base_price(self):
return self._quality * self._item_price
def calc_price(self):
base_price = self.base_price()
if base_price > 1000:
return base_price * 0.95
else:
return base_price * 0.98
对于python,query还可以实现为property的形式,如果确定query的结果是固定的,还可以使用cached_porperty优化。
introduce explaining variable
将复杂表达式变成一个解释性的局部变量,解决可读性问题
split temporary variable
一段代码中,一个临时变量只能代表一个意思,否则应使用不同的临时变量。
remove assignment to parameter
移除对参数的赋值,防止误改、不小心的覆盖,可读性更好
- 不要对参数进行赋值,以一个临时变量取代参数的位置
- java只采用pass by value传递方式。对于基本类型,同C++一样;对于引用类型,可以改变参数内部的状态(调用者实参的内部状态随之改变),但对参数重新赋值没有任何意义。
- 可以给参数强制加上final修饰符,保证参数不被赋值
在对象之间搬移特性
move method
迁移的过程中可能需要用到source class的特性(成员变量或者成员方法)。处理方式:
- 将这个特性移到target class中;
- 在target class中建立一个对source class的引用;
- 将source object作为一个参数传递给target method(eclipse中的move就是该方法);
- 将特性作为参数传递给target method
movie field
常常是extract class
的一部分,先移动field,在移动method
extract class
先 move field,再move 必要的 method
需要考虑的是,新的类要不要对外公布
inline class
Hide delegate
eg:
value = AObject.getBObject().getVlaue()
到
public int AObject::getValue(){ return bObject.getgetVlaue()}
value = AObject.getVlaue()
而且应该考虑要不要干掉 AObject.getBObject
remove middle man
与Hide delegate
相反,如果一个server全是各种简单委托
introduce foreign method
需要调用的类缺少一个你需要的方法
良好的建议在于:这个方法应该属于服务类,因此只需将类的对象作为第一个参数就行,(其他参数应该是服务类 “新方法”的参数)
introduce local extension
- 已有且不能修改的类无法完成需求
- 使用继承或者组合解决
重新组织数据
self encapsulate field
对属性的访问通过getter和setter实现
适用情况:
- 可能对属性访问做控制
- 可能会有subclass,且subclass的getter、setter方法不同于superclass
replace array with object
一个数组,其中的元素表示不同的东西
duplicated observed date
有一些domain data(业务处理逻辑相关的)置身于GUI控件中,而domain method需要访问之。
domain class 和GUI呈现分离,共享的数据通过观察者模式实现同步控制
replace magic number with symbolic constant
magic number 真的是人见人恨
encapsulate collection
如果函数返回一个集合,那么这个返回值应该是只读的,而且不应该提供群集合的 setter 方法,而应提供加入、删除集合元素的方法
Java中的unmodifiable
系列就是返回只读集合
replace record with data class
record 比如来自数据库,用一个 dataclass 将所有 field 声明为 private ,提供对应的访问函数
replace type code with class
类型编码(type code)是一些常量或变量,一般有多个可能的值。普通常量使用的时候缺乏类型检查,类似C++中的define,而class强加类型检查。
比如血型如果用4个整数(c语言中的enum)表示,那么是传参的时候无法限制类类型,可读性也差。C++11中enum class就解决了这个问题
前提是类型码不会用于switch中,否则就得使用下面的重构手法
replace type code with subclass
如图所示:
type code影响到了其所在类的行为, 那么就得使用多态,该方法为replace conditional with polymorphism
做准备。
前提是type code在对象创建的时候就确定,且声明周期内不可变。如果 type code可能是变化的,只能使用replace type code with state/strategy
replace type code with state/strategy
和replace type code with subclass
一样,都是为replace conditional with polymorphism
做准备
简化条件表达式
Decompose conditional
从if,then,else三个段落中提炼出独立函数,使代码更加清晰
consolidate conditional expression
一系列条件测试如果得到的是相同的结果,那么将这些条件合并为一个表达式,并将这个表达式提炼为一个独立函数。extract method
也更好体现了做什么,而不是怎么做。
如果这些条件逻辑上本来是彼此独立的,那么不应该使用本项重构
consolidate duplicated conditional Fragments
在条件分支上有相同的一段代码,那么应该将这一段代码移到条件式之外,这是经常遇到的情况。
关键是这样更好体现了哪些是随条件变化而变化的,同时避免 duplicated code。
remove control flag
在循环的布尔表达式中,某个变量起控制标记,如 while(exit)
,以break语句或者return语句代替控制语句
replace nested conditional with guard clause
卫语句(guard clause):如果某一条件极其罕见,就应该单独检查该条件,并在该条件为真时立刻返回,这样的单独检查成为卫语句。当然,我更喜欢称之为early return,往往能减少嵌套的深度,让代码可读性更好。
本质:给予某一条件特别的重视(if then else表示对分支的重视是相同的),卫语句表示:一旦这种情况发生,应该做一些必要的清理工作,然后退出。
replace conditional with polymorphism
将一个条件表达式的分支放进一个subclass的覆写函数内,并将原始函数声明为抽象函数
关于对replace type code with state/strategy
和replace type code with subclass
的选择:核心在于 type code 是否可能会在对象的生命周期内改变。
introduce null object
如果需要再三检查一个对象是不是null,那么以一个null object
替换为null时的情况。null object 一般是常量,可以用 singleton 封装,其属性不会发生改变
需要注意的是:
- 只在有大多数的客户代码需要 null object 做出相应相应时,才有必要使用 null object。当然,如果少数地方需要做出不同响应,那么也可以用
object.isNull
区分 - null object 是逻辑上可能出现的,是一种特殊情况,并不是异常。比如书中的例子:一个出租房确实可能暂时没有租客。
introduce assertion
assertion 应该是一个永远为真的表达式,如果失败,表示程序出了错误。assert既可以帮助排查bug,也可以帮助读者理解代码作者的假设、约束
简化函数调用
rename method
add parameter
需要考虑增加参数是否会导致坏味道,long parameter list
。如果可以通过已有的参数、属性获得新参数的值,那么就不应该增加。
remove parameter
重构的时候要注意多态(继承)的情况,不要遗漏。上同
separate query from modifier
将查询操作和修改操作分开
parameterize method
若干函数做了类似的工作,只是函数本体中包含了不同的值。将导致函数差异的值作为参数传入,如下面的代码:
def tenPercentRaise(self):
self._salary *= 1.1
def fivePercentRaise(self):
self._salary *= 1.05
# 重构后的代码
def raiseWithFactor(self, factor):
self._salary *= (1 + factor)
重构后,只保留一个方法raiseWithFactor
,但新函数应该加上参数合法性的检查。个人认为,如果factor
的取值固定为少数的几个值,那么提供不同的接口也是可以的,只不过对外接口统一调用同一个私有接口。
replace parameter with explicit methods
函数的操作完全取决于参数值,则针对参数的每个参数值,建立一个独立的函数
坏味道很明显,参数是离散的,函数内以条件式检查这些参数值,并根据不同参数值做出不同反应。比如下面这种类型的代码
def setSwitch(self, is_on):
if is_on:
# do a lot of thing let switch on
else:
# do a lot of thing let switch off
preserve whole object
解决long parameter list
的一种重构手法
replace parameter with methods
对象调用某个函数,将其返回值作为参数传递给另一个函数,而后面一个函数也可以调用前一个函数 。那么在后一个函数中取出该项参数,并直接调用前面一个函数。动机在于如果可以通过非参数列表的方式获得参数值,那么就不要使用参数列表
使用前提
- 参数计算过程(即前一个函数)不会依赖调用端的某个参数
- 参数的存在可能是为了将来的弹性时也不能使用本项重构
introduce parameter object
某些参数总是很自然地同时出现----把这些参数抽象为一个对象,比如经常遇到的是以一对值代表一个范围,如(start、end)、(lower、upper),用Range取代之
好处:
- 缩减参数列表长度;
- 更易理解和修改;
- 可以把一些反复进行的计算移到对象里面(比如计算range差值)
remove setting method
如果class中某个属性在初始化的时候就设置,以后就不在改变了,那么应该去掉改属性的setter,还可以将属性设置为final(or const)。
hide method
一个函数,从来没有被其它class使用过,那么它应该为private。最小化对外接口,需要的时候再开放。
replace constructor with factory method
常常在多态 或者replace type code with subclass
中使用。可以用来实现change value to reference
,或者单例。同时,工厂方法也会比构造函数重载可读性更好
encapsulate downcast
不要让用户对你的函数返回值进行downcast,返回用户需要的类型
这是低版本Java的问题(没有模板),Java5.0之后没问题了。这也是强类型OO语言的问题,python就没有这个问题。
replace error code with exception
错误,异常与自定义异常 这篇文章对error code 和 exception 有较多讨论
replace exception with test
exception 不应该作为流程控制的手段,如果某种情况的出现不是意料之外的,那么就不应该抛出异常
处理继承体系
pull up field
把子类重复的属性移动到基类
pull up method
把子类重复的函数移动到基类,如果两个子函数相似但不尽相同,可以考虑使用form template method
pull up constructor body
各个子类的构造函数代码有一样的部分,则在基类中创建构造函数,在子类中调用
push down method
push down field
extract subclass
class中一些特性只被某些实体使用,那么新建一个subclass中,将这些特性转移到subclass
需要考虑到extract class与extract subclass的区别(委托与继承)
extract superclass
两个类有相似特性,把相似的部分移动到superclass,有时候为了代码复用也可以这么做
collapse hierarchy
基类与子类没有太大区别----合并之
form template method
replace inheritance with delegation
某个subclass只使用superclass的一部分,或是根本不需要继承而来的数据,则可以改继承为委托。
继承和委托也是adapter模式的两种实现方式,本人也倾向于delegation
replace Delegation with inheritance
太多的简单委托关系
关于引入新技术、新思想的思考
即使一个新技术(新思想)已经经过社区的验证,要引入到开发团队来也不是一件容易的事情,也会遇到重重阻力:
- 大多数人还不知道如何使用这项新技术
- 引入新技术的收益要长期才能看出来,那么何必现在去付出呢?如果回报周期过长,那么在收获的时候可能已经不再当前的位置了
- 新技术的引入并不是非用不可,还需要花掉一些时间,老板(项目经理)愿意吗?
- 在线上项目使用新技术,反而可能引入BUG,冒险是否值得?
如果你本身就是老板(技术Leader),且能够顶住来自产品的压力,那么可以强推一项新技术,虽然强推效果也不一定好。但如果是作为平级,怎么推广呢?如何解决这些障碍?
第一:培训与工具,通过培训、分享让团队快速掌握新技术,使用工具让成员掌握新思想。比如,想要遵守同一套代码规范,那么最好配上相应的代码检查。
第二:展现短期、肉眼可见的利益,新技术不仅要有长期收益,还得在短期内就展现出其优点。如果短期内就能看到好处,大家就愿意去积极尝试。
第三:降低开销,降低上手难度,技术的投入也是讲究投入产出比的,使用成本越低,大家就不会排斥。
第四:安全过渡,逐步进行,如果是线上项目,最好能有健全的回滚机制。
本质上,都是通过向你的Leader或者小伙伴展示,这个新东西又好又不贵,使用起来还很方便。
更有意思的是,书中提到Geoffrey Moore提出来的技术接纳曲线:
一个思想、技术、产品即使有先行者、尝鲜者的支持,但想要大众市场接受,还要跨过一条鸿沟。鸿沟的存在源于不同人的不同诉求:先行者关注的是新技术本身,而普罗大众关注的是成熟度、引入(使用)成本。
在构建之法也有很详细的分析。