refactoring-2e 阅读笔记(边读边记)
原文:library/refactoring-2e/text/。行号 = 清洗文本行号(1 行 1 段)。
导航章跳过:01-04-fm(版权/提要/赞誉)、08-fm(服务与支持)、21-fm(参考文献)、22-fm(重构列表)、23-fm(速查表)。
译者序(05-fm,熊节导读,有独立立场)
- 熊节 2009 整理第 1 版译稿时就察觉「重构」概念的表里分离:招牌人人挂,实际干的多是「刀劈斧砍」(
05-fm.txt:3-8)。 - 10 年后愈演愈烈:当年一线技术人员走上领导岗位,把「重构」名号用到架构、组织调整上,基本功欠缺如影随形(
05-fm.txt:10-14)。 - 猜 Fowler 第 2 版会「拔高」谈架构级/组织级重构,孰料他反而做得更扎实(
05-fm.txt:16-20)。 - 两版对比:第 2 版手法更内聚、更连贯、重视组合;第 1 版整章的「大型重构」全删;复杂手法(被监视数据、塑造模板函数)不再收录;新增提炼变量、移动语句、拆分循环、拆分变量等更细致的操作(
05-fm.txt:22-28)。 - 基本功 = 识别坏味道、测试先行、行为保持的变更动作(
05-fm.txt:30)。 - 例:保持对象完整,1 版是原函数添参数,2 版是先建空函数做完调整再整体替换——出错时测试失败范围更小(
05-fm.txt:32-34)。 - 王健「十六字心法」:旧的不变/新的创建/一步切换/旧的再见(
05-fm.txt:36-44)。 - 核心分歧: Fowler 引入「很快会再次修改甚至删除」的临时元素——这是对变更过程(而非只结果)的设计;「刀劈斧砍」只抱结果憧憬提刀上阵(
05-fm.txt:46-52)。 - 立场:重构代表软件开发的匠艺——对「正确的做事方式」的重视(
05-fm.txt:54-58)。 - 落款:熊节 2019-01-26 成都,与林从羽合译(
05-fm.txt:71-73)。
第1版序(06-ch01.txt,Erich Gamma,1999-01)
- 「重构」概念来自 Smalltalk 圈子,框架开发中诞生:这东西不可能一开始就完全正确(
06-ch01.txt:3-8)。 - 代码被阅读和修改的次数远多于编写次数(
06-ch01.txt:6)。 - 重构有风险:修改正在工作的程序可能引入不易察觉的错误;「挖得越深机会越多……最后给自己挖了个大坑」——必须系统化(
06-ch01.txt:10-14)。 - 《设计模式》:「设计模式为重构提供了目标」,但达到目标是另一难题(
06-ch01.txt:14-16)。 - Gamma 与 Kent Beck 三万英尺高空结对,体验小步重构,开发压力反而小(
06-ch01.txt:26-29)。
前言(07-fm)
- 开场故事:顾问建议整理凌乱继承体系,项目经理拒绝(「能运行就别动」),程序员花两天删掉一半代码功能无损;6 个月后项目失败——代码太复杂无法调试;重启时 Kent Beck 以持续重构整理代码(
07-fm.txt:3-36)。 - 定义:重构 = 在不改变代码外在行为的前提下对代码做出修改,以改进内部结构;「在代码写好之后改进它的设计」(
07-fm.txt:44-46)。 - 与传统「先设计后编码」相反:设计在开发过程中逐渐浮现,「构筑-设计」反复互动(
07-fm.txt:53-61)。 - 结构:第 1 章示例、第 2 章原则、第 3 章坏味道(Kent Beck 写)、第 4 章测试、第 5 章起重构名录(
07-fm.txt:64-80)。 - Web 优先的书:权威版本是网站,纸质版是精选(
07-fm.txt:95-100);编者注:该表述仅适用英文原版(07-fm.txt:182)。 - JS:选 JS 因为多数读者看得懂;不推荐该语言;不是「用 JavaScript 进行重构」的书;JS 特有重构(回调→promise/async-await)不讨论(
07-fm.txt:128-147)。 - 命名的价值:「给形形色色的重构手法命名是编 写本书的重要部分」——提炼函数(106)、拆分阶段(154)成为沟通词汇,帮开发者选择自动化重构(
07-fm.txt:177-180)。 - 渊源:Ward Cunningham、Kent Beck 最早倡导;Bill Opdyke 博士论文是重构研究第一份详细书面成果;Refactoring Browser 是第一个自动化重构工具(
07-fm.txt:190-198)。
第1章 重构,第一个示例(09-ch01.txt,1984 行)
示例:戏剧演出团账单。plays.json:hamlet(悲剧)/as-like(喜剧)/othello(悲剧);invoice:BigCo,55/35/40 座。输出总额 $1,730.00、47 credits(09-ch01.txt:33-122)。
计费:悲剧基础 40000(美分),超 30 座每座 +1000;喜剧基础 30000,超 20 座 +10000+500×(座-20),另每座 +300。积分:max(座-30,0),喜剧再加 floor(座/5)。
- 1.2 评价:程序能跑,但修改点难找→易错;「如果你要给程序添加一个特性,但发现代码因缺乏良好的结构而不易于进行更改,那就先重构那个程序……然后再添加该特性」(
09-ch01.txt:140);「是需求的变化使重构变得必要。如果一段代码能正常工作,并且不会再被修改,那么完全可以不去重构它」(09-ch01.txt:160)。两个变化:HTML 输出 + 更多剧种(计费/积分规则会再改)。 - 1.3 第一步永远是可靠测试;自我检验(绿/红)(
09-ch01.txt:165-180,箴言在:180)。 - 1.4 分解 statement:提炼函数(106) amountFor;改名 thisAmount→result(个人风格:返回值一律叫 result
:362);参数名带类型 aPerformance(从 Kent Beck 学,:389-392,箴言「傻瓜都能写出计算机可以理解的代码」:394);以查询取代临时变量(178) playFor;内联变量(123);改变函数声明(124) 删 play 参数;性能警钟:查找从 1 次变 3 次(:559),先不管(:561-562)。 - volumeCreditsFor 提炼(累加变量:整块提炼再返回,
:628-630);format→usd(名字表意,顺带把 /100 搬进去;美分整数是常见做法,避免浮点存货币,:761-775)。 - 移除 volumeCredits:拆分循环(227)→移动语句(223)→提炼 totalVolumeCredits→内联变量,4 步每步编译测试提交(
:890-898)。totalAmount 同法,函数名被变量占用→先取名叫 appleSauce(:912)。 - 性能总口径:「大多数情况下可以忽略它。如果重构引入了性能损耗,先完成重构,再做性能优化」(
:887-888);「软件的性能通常只与代码的一小部分相关」(:879)。变复杂=测试失败且无法马上看清→回滚重做更小步(:900-903)。 - 1.5 进展:顶层 statement 只剩 7 行(
:1043)。 - 1.6 拆分阶段(154):renderPlainText + statementData 中转数据结构;enrichPerformance 用 Object.assign 浅副本,「我总是尽量保持数据不可变——可变的状态会很快变成烫手的山芋」(
:1183-1185);搬移函数(198) 把 playFor/amountFor/volumeCreditsFor 搬进第一阶段;以管道取代循环(231) reduce(:1332);createStatementData 拆到独立文件(:1363-1385);htmlStatement 复用同一数据(:1390-1406)。 - 1.7:44 行→70 行,行数增加但可读性提高,「可演化的软件却以明确为贵」(
:1511-1515);营地法则箴言(:1517)。 - 1.8 多态:PerformanceCalculator 类;以多态取代条件表达式(272)、以子类取代类型码(362)、以工厂函数 取代构造函数(334)——JS 构造函数无法返回子类(
:1746-1747);Tragedy/ComedyCalculator;超类 get amount(){throw 'subclass responsibility'}(:1843-1845);volumeCredits 通用逻辑(max(aud-30,0))放超类,喜剧 super+floor(aud/5)(:1852-1862)。 - 洞见:越多函数依赖同一套类型分支,多态越有益(
:1946-1949);把分支查找集中到一个工厂函数(:1946)。 - 1.10 结语:三个节点=①分解成嵌套函数②拆分阶段分离计算与渲染③多态计算器(
:1961-1963);好代码检验标准=「人们是否能轻而易举地修改它」(:1973,:1979);节奏感:步子之小+每步可工作,「小的步子可以更快前进」(:1981-1985)。 - Ward Cunningham:理解只是脑海中转瞬即逝的灵光,要把它搬回代码里(
:237-240)。 - JS「编译」脚注:Babel 等(
:317-319)。
第6章 第一组重构(14-ch06.txt,2055 行)
章首:最有用的一组。提炼↔内联(函数/变量)互为反向;提炼关键在命名;改变函数声明管「关节」;先封装变量再改名;参数结伴→引入参数对象;函数+数据→组合成类或变换;再进一步→拆分阶段(14-ch06.txt:3-17)。
- 提炼函数(106):动机=「将意图与实现分开」——需要花时间弄清一段代码在干什么,就提炼并以用途命名(
:44-56);「一个函数一旦超过6行,就开始散发臭味」,常写 1 行函数(:58-59);highlight 例子:名字比实现 还长没关系,意图与实现距离大(:60-64);性能担忧如今罕见,短函数反而利于缓存(:66-69);注释常提示好名字(:73-74)。做法:被赋值的局部变量多时先放弃提炼,先用拆分变量/以查询取代临时变量(:105-107)。返回多个值:最好挑另一块代码/返回记录,更常用查询化(:387-392)。嵌套函数→搬移时局部变量问题「直到最后的搬移时才突然暴露」(:394-398)。 - 内联函数(115):动机=函数本体与名字同样清晰时去掉间接(
:417-422);一群组织不合理的函数→先全内联再重新提炼(:424-425);「间接层有其价值,但不是所有间接层都有价值」(:427-429)。不具多态性才可内联(:433-435);递归/多返回点等复杂情况就不该用(:445-447)。 - 提炼变量(119):复杂表达式难读→局部变量命名(
:542-546);调试抓手(:548);名字在更宽上下文有意义→提炼成函数(:550-554);类中版本:basePrice 等概念适用整类→提炼成方法(:665-666)。 - 内联变量(123):名字不比表达式更有表现力/妨碍重构(
:698-702)。 - 改变函数声明(124):函数声明是「软件系统的关节」(
:733-737);名字+参数列表;参数「人」→「电话号码」扩大适用面、减少耦合(:747-756);支付 vs 到期日:「唯一正确的答案是『没有正确答案』」(:758-766);好办法:先写注释描述用途,再把注释变成名字(:744-745)。两套做法:简单做法(一次改声明+所有调用者)vs 迁移式做法(提炼新函数→旧函数转发→逐个迁移调用者→内联旧函数)(:775-813);已发布 API:标记 deprecated,等客户端迁移,「即 便永远也抵达不了删除旧函数这个快乐的终点,至少新代码有了更好的名字」(:867-871);添加参数时用引入断言抓漏改(:914-925);inNewEngland 例子:参数 customer→stateCode(:938-1001)。 - 封装变量(132):数据难搬移因为无法转发——「把『重新组织数据』的困难任务转化为『重新组织函数』这个相对简单的任务」(
:1029-1030);「对于所有可变的数据,只要它的作用域超出单个函数,我就会将其封装起来」(:1033-1034);不可变数据更重要:「不可变性是强大的代码防腐剂」(:1045-1047);封装值:返回副本/类封装;「数据被使用得越广,就越是值得花精力给它一个体面的封装」(:1176-1178);重载取值/设值函数强烈反对(:1112-1114)。 - 变量改名(137):使用范围越广,名字越重要;动态类型语言把类型放进名字(aCustomer)(
:1191-1194);已发布变量不能改名(:1203-1204)。 - 引入参数对象(140):数据泥团;「真正的意义在于,它会催生代码中更深层次的改变」——识别出新结构→重组行为→新抽象(
:1288-1292);倾向用类+值对象(:1296-1299);温度读数例子:station {name ZB1, readings 47/53/58/53/51}, min/max→NumberRange(:1310-1349)。 - 函数组合成类(144):一组函数形影不离操作同一块数据→组建类;少传参数、便于传递(
:1456-1458);茶水读数例子:reading {customer ivan, quantity 10, month 5, year 2017},三处客户端重复计算基础费用(:1485-1521);好处:改核心数据,派生数据自动一致(:1463-1465)。 - 函数组合成变换(149):变换函数接受源数据,算出全部派生数据填进输出(
:1640-1648);与组合成类的区别:「如果代码中会对源数据做更新,那么使用类要好得多」——变换+源数据更新=数据不一致(:1649-1653);深复制(:1661-1664)。 - 拆分阶段(154):同时处理两件事→拆成顺序阶段,修改时只考虑一个主题(
:1848-1852);编译器例子:词法分析→语法树→转换→目标码(:1858-1862);线索:上下几段各用不同的一组数据和函数(:1864-1867);中转数据结构沟通两阶段(:1869-1884);priceOrder 例子:商品相关价格 + 配送成本(:1888-1973)。
第7章 封装(15-ch07.txt,1273 行)
章首:分解模块最重要的标准=识别模块应该对外隐藏的小秘密[Parnas] (15-ch07.txt:3);类是为隐藏信息而生的(:10);隐藏委托关系↔移除中间人(:13-14)。
- 封装记录(162):记录型结构缺陷=强迫区分「存储的数据」和「计算得到的数据」;区间 {start:1,end:5} vs {start:1,length:5}——3 个值都想知道(
:37-42);可变数据偏爱类,不可变数据记录即可(:44-51);散列/map 类记录:持有什么字段不直观(:53-61);嵌套 JSON/XML 结构值得封装(:63-65)。 - 封装集合(170):常见错误=封装了访问但取值函数返回集合本身,成员仍可被改(
:377-382);提供 add/remove 方法;「依赖于别人的好习惯是不明智的」(:387-389);不赞成用 numberOfOrders 之类特殊方法取代集合(:391-395);最常见做法=返回副本(:402-406);代理 vs 副本区别(:408-409);「最重要的是在同一个代码库中做法要保持一致」(:411-412);Person._courses 例子(:428-441)。 - 以对象取代基本类型(174):字符串「电话号码」需要格式化/抽区号→逻辑占领代码库(
:532-537);「一旦我发现对某个数据的操作不仅仅局限于打印时,我就会为它创建一个新类」(:539-541);「最实用的重构手法之一,价值常为新手低估」(:541-543);Order.priority "high"/"rush" → Priority 类,higherThan(new Priority("normal"))(:527-530,563-649);toString 而非取值函数,传达「数据转换」(:596-598);构造函数 value instanceof Priority 直接返回(:642-646)。 - 以查询取代临时变量(178):分解长函数时,变量变查询→不必作参数传递(
:703-705);类中效果最好,顶层函数参数过多会冲淡好处(:710-712);只适用「只被计算一次且之后不再修改」的变量;快照型(oldAddress)不可用(:714-718);Order basePrice/discountFactor 例子,discountFactor 0.98/超 1000 减 0.03(:740-821)。 - 提炼类(182):类不断成长扩展→一团乱麻(
:839-842);信号:某些数据和函数总一起出现/一起变化(:844-847);子类化只影响部分特性(:849-851);「搬移了某些字段和函数,其他是否因此变得无意义?」测试(:846-847);Person/TelephoneNumber 例子,officeAreaCode/officeNumber(:870-963)。 - 内联类(186):「萎缩类」不再承担足够责任→塞进最频繁用户(
:979-983);重新安排职责:先内联再提炼更简单(:985-988);TrackingInformation 例子(:1000-1019)。 - 隐藏委托关系(189):封装=每个模块尽可能少了解系统其他部分(
:1090-1092);客户端 aPerson.department.manager 揭露 Department 工作原理→Person.get manager(:1097-1100,1132-1140)。 - 移除中间人(192):委托的代价:受托类特性越多转发函数越多,服务类变成中间人(
:1162-1165);迪米特法则吐槽:「如果这条法则当初叫作『偶尔有用的迪米特建议』,如今能少很多烦恼」(:1165-1166);「6个月前恰如其分的封装,现今可能就显得笨拙。重构的意义就在于:你永远不必说对不起」(:1170-1171)。 - 替换算法(195):「删掉整个算法,代之以较简单的算法」(
:1253-1259);先分解原函数+备好测试固定行为(:1264-1265,1269-1270);foundPerson 例子 ["Don","John","Kent"] (:1233-1251)。
第8章 搬移特性(16-ch08.txt,1626 行)
章首:两大类:在上下文之间搬移(函数/字段)、对语句搬移(入函数/到调用者/调整顺序);拆分循环+管道取代循环;移除死代码是「任何一个合格程序员的至爱」(16-ch08.txt:3-16)。
- 搬移函数(198):模块化=修改时只需理解一小部分(
:28-32);最直接动因:频繁引用其他上下文、对自身上下文关心甚少→「让它去与那些更亲密的元素相会」(:39-41);决定越难做,通常说明搬不搬重要性越低;先安置再观察(:47-52);做法:先搬被调用的其他函数(从依赖最少入手)(:56-60);Account.overdraftCharge→AccountType 例子(:21-25);trackSummary/calculateDistance GPS 例子(:86-110)。 - 搬移字段(207):「数据结构才是一个健壮程序的根基」;坏数据结构招致无用代码、掩藏真实意图(
:419-422);数据结构很难一次做对,「这个星期看来合理而正确的设计决策,到了下个星期可能就不再正确了」(:424-429);征兆:调用总要多传另一条记录的字段/改一条总要改另一条(:434-439);类=「多了实例函数的记录」,封装使搬移更容易(:446-452);Customer.discountRate→CustomerContract 例子(:479-507)。 - 搬移语句到函数(213):黄金守则「消除重复」;调用时总有相同代码执行→合并进函数(
:662-666);emitPhotoData/photoDiv 例子,打印 title 行重复(:690-734)。 - 搬移语句到调用者(217):抽象边界随演进偏移;「曾经视为一个整体的行为,如今分化出不同的关注点」(
:808-812);征兆:共用行为在某些调用点需要不同行为(:814-817);只适合边界仅有些许偏移的场景,相去甚远只能重新设计(先内联再重新提炼)(:819-821)。 - 以函数调用取代内联代码(222):命名良好的函数解释用途+消除重复(
:1029-1033);注意「外表相似但无本质联系」——判断:函数名放在内联代码语境里协不协调(:1035-1040);states.includes("MA") 例子(:1020-1025)。 - 移动语句(223):让存在关联的东西一起出现(
:1064-1068);常是提炼函数前的准备(:1070-1073);安全判据:副作用;「对于没有副作用的代码,我几乎可以随心所欲地编排它们的顺序」(:1129-1131);pricingPlan 13 行例子(:1105-1118);命令与查询分离(:1136)。 - 拆分循环(227):身兼多职的循环;一次循环做两件事→修改时得同时理解两件事(
:1239-1242);拆分后单值循环直接返回(:1244-1246);执行两次循环的不安→「先进行重构,然后再进行性能优化」,循环很少成为瓶颈(:1248-1252);youngest/totalSalary 例子(:1264-1296)。 - 以管道取代循环(231):集合管道[mf-cp]:map/filter;「只消从头到尾阅读一遍代码,就能弄清对象在管道中间的变换过程」(
:1371-1378);acquireData CSV 印度办公室例子(:1395-1443)。 - 移除死代码(237):无用代码的代价是阅读时的思维负担;「一旦代码不再被使用,我们就该立马删除它…就算真的需要,也可以从版本控制系统里再次将它翻找出来」(
:1613-1617);注释掉死代码是版本控制不普及年代的产物(:1619-1621)。
第9章 重新组织数据(17-ch09.txt,685 行)
章首:一个值用于多个用途=混乱和 bug 的温床(17-ch09.txt:3-4);引用和值的混淆经常造成问题(:9-10)。
- 拆分变量(240):变量被赋值超过一次=承担一个以上责任→分解成多个变量(
:33-36);循环变量/结果收集变量是正当例外(:28-31);苏格兰布丁例子:acc 两责(初始加速度/合力加速度)→primaryAcceleration + secondaryAcceleration(:57-121);对输入参数赋值:inputValue 既是输入又带结果→result(:131-160);判断基准用原始输入(:158-160)。 - 字段改名(244):Fred Brooks:「只给我看你的工作流程却隐藏表单,我将仍然一头雾水」→数据结构是理解程序行为的关键(
:179-181)。 - 以查询取代派生变量(248):可变数据是最大的错误源头之一(
:316-318);能随时算出来的变量→去掉=朝消除可变性迈进一步;避免「源数据改了忘了更新派生变量」(:320-322);例外:源数据不可变且结果也不变(:324-329);对象风格 vs 函数风格(:326-329);引入断言验证「可以即时计算」的猜想(:359-360);ProductionPlan 例子:applyAdjustment 里 _production += amount 是「数据的重复」(:345-357)。 - 将引用对象改为值对象(252):差异在更新方式:引用=改内部属性,值=整个换新对象(
:463-466);值对象不可变→易理解、可放心传、可复制,分布式/并发尤有用(:468-471);何时不该用:需要多方看见共享修改(:473-474);Product.applyDiscount:money -= → new Money(...)(:452-459)。 - 将值对象改为引用对象(256):共享数据需要更新时,复制多份→必须更新所有副本,漏一个=数据不一致(
:600-602);结果=「对一个客观实体只有一个代表它的对象」→需要仓库(repository)(:604-606);5 个订单同 ID 123 顾客=5 个独立 Customer 对象;「重复的对象总是会让我紧张」(:634-638);仓库对象 registerCustomer/findCustomer Map(:643-658);全局仓库像强力的药物(:684-685)。
第10章 简化条件逻辑(18-ch10.txt,1840 行)
章首:「程序的大部分威力来自条件逻辑,但很不幸,程序的复杂度也大多来自条件逻辑」(18-ch10.txt:3);引入特例常被称作引入 Null 对象(:9-10)。
- 分解条件表达式(260):复杂条件逻辑最常导致复杂度上升;代码告诉「发生的事」但弄不清「为什么发生」(
:27-31);提炼条件判断与分支为函数,突出每个分支的作用与原因(:33-36);「本重构手法其实只是提炼函数的一个应用场景。但我要特别强调这个场景」(:38-39);夏季/常规价格例子(:47-60)。 - 合并条件表达式(263):各不相同、行为一致的条件检查→合并;理由:表述「实际上只有一次条件检查」+为提炼函数做准备(把「做什么」换成「为什么这样做」)(
:131-139);不合并的理由:检查确彼此独立(:141-142);disabilityAmount 例子 seniority<2||monthsDisabled>12||isPartTime→isNotEligableForDisability(:162-194)。 - 以卫语句取代嵌套条件表达式(266):两种风格:双分支都正常 vs 一个正常一个异常;罕见条件单独检查立即返回=卫语句(
:242-248);精髓=「给某一条分支以特别的重视」;if-then-else 传递「各分支同等重要」,卫语句告诉阅读者「这种情况不是本函数的核心逻辑所关心的」(:249-251);「单一出口」规则其实没那么有用,保持清晰才是关键(:253-256);payAmount isSeparated/isRetired 例子(:270-310)。 - 以多态取代条 件表达式(272):复杂条件逻辑是编程中最难理解的东西之一(
:451);场景一:好几个函数都有基于类型码的 switch→每分支一个类(:455-458);场景二:基础逻辑+变体→基础放超类,变体放子类强调差异(:460-462);「有人争论说所有条件逻辑都应该用多态取代。我不赞同」(:464-467);做法:工厂函数创建多态(:471-481);鸟例子 plumage/speeds:EuropeanSwallow "average"/35;AfricanSwallow coconuts>2? "tired"/40-2×coconuts;NorwegianBlueParrot voltage>100? "scorched"/"beautiful"(:487-513)。 - 引入特例(289):重复=多处以同样方式应对同一个特殊值→收拢到一处(
:1186-1188);特例(Special Case)模式;表现形式:字面量对象/特殊对象/封装类返回/变换插入(:1193-1196);null 是「特例的一种特例」(Null 对象)(:1198-1199);Site.customer="unknown" 例子(:1224-1243)。 - 引入断言(302):假设通常没在代码中表现出来,须读整个算法才看得出;断言=条件表达式,应总为真,失败=程序员犯了错,不该被捕捉(
:1766-1773);断言是交流形式:告诉阅读程序执行到此时对状态的假设(:1775-1777);自测试代码降低断言调试价值但交流价值仍在(:1778-1779);「加入断言永远都应该是行为保持的」(:1785);discountRate 恒为正例子(:1794-1808)。
第11章 重构API(19-ch11-11-api.txt,1605 行)
章首:「模块和函数是软件的骨肉,而API则是将骨肉连接起来的关节」(19-ch11-11-api.txt:3);好 API 把更新数据的函数与只读取数据的函数清晰分开(:7-8)。
- 将查询函数和修改函数分离(306):只提供值、无副作用的函数可任意调用、易测,「需要操心的事情少多了」(
:44-46);命令与查询分离(CQS):任何有返回值的函数都不应有看得到的副作用;「我并不绝对遵守它,不过我总是尽量遵守」(:48-51);缓存是「察觉不到的修改」不算违反(:56-58);getTotalOutstandingAndSendBill 拆成 totalOutstanding+sendBill(:28-40);alertForMiscreant 恶棍名单例子(:78-85)。 - 函数参数化(310):两个函数功能相似只数值不同→统一。要点另见 ch08 记录。
- 移除标记参数(314):标记参数=调用者用它指示被调函数执行哪部分逻辑;布尔型最糟,「我很难弄清true到底是什么意思」(
:341-345);判定:只有传入字面量值、且参数值影响函数内部控制流,才是标记参数(:349-352);去掉后工具更能发挥作用(:354-355);多个标记参数→函数做得太多(:357-359);bookConcert(aCustomer, isPremium)→premiumBookConcert(:321-347);deliveryDate(anOrder, isRush) 例子,MA/CT 1 天、NY/NH 2 天(:371-389)。 - 保持对象完整(319):从记录导出几个值再一起传→改为传整个记录;应对变化+缩短参数列表(
:504-510);不该用:不想让被调函数依赖完整对象(跨模块)(:512-513);抽取几个值单独做逻辑=依恋情结坏味道(:515-517);this 作为参数传递(:522-523);温控 withinRange(low,high)→withinRange(aRoom.daysTempRange)(:496-553)。 - 以查询取代参数(324):参数列表应总结函数的可变性,标示行为差异的主要方式(
:725-726);「同样容易」四个字划出判断界限:责任在调用者还是函数本身(:732-735);常见风险:增加不必要的依赖(:737-741);最安全场景:参数值只需向另一参数查询(:743-744);引用透明性不许丢(:746-749);availableVacation(anEmployee, grade) 例子(:713-721);finalPrice/discountLevel 例子(:762-775)。 - 以参数取代查询(327):引用不快的引用(全局变量)→变参数,责任转交调用者(
:836-838);权衡:全变参数→列表冗长;共享太多→依赖过度(:841-844);引用透明性:同样参数永远同样结果;「纯函数模块+外层包裹 I/O」模式(:846-852);代价:调用者复杂度增加;「决策既不容易,也不会一劳永逸」(:854-857);thermostat 例子(:872-893)。 - 移除设值函数(331):提供设值函数暗示字段可被改变;不想让它被改就不要提供(
:987-990);两种情况:构造函数是设值函数唯一使用者;创建脚本(构造+一串 set)(:992-1000);不能替换为「创建新对象」(多处共享引用)就放弃(:1012-1013);Person id 例子(:1022-1042)。 - 以工厂函数取代构造函数(334):构造函数的丑陋局限:只能返回本类实例(无法返回子类/代理)、名字固定、要 new 操作符(
:1074-1078);工厂函数不受限制(:1080-1081);Employee/E/M/S 例子,createEmployee/createEngineer/createManager(:1092-1139)。 - 以命令取代函数(337):命令对象=只服务于单一函数的对象(
:1182-1185);好处:撤销、生命周期管理、继承/钩子定制、拆解复杂函数(:1187-1192);「95%的时候我都会选函数。只有当我特别需要命令对象提供的某种能力…才会考虑」(:1195-1197);「命令」一词三义(命令模式/命令查询分离)(:1199-1205);score 保险评分例子,Scorer.execute(:1221-1264)。 - 以函数取代命令(340):反向,命令太简单就退回函数。score 反例(
:1458+)。
第12章 处理继承关系(20-ch12.txt,2169 行)
章首:继承十分实用却经常被误用,「常得等你用上一段时间,遇见了痛点,才能察觉误用所在」(20-ch12.txt:3-5);手法分三组:上下调整(上移/下移)、增删类(提炼超类/移除子类/折叠)、类型码→子类、继承→委托(:7-15)。
- 函数上移(350):重复=「修改其中一个却未能修改另一个」的风险;观察重复函数间的差异「经常向我展示那些我忘记测试的行为」(
:37-44);先函数参数化再上移(:46-48);Party.annualCost 例子:两个子类不同名同体→统一名字→上移;monthlyCost 留子类→陷阱(trap)函数 throw SubclassResponsibilityError(:74-120)。 - 以子类取代类型码(362):类型码=枚举/符号/字符串/数字,取值常来自外部服务(
:400-403);继承两个诱人之处:多态处理条件逻辑;类型特有字段可下移到子类(:405-413);直接子类 vs 包装类型码类:类别可变就不能用直接继承(:415-420);createEmployee switch engineer/salesman/manager(:388-396)。 - 移除子类(369):「子类存在着就有成本,阅读者要花心思去理解它的用意」;用处太少就换成超类字段(
:697-698);Male/Female/X 例子(:677-731)。 - 提炼超类(375):「合理的继承关系是在程序演化的过程中才浮现出来的」(
:928-931);提炼超类 vs 提炼类=继承与委托的选择;首选提炼超类,「即便选错了,也总有以委托取代超类(399)这瓶后悔药可吃」(:933-935);Party 例子 name/annualCost(:896-921)。 - 以委托取代子类(381):继承短板 1:「继承这张牌只能打一次」——只能处理一个方向的变化(年龄段 vs 收入水平)(
:1158-1161);短板 2:超类与子类关系过紧,超类修改易破坏子类(:1163-1165);委托解决两者(:1167-1169);「对象组合优于类继承」:很多人解读为继承有害,「我会从继承开始,如果开始出现问题,再转而使用委托」;原则是对彼时继承被滥用的回应(:1171-1176);=用 State/Strategy 模式取代子类(:1177-1180);Booking→showDelegate 例子;Bird/SpeciesDelegate:EuropeanSwallowDelegate 35 / African 40-2×coconuts / NorwegianBlue (isNailed)?0:10+voltage/10(:1901-1938);「新的继承体系范围更收拢了」(:1940-1943);金句:「『对象组合优于类继承』更确切的表述可能是『审慎地组合使用对象组合与类继承,优于单独使用其中任何一种』——不过这就不太上口了」(:1949-1950)。 - 以委托取代超类(399):经典误用:stack 继承 list——列表操作都出现在栈接口上,大部分不适用(
:1970-1973);判据 1:超类一些函数对子类不适用→不该继承(:1975-1976);判据 2:子类所有实例都应是超类实例;车模/汽车「类型与实例名不符实」(:1978-1982);转发函数乏味但简单几乎不出错(:1989-1991);「首先(尽量)使用继承,如果发现继承有问题,再使用以委托取代超类」(:1992-1996);CatalogItem/Scroll 卷轴例子:灰鳞病卷轴好几卷但目录一个条目(:2011-2052)。
手法编号对照(ch06-12 各条目首行)
106 提炼函数/115 内联函数/119 提炼变量/123 内联变量/124 改变函数声明/132 封装变量/137 变量改名/140 引入参数对象/144 函数组合成类/149 函数组合成变换/154 拆分阶段/162 封装记录/170 封装集合/174 以对象取代基本类型/178 以查询取代临时变量/182 提炼类/186 内联类/189 隐藏委托关系/192 移除中间人/195 替换算法/198 搬移函数/207 搬移字段/213 搬移语句到函数/217 搬移语句到调用者/222 以函数调用取代内联代码/223 移动语句/227 拆分循环/231 以管道取代循环/237 移除死代码/240 拆分变量/244 字段改名/248 以查询取代派生变量/252 引用改值/256 值改引用/260 分解条件/263 合并条件/266 卫语句/272 多态/289 引入特例/302 引入断言/306 查询修改分离/310 函数参数化/314 移除标记参数/319 保持对象完整/324 以查询取代参数/327 以参数取代查询/331 移除设值函数/334 工厂函数/337 以命令取代函数/340 以函数取代命令/350 函数上移/353 字段上移/355 构造函数本体上移/359 函数下移/361 字段下移/362 以子类取代类型码/369 移除子类/375 提炼超类/380 折叠继承体系/381 以委托取代子类/399 以委托取代超类。 (编号是 Web 版编号,纸质版条目按章重排;引用时 以中文章名+编号呈现。)