auto commit
This commit is contained in:
parent
02d2c5793a
commit
bc28b775d8
12
README.md
12
README.md
|
@ -70,6 +70,18 @@
|
|||
|
||||
File、InputStream 和 OutputStream、Reader 和 Writer、Serializable、Socket 以及 NIO
|
||||
|
||||
# 编码实践
|
||||
|
||||
> [重构](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/重构.md)
|
||||
|
||||
> [编写可读代码的艺术](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/编写可读代码的艺术.md)
|
||||
|
||||
# 个人提升
|
||||
|
||||
> [黑客与画家](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/黑客与画家.md)
|
||||
|
||||
> [程序员的职业素养](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/程序员的职业素养.md)
|
||||
|
||||
# 资料下载
|
||||
|
||||
> [百度网盘](https://pan.baidu.com/s/1o9oD1s2#list/path=%2F)
|
||||
|
|
129
notes/程序员的职业素养.md
Normal file
129
notes/程序员的职业素养.md
Normal file
|
@ -0,0 +1,129 @@
|
|||
<!-- GFM-TOC -->
|
||||
* [译者序](#译者序)
|
||||
* [前言](#前言)
|
||||
* [专业主义](#专业主义)
|
||||
* [说不](#说不)
|
||||
* [说是](#说是)
|
||||
* [编程](#编程)
|
||||
* [测试驱动开发](#测试驱动开发)
|
||||
* [练习](#练习)
|
||||
<!-- GFM-TOC -->
|
||||
|
||||
# 译者序
|
||||
|
||||
作者 Bob 大叔,著有《敏捷软件开发:原则、模式与实践》《代码整洁之道》《程序员的职业素养》
|
||||
|
||||
对于解决问题,重要的不是问题本身,而是解决问题的方式、步骤以及反思的深度,这就是职业素养。
|
||||
|
||||
# 前言
|
||||
|
||||
由于管理者面临很大的财务和政治压力,他们无视技术人员发出的危险警告,抱着侥幸心理发射了“挑战者”航天飞机,最终导致航天飞机在高空爆炸。虽说技术人员已经做很多事情去阻止这次发射,但是并不是说做了所有他们能做的,例如他们就没有打电话去新闻台揭露此次发射的危险性。
|
||||
|
||||
![](https://github.com/CyC2018/InterviewNotes/blob/master/pics/bccb799f-56e2-4356-95f0-a9ea05b0de2a.jpg)
|
||||
|
||||
|
||||
# 专业主义
|
||||
|
||||
专业主义不仅意味着荣誉和骄傲,也意味着责任。
|
||||
|
||||
作者在自己开发的一个电话线路故障检测系统时,因为赶着在到期日交付新功能,没有对整个系统进行测试,导致了一个 bug 的出现。作者认为这是一种不负责任的表现,也就是不专业的表现。
|
||||
|
||||
代码中难免有 bug,但是不意味着你不用为 bug 负责。
|
||||
|
||||
有人把 QA 当成查 bug 的机器,等着 QA 发现 bug,而自己却不主动去发现 bug,这是不对的。
|
||||
|
||||
为了保证代码的正确性,需要写一些随时可以运行的单元测试,并且不断地去运行它们。
|
||||
|
||||
软件项目的根本原则 - 易于修改。为了让自己的软件易于修改,需要经常去修改它!
|
||||
|
||||
单元测试可以让程序员更有自信去修改代码,因为它会让人确信修改地是否正确。
|
||||
|
||||
专业素养包括以下内容:
|
||||
|
||||
1. 坚持学习
|
||||
2. 每天练习一些简单的编程题目
|
||||
3. 与他们合作,从彼此身上学到更多东西
|
||||
4. 交流。通过交流表达自己的思想,发现自己的不足以及与别人思想上的差异。
|
||||
5. 了解业务领域,找几本相关领域的书看。
|
||||
6. 与雇主 / 客户保持一致
|
||||
7. 谦逊,因为他们知道自己也可能犯错。
|
||||
|
||||
# 说不
|
||||
|
||||
对一些不合理的要求,不能只是说“试试看”,这会被当成是接受了这个要求。如果自己深知这种不合理的要求会导致严重的后果,就不能任由它发展下去,对这一要求说“不”。
|
||||
|
||||
说“不”时要用最详细的细节来说明,而不是烦躁地反驳。
|
||||
|
||||
当要求明显不合理时,可以协商一些双方都能接受的方案。
|
||||
|
||||
"有可能写出好代码吗" 这篇博文,讲述了一次开发一家零售商为了在"黑色星期五"能够让顾客看到商品信息商店信息以及发布优惠码等功能的 ipone 应用,最开始零售商的经理承诺说只要写个硬编码的应用就行了,但是客户所要的任何一项功能,总比它最开始时所说的要复杂许多。面对时间的紧急,客户方的不配合,以及需求的变更,让博文作者写了一堆很烂的代码,而他自己确实对那些设计模式等高级开发技术是非常热衷的。所以他认为在实际的开发中因为客户的需求等因素,很难写作自己想写的代码。Bob 认为,上面那篇博文的作者才是此次开发过程碰到的那些问题的负责人,因为他没有对那些不合理的要求说不。
|
||||
|
||||
# 说是
|
||||
|
||||
做出承诺的是三个步骤:口头上答应,放在心里,付诸行动。
|
||||
|
||||
缺乏承诺的词语:
|
||||
|
||||
1. 需要:我需要把这事做完;
|
||||
2. 希望:希望今天我能完成任务;
|
||||
3. 让我们:让我们把这件事做完。
|
||||
|
||||
缺乏承诺的人不会把重点放在自己身上,也不会明确说明一件事情的截止日期。真正的承诺是这样的“我会在...之前...”
|
||||
|
||||
对自己无法完全掌握的事,不要轻易承诺。
|
||||
|
||||
发现自己无法做到承诺,立马去调整别人对你的预期。
|
||||
|
||||
“试试看”说明自己对承诺的内容有点顾忌,因此不要用试试看来作为一种承诺。如果不能确信自己能达到承诺的内容而随便承诺,那么最后往往会产生严重的后果。
|
||||
|
||||
# 编程
|
||||
|
||||
要精通一项技术,要对该技术具备“信心”和“感知能力”。
|
||||
|
||||
状态不好的时候最好别写代码。状态不好分为:疲劳,比如熬夜;焦虑,因为外界的事情让自己不能安心下来,这个时候应该下调整好再进行编程。
|
||||
|
||||
进入流状态的感觉很好,觉得可以做很多事,但是流状态其实是一种浅层冥想,思维能力会下降,因此要避免进入流状态。
|
||||
|
||||
![](https://github.com/CyC2018/InterviewNotes/blob/master/pics/f0321ed1-fa93-460e-951b-4239fef819f3.jpg)
|
||||
|
||||
流状态的特点是隔绝沟通,而结对编程能够打破这种隔绝,因此结对编程能够防止进入流状态。
|
||||
|
||||
当自己没法继续编程时,结对编程是个好选择,结对编程时,大脑和身体会有化学变化,也就是说思维方式会改变,有助于突破当前的思维阻塞状态。
|
||||
|
||||
听音乐时音乐会占用一部分脑力资源,因此会导致效率下降,但是这不是绝对。
|
||||
|
||||
多输入一些创造性的内容,比如科幻小说等,自己的创造力也会得到提升。
|
||||
|
||||
调试时间也很宝贵,测试驱动开发能够降低调试的时间。
|
||||
|
||||
编码和跑马拉松一样,无法全程以最快的速度冲刺,只能通过保持体力和维持节奏来取得胜利。因此要保持好自己的节奏,在疲劳的时候适当休息。
|
||||
|
||||
定期做进度衡量,用乐观预估、标准预估和悲观预估这三个时间点。
|
||||
|
||||
如果自己预估的进度赶不上截止时间,不要答应在截止时间去完成,因为你知道根据自己的预估这是不可能的。
|
||||
|
||||
任务完成的标准通常用一个自动化的验收测试来定义。
|
||||
|
||||
帮助他人也会给自己带来好处,在帮助别人的时候不要让自己看起来很仓促,就像是在随便应付。一般帮助别人不需要特别多的时间。也要学会请求别人的帮助,因为自己不可能是万能的,通过别人的帮助能更好的解决问题。
|
||||
|
||||
# 测试驱动开发
|
||||
|
||||
TDD 的运行周期很短,可能一两分钟就可以运行一次程序,短周期可以快速得到反馈,反馈在开发中特别重要,一方面是提高程序员编码的激情,另一方面是能够及早发现 bug,而 bug 越早发现修改地代价也越低。
|
||||
|
||||
TDD 三法则:
|
||||
|
||||
1. 先写测试;
|
||||
2. 在一个单元测试失败的时候,不要再写测试代码;
|
||||
3. 产品代码能恰好通过当前失败的单元测试即可,不要多写。
|
||||
|
||||
TDD 优点
|
||||
|
||||
1. 确定性:确定当前系统是否正确;
|
||||
2. 信心:程序员更有信息去修改混乱的代码,因为通过单元测试可以知道是否修改地正确;
|
||||
3. 文档:单元测试是底层设计细节的文档,通过阅读单元测试能够知道程序的运行方式;
|
||||
|
||||
# 练习
|
||||
|
||||
卡塔在武术里面是一套设计好的招式,该招式模拟真实搏斗场景,习武者不断训练来让身体熟悉该招式,从而做到纯熟。编程卡塔是一套编程过程敲击鼠标和键盘的动作,练习者不是为了解决真正的问题,因为已经知道了解决方案,而是练习解决这个问题所需要的动作和决策。
|
||||
|
||||
瓦萨即使两人对练的卡塔。在编程领域,可以是一个人写单元测试,另一个人写程序通过单元测试。比如,一个人实现排序算法,写测试的人可以很容易地限制速度和内存,给同伴施压。
|
342
notes/编写可读代码的艺术.md
Normal file
342
notes/编写可读代码的艺术.md
Normal file
|
@ -0,0 +1,342 @@
|
|||
<!-- GFM-TOC -->
|
||||
* [第 1 章 可读性的重要性](#第-1-章-可读性的重要性)
|
||||
* [第 2 章 用名字表达代码含义](#第-2-章-用名字表达代码含义)
|
||||
* [第 3 章 名字不能带来歧义](#第-3-章-名字不能带来歧义)
|
||||
* [第 4 章 良好的代码风格](#第-4-章-良好的代码风格)
|
||||
* [第 5 章 编写注释](#第-5-章-编写注释)
|
||||
* [第 6 章 如何编写注释](#第-6-章-如何编写注释)
|
||||
* [第 7 章 提高控制流的可读性](#第-7-章-提高控制流的可读性)
|
||||
* [第 8 章 拆分长表达式](#第-8-章-拆分长表达式)
|
||||
* [第 9 章 变量与可读性](#第-9-章-变量与可读性)
|
||||
* [第 10 章 抽取函数](#第-10-章-抽取函数)
|
||||
* [第 11 章 一次只做一件事](#第-11-章-一次只做一件事)
|
||||
* [第 12 章 用自然语言表述代码](#第-12-章-用自然语言表述代码)
|
||||
* [第 13 章 减少代码量](#第-13-章-减少代码量)
|
||||
<!-- GFM-TOC -->
|
||||
|
||||
# 第 1 章 可读性的重要性
|
||||
|
||||
编程有很大一部分时间是在阅读代码,不仅要阅读自己的代码,而且要阅读别人的代码。因此,可读性良好的代码能够大大提高编程效率。
|
||||
|
||||
可读性良好的代码往往会让代码架构更好,因为程序员更愿意去修改这部分代码,而且也更容易修改。
|
||||
|
||||
只有在核心领域为了效率才可以放弃可读性,否则可读性是第一位。
|
||||
|
||||
# 第 2 章 用名字表达代码含义
|
||||
|
||||
一些比较有表达力的单词:
|
||||
|
||||
| 单词 | 可替代单词 |
|
||||
| --- | --- |
|
||||
| send | deliver、dispatch、announce、distribute、route |
|
||||
| find | search、extract、locate、recover |
|
||||
| start| launch、create、begin、open|
|
||||
|make|create、set up、build、generate、compose、add、new|
|
||||
|
||||
使用 i、j、k 作为循环迭代器不总是好的,为迭代器添加更有表达力的名字会更好,比如 user_i、member_i。因为循环层次越多,代码越难理解,有表达力的迭代器名字可读性会更高
|
||||
|
||||
为名字添加形容词等信息能让名字更具有表达力,但是名字也会变长。名字长短的准则是:作用域越大,名字越长。因此只有在短作用域才能使用一些简单名字。
|
||||
|
||||
# 第 3 章 名字不能带来歧义
|
||||
|
||||
起完名字要思考一下别人会对这个名字有何解读,会不会误解了原本想表达的含义。
|
||||
|
||||
用 min、max 表示数量范围;
|
||||
|
||||
用 first、last 表示访问空间的包含范围,begin、end 表示访问空间的排除范围,即 end 不包含尾部。
|
||||
|
||||
![](https://github.com/CyC2018/InterviewNotes/blob/master/pics/26772ecc-a3e3-4ab7-a46f-8b4656990c27.jpg)
|
||||
|
||||
布尔相关的命名加上 is、can、should、has 等前缀。
|
||||
|
||||
# 第 4 章 良好的代码风格
|
||||
|
||||
适当的空行和缩进
|
||||
|
||||
排列整齐的注释:
|
||||
|
||||
```
|
||||
int a = 1; // 注释
|
||||
int b = 11; // 注释
|
||||
int c = 111; // 注释
|
||||
```
|
||||
|
||||
语句顺序不能随意,比如与 html 表单相关联的变量的赋值应该和表单在 html 中的顺序一致;
|
||||
|
||||
把相关的代码按块组织放在一起。
|
||||
|
||||
# 第 5 章 编写注释
|
||||
|
||||
阅读代码首先会注意到注释,如果注释没太大作用,那么就会浪费代码阅读的时间。那些能直接看出含义的代码不需要写注释,特别是并不需要为每个方法都加上注释,比如那些简单的 getter 和 setter 方法,为这些方法写注释反而让代码可读性更差。
|
||||
|
||||
不能因为有注释就随便起个名字,而是争取起个好名字而不写注释。
|
||||
|
||||
可以用注释来记录采用当前解决办法的思考过程,从而让读者更容易理解代码。
|
||||
|
||||
注释用来提醒一些特殊情况。
|
||||
|
||||
用 TODO 等做标记:
|
||||
|
||||
| 标记 | 用法 |
|
||||
|---|---|
|
||||
|TODO| 待做 |
|
||||
|FIXME| 待修复 |
|
||||
|HACH| 粗糙的解决方案 |
|
||||
|XXX| 危险!这里有重要的问题 |
|
||||
|
||||
# 第 6 章 如何编写注释
|
||||
|
||||
尽量简洁明了:
|
||||
|
||||
```
|
||||
// The first String is student's name
|
||||
// The Second Integer is student's score
|
||||
Map<String, Integer> scoreMap = new HashMap<>();
|
||||
```
|
||||
|
||||
```
|
||||
// Student' name -> Student's score
|
||||
Map<String, Integer> scoreMap = new HashMap<>();
|
||||
```
|
||||
|
||||
添加测试用例来说明:
|
||||
|
||||
```
|
||||
//...
|
||||
// Example: add(1, 2), return 3
|
||||
int add(int x, int y) {
|
||||
return x + y;
|
||||
}
|
||||
```
|
||||
|
||||
在很复杂的函数调用中对每个参数标上名字:
|
||||
|
||||
```
|
||||
int a = 1;
|
||||
int b = 2;
|
||||
int num = add(\* x = *\ a, \* y = *\ b);
|
||||
```
|
||||
|
||||
使用专业名词来缩短概念上的解释,比如用设计模式名来说明代码。
|
||||
|
||||
# 第 7 章 提高控制流的可读性
|
||||
|
||||
条件表达式中,左侧是变量,右侧是常数。比如下面第一个语句正确:
|
||||
|
||||
```
|
||||
if(len < 10)
|
||||
if(10 > len)
|
||||
```
|
||||
|
||||
if / else 条件语句,先处理以下逻辑:① 正逻辑;② 关键逻辑;③:简单逻辑
|
||||
```
|
||||
if(a == b) {
|
||||
// 正逻辑
|
||||
} else{
|
||||
// 反逻辑
|
||||
}
|
||||
```
|
||||
|
||||
只有在逻辑简单的情况下使用 ? : 三目运算符来使代码更紧凑,否则应该拆分成 if / else;
|
||||
|
||||
do / while 的条件放在后面,不够简单明了,并且会有一些迷惑的地方,最好使用 while 来代替。
|
||||
|
||||
如果只有一个 goto 目标,那么 goto 尚且还能接受,但是过于复杂的 goto 会让代码可读性特别差,应该避免使用 goto。
|
||||
|
||||
在嵌套的循环中,用一些 return 语句往往能减少嵌套的层数。
|
||||
|
||||
# 第 8 章 拆分长表达式
|
||||
|
||||
长表达式的可读性很差,可以引入一些解释变量从而拆分表达式:
|
||||
|
||||
```
|
||||
if line.split(':')[0].strip() == "root":
|
||||
...
|
||||
```
|
||||
```
|
||||
username = line.split(':')[0].strip()
|
||||
if username == "root":
|
||||
...
|
||||
```
|
||||
|
||||
使用摩根定理简化一些逻辑表达式:
|
||||
|
||||
```
|
||||
if(!a && !b) {
|
||||
...
|
||||
}
|
||||
```
|
||||
```
|
||||
if(a || b) {
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
# 第 9 章 变量与可读性
|
||||
|
||||
去除控制流变量。在循环中通过 break 或者 return 可以减少控制流变量的使用。
|
||||
|
||||
```
|
||||
boolean done = false;
|
||||
while(/* condition */ && !done) {
|
||||
...
|
||||
if(...) {
|
||||
done = true;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
```
|
||||
```
|
||||
while(/* condition */) {
|
||||
...
|
||||
if(...) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
减小变量作用域。作用域越小,越容易定位到变量所有使用的地方。
|
||||
|
||||
JavaScript 可以用闭包减小作用域。以下代码中 submit_form 是函数变量,submitted 变量控制函数不会被提交两次。第一个实现中 submitted 是全局变量,第二个实现把 submitted 放到匿名函数中,从而限制了起作用域范围。
|
||||
|
||||
```
|
||||
submitted = false;
|
||||
var submit_form = function(form_name) {
|
||||
if(submitted) {
|
||||
return;
|
||||
}
|
||||
submitted = true;
|
||||
};
|
||||
```
|
||||
|
||||
```
|
||||
var submit_form = (function() {
|
||||
var submitted = false;
|
||||
return function(form_name) {
|
||||
if(submitted) {
|
||||
return;
|
||||
}
|
||||
submitted = true;
|
||||
}
|
||||
}()); // () 使得外层匿名函数立即执行
|
||||
```
|
||||
|
||||
JavaScript 中没有用 var 声明的变量都是全局变量,而全局变量很容易造成迷惑,因此应当总是用 var 来声明变量。
|
||||
|
||||
变量定义的位置应当离它使用的位置最近。
|
||||
|
||||
**实例解析**
|
||||
|
||||
在一个网页中有以下文本输入字段:
|
||||
|
||||
```
|
||||
<input type = "text" id = "input1" value = "a">
|
||||
<input type = "text" id = "input2" value = "b">
|
||||
<input type = "text" id = "input3" value = "">
|
||||
<input type = "text" id = "input4" value = "d">
|
||||
```
|
||||
|
||||
现在要接受一个字符串并把它放到第一个空的 input 字段中,初始实现如下:
|
||||
|
||||
```
|
||||
var setFirstEmptyInput = function(new_alue) {
|
||||
var found = false;
|
||||
var i = 1;
|
||||
var elem = document.getElementById('input' + i);
|
||||
while(elem != null) {
|
||||
if(elem.value === '') {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
i++;
|
||||
elem = document.getElementById('input' + i);
|
||||
}
|
||||
if(found) elem.value = new_value;
|
||||
return elem;
|
||||
}
|
||||
```
|
||||
|
||||
以上实现有以下问题:
|
||||
|
||||
- found 可以去除;
|
||||
- elem 作用域过大;
|
||||
- 可以用 for 循环代替 while 循环;
|
||||
|
||||
```
|
||||
var setFirstEmptyInput = function(new_value) {
|
||||
for(var i = 1; true; i++) {
|
||||
var elem = document.getElementById('input' + i);
|
||||
if(elem === null) {
|
||||
return null;
|
||||
}
|
||||
if(elem.value === '') {
|
||||
elem.value = new_value;
|
||||
return elem;
|
||||
}
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
# 第 10 章 抽取函数
|
||||
|
||||
工程学就是把大问题拆分成小问题再把这些问题的解决方案放回一起。
|
||||
|
||||
首先应该明确一个函数的高层次目标,然后对于不是直接为了这个目标工作的代码,抽取出来放到独立的函数中。
|
||||
|
||||
介绍性的代码:
|
||||
|
||||
```
|
||||
int findClostElement(int[] arr) {
|
||||
int clostIdx;
|
||||
int clostDist = Interger.MAX_VALUE;
|
||||
for(int i = 0; i < arr.length; i++) {
|
||||
int x = ...;
|
||||
int y = ...;
|
||||
int z = ...;
|
||||
int value = x * y * z;
|
||||
int dist = Math.sqrt(Math.pow(value, 2), Math.pow(arr[i], 2));
|
||||
if(dist < clostDist) {
|
||||
clostIdx = i;
|
||||
clostDist = value;
|
||||
}
|
||||
}
|
||||
return clostIdx;
|
||||
}
|
||||
```
|
||||
|
||||
以上代码中循环部分主要计算距离,这部分不属于代码高层次目标,高层次目标是寻找最小距离的值,因此可以把这部分代替提取到独立的函数中。这样做也带来一个额外的好处有:可以单独进行测试、可以快速找到程序错误并修改。
|
||||
|
||||
```
|
||||
public int findClostElement(int[] arr) {
|
||||
int clostIdx;
|
||||
int clostDist = Interger.MAX_VALUE;
|
||||
for(int i = 0; i < arr.length; i++) {
|
||||
int dist = computDist(arr, i);
|
||||
if(dist < clostDist) {
|
||||
clostIdx = i;
|
||||
clostDist = value;
|
||||
}
|
||||
}
|
||||
return clostIdx;
|
||||
}
|
||||
```
|
||||
|
||||
并不是函数抽取的越多越好,如果抽取过多,在阅读代码的时候可能需要不断跳来跳去。只有在当前函数不需要去了解某一块代码细节而能够表达其内容时,把这块代码抽取成子函数才是好的。
|
||||
|
||||
函数抽取也用于减小代码的冗余。
|
||||
|
||||
# 第 11 章 一次只做一件事
|
||||
|
||||
只做一件事的代码很容易让人知道其要做的事;
|
||||
|
||||
基本流程:列出代码所做的所有任务;把每个任务拆分到不同的函数,或者不同的段落。
|
||||
|
||||
# 第 12 章 用自然语言表述代码
|
||||
|
||||
先用自然语言书写代码逻辑,也就是伪代码,然后再写代码,这样代码逻辑会更清晰。
|
||||
|
||||
# 第 13 章 减少代码量
|
||||
|
||||
不要过度设计,编码过程会有很多变化,过度设计的内容到最后往往是无用的。
|
||||
|
||||
多用标准库实现。
|
979
notes/重构.md
Normal file
979
notes/重构.md
Normal file
|
@ -0,0 +1,979 @@
|
|||
<!-- GFM-TOC -->
|
||||
* [第一章 重构,第一个案例](#第一章-重构,第一个案例)
|
||||
* [第二章 重构原则](#第二章-重构原则)
|
||||
* [第三章 代码的坏味道](#第三章-代码的坏味道)
|
||||
* [1. Duplicated Code(重复代码)](#1-duplicated-code(重复代码))
|
||||
* [2. Long Method(过长函数)](#2-long-method(过长函数))
|
||||
* [3. Large Class(过大的类)](#3-large-class(过大的类))
|
||||
* [4. Long Parameter List(过长的参数列)](#4-long-parameter-list(过长的参数列))
|
||||
* [5. Divergent Change(发散式变化)](#5-divergent-change(发散式变化))
|
||||
* [6. Shotgun Surgery(散弹式修改)](#6-shotgun-surgery(散弹式修改))
|
||||
* [7. Feature Envy(依恋情结)](#7-feature-envy(依恋情结))
|
||||
* [8. Data Clumps(数据泥团)](#8-data-clumps(数据泥团))
|
||||
* [9. Primitive Obsession(基本类型偏执)](#9-primitive-obsession(基本类型偏执))
|
||||
* [10. Switch Statements(switch 惊悚现身)](#10-switch-statements(switch-惊悚现身))
|
||||
* [11. Parallel Inheritance Hierarchies(平行继承体系)](#11-parallel-inheritance-hierarchies(平行继承体系))
|
||||
* [12. Lazy Class(冗余类)](#12-lazy-class(冗余类))
|
||||
* [13. Speculative Generality(夸夸其谈未来性)](#13-speculative-generality(夸夸其谈未来性))
|
||||
* [14. Temporary Field(令人迷惑的暂时字段)](#14-temporary-field(令人迷惑的暂时字段))
|
||||
* [15. Message Chains(过度耦合的消息链)](#15-message-chains(过度耦合的消息链))
|
||||
* [16. Middle Man(中间人)](#16-middle-man(中间人))
|
||||
* [17. Inappropriate Intimacy(狎昵关系)](#17-inappropriate-intimacy(狎昵关系))
|
||||
* [18. Alernative Classes with Different Interfaces(异曲同工的类)](#18-alernative-classes-with-different-interfaces(异曲同工的类))
|
||||
* [19. Incomplete Library Class(不完美的类库)](#19-incomplete-library-class(不完美的类库))
|
||||
* [20. Data Class(幼稚的数据类)](#20-data-class(幼稚的数据类))
|
||||
* [21. Refused Bequest(被拒绝的馈赠)](#21-refused-bequest(被拒绝的馈赠))
|
||||
* [22. Comments(过多的注释)](#22-comments(过多的注释))
|
||||
* [第四章 构筑测试体系](#第四章-构筑测试体系)
|
||||
* [第五章 重构列表](#第五章-重构列表)
|
||||
* [第六章 重新组织函数](#第六章-重新组织函数)
|
||||
* [1. Extract Method(提炼函数)](#1-extract-method(提炼函数))
|
||||
* [2. Inline Method(内联函数)](#2-inline-method(内联函数))
|
||||
* [3. Inline Temp(内联临时变量)](#3-inline-temp(内联临时变量))
|
||||
* [4. Replace Temp with Query(以查询取代临时变量)](#4-replace-temp-with-query(以查询取代临时变量))
|
||||
* [5. Introduce Explaining Variable(引起解释变量)](#5-introduce-explaining-variable(引起解释变量))
|
||||
* [6. Split Temporary Variable(分解临时变量)](#6-split-temporary-variable(分解临时变量))
|
||||
* [7. Remove Assigments to Parameters(移除对参数的赋值)](#7-remove-assigments-to-parameters(移除对参数的赋值))
|
||||
* [8. Replace Method with Method Object(以函数对象取代函数)](#8-replace-method-with-method-object(以函数对象取代函数))
|
||||
* [9. Subsititute Algorithn(替换算法)](#9-subsititute-algorithn(替换算法))
|
||||
* [第七章 在对象之间搬移特性](#第七章-在对象之间搬移特性)
|
||||
* [1. Move Method(搬移函数)](#1-move-method(搬移函数))
|
||||
* [2. Move Field(搬移字段)](#2-move-field(搬移字段))
|
||||
* [3. Extract Class(提炼类)](#3-extract-class(提炼类))
|
||||
* [4. Inline Class(将类内联化)](#4-inline-class(将类内联化))
|
||||
* [5. Hide Delegate(隐藏“委托关系”)](#5-hide-delegate(隐藏“委托关系”))
|
||||
* [6. Remove Middle Man(移除中间人)](#6-remove-middle-man(移除中间人))
|
||||
* [7. Introduce Foreign Method(引入外加函数)](#7-introduce-foreign-method(引入外加函数))
|
||||
* [8. Introduce Local Extension(引入本地扩展)](#8-introduce-local-extension(引入本地扩展))
|
||||
* [第八章 重新组织数据](#第八章-重新组织数据)
|
||||
* [1. Self Encapsulate Field(自封装字段)](#1-self-encapsulate-field(自封装字段))
|
||||
* [2. Replace Data Value with Object(以对象取代数据值)](#2-replace-data-value-with-object(以对象取代数据值))
|
||||
* [3. Change Value to Reference(将值对象改成引用对象)](#3-change-value-to-reference(将值对象改成引用对象))
|
||||
* [4. Change Reference to value(将引用对象改为值对象)](#4-change-reference-to-value(将引用对象改为值对象))
|
||||
* [5. Replace Array with Object(以对象取代数组)](#5-replace-array-with-object(以对象取代数组))
|
||||
* [6. Duplicate Observed Data(赋值“被监视数据”)](#6-duplicate-observed-data(赋值“被监视数据”))
|
||||
* [7. Change Unidirectional Association to Bidirectional(将单向关联改为双向关联)](#7-change-unidirectional-association-to-bidirectional(将单向关联改为双向关联))
|
||||
* [8. Change Bidirectional Association to Unidirectional(将双向关联改为单向关联)](#8-change-bidirectional-association-to-unidirectional(将双向关联改为单向关联))
|
||||
* [9. Replace Magic Number with Symbolic Constant(以字面常量取代魔法数)](#9-replace-magic-number-with-symbolic-constant(以字面常量取代魔法数))
|
||||
* [10. Encapsulate Field(封装字段)](#10-encapsulate-field(封装字段))
|
||||
* [11. Encapsulate Collection(封装集合)](#11-encapsulate-collection(封装集合))
|
||||
* [12. Replace Record with Data Class(以数据类取代记录)](#12-replace-record-with-data-class(以数据类取代记录))
|
||||
* [13. Replace Type Code with Class(以类取代类型码)](#13-replace-type-code-with-class(以类取代类型码))
|
||||
* [14. Replace Type Code with Subcalsses(以子类取代类型码)](#14-replace-type-code-with-subcalsses(以子类取代类型码))
|
||||
* [15. Replace Type Code with State/Strategy (以 State/Strategy 取代类型码)](#15-replace-type-code-with-statestrategy-(以-statestrategy-取代类型码))
|
||||
* [16. Replace Subclass with Fields(以字段取代子类)](#16-replace-subclass-with-fields(以字段取代子类))
|
||||
* [第九章 简化条件表达式](#第九章-简化条件表达式)
|
||||
* [1. Decompose Conditional(分解条件表达式)](#1-decompose-conditional(分解条件表达式))
|
||||
* [2. Consolidate Conditional Expression(合并条件表达式)](#2-consolidate-conditional-expression(合并条件表达式))
|
||||
* [3. Consolidate Duplicate Conditional Fragments (合并重复的条件片段)](#3-consolidate-duplicate-conditional-fragments-(合并重复的条件片段))
|
||||
* [4. Remove Control Flag(移除控制标记)](#4-remove-control-flag(移除控制标记))
|
||||
* [5. Replace Nested Conditional with Guard Clauses (以卫语句取代嵌套条件表达式)](#5-replace-nested-conditional-with-guard-clauses-(以卫语句取代嵌套条件表达式))
|
||||
* [6. Replace Conditional with Polymorphism (以多态取代条件表达式)](#6-replace-conditional-with-polymorphism-(以多态取代条件表达式))
|
||||
* [7. Introduce Null Object(引入Null对象)](#7-introduce-null-object(引入null对象))
|
||||
* [8. Introduce Assertion(引入断言)](#8-introduce-assertion(引入断言))
|
||||
* [第十章 简化函数调用](#第十章-简化函数调用)
|
||||
* [1. Rename Method(函数改名)](#1-rename-method(函数改名))
|
||||
* [2. Add Parameter(添加参数)](#2-add-parameter(添加参数))
|
||||
* [3. Remove Parameter(移除参数)](#3-remove-parameter(移除参数))
|
||||
* [4. Separate Query from Modifier(将查询函数和修改函数分离)](#4-separate-query-from-modifier(将查询函数和修改函数分离))
|
||||
* [5. Parameterize Method(令函数携带参数)](#5-parameterize-method(令函数携带参数))
|
||||
* [6. Replace Parameter with Explicit Methods(以明确函数取代参数)](#6-replace-parameter-with-explicit-methods(以明确函数取代参数))
|
||||
* [7. Preserve Whole Object(保持对象完整)](#7-preserve-whole-object(保持对象完整))
|
||||
* [8. Replace Parameter with Methods(以函数取代参数)](#8-replace-parameter-with-methods(以函数取代参数))
|
||||
* [9. Introduce Parameter Object(引入参数对象)](#9-introduce-parameter-object(引入参数对象))
|
||||
* [10. Remove Setting Method(移除设值函数)](#10-remove-setting-method(移除设值函数))
|
||||
* [11. Hide Method(隐藏函数)](#11-hide-method(隐藏函数))
|
||||
* [12. Replace Constructor with Factory Method (以工厂函数取代构造函数)](#12-replace-constructor-with-factory-method-(以工厂函数取代构造函数))
|
||||
* [13. Encapsulate Downcast(封装向下转型)](#13-encapsulate-downcast(封装向下转型))
|
||||
* [14. Replace Error Code with Exception (以异常取代错误码)](#14-replace-error-code-with-exception-(以异常取代错误码))
|
||||
* [15. Replace Exception with Test(以测试取代异常)](#15-replace-exception-with-test(以测试取代异常))
|
||||
* [第十一章 处理概括关系](#第十一章-处理概括关系)
|
||||
* [1. Pull Up Field(字段上移)](#1-pull-up-field(字段上移))
|
||||
* [2. Pull Up Method(函数上移)](#2-pull-up-method(函数上移))
|
||||
* [3. Pull Up Constructor Body(构造函数本体上移)](#3-pull-up-constructor-body(构造函数本体上移))
|
||||
* [4. Push Down Method(函数下移)](#4-push-down-method(函数下移))
|
||||
* [5. Push Down Field(字段下移)](#5-push-down-field(字段下移))
|
||||
* [6. Extract Subclass(提炼子类)](#6-extract-subclass(提炼子类))
|
||||
* [7. Extract Superclass(提炼超类)](#7-extract-superclass(提炼超类))
|
||||
* [8. Extract Interface(提炼接口)](#8-extract-interface(提炼接口))
|
||||
* [9. Collapse Hierarchy(折叠继承体系)](#9-collapse-hierarchy(折叠继承体系))
|
||||
* [10. Form Template Method(塑造模板函数)](#10-form-template-method(塑造模板函数))
|
||||
* [11. Replace Inheritance with Delegation (以委托取代继承)](#11-replace-inheritance-with-delegation-(以委托取代继承))
|
||||
* [12. Replace Delegation with Inheritance (以继承取代委托)](#12-replace-delegation-with-inheritance-(以继承取代委托))
|
||||
<!-- GFM-TOC -->
|
||||
|
||||
# 第一章 重构,第一个案例
|
||||
|
||||
如果你发现自己需要为程序添加一个特性,而代码结构使你无法很方便地达成目的,那就先重构这个程序。
|
||||
|
||||
在重构前,需要先构建好可靠的测试环境,确保安全地重构。
|
||||
|
||||
重构是以微小的步伐修改程序,如果犯下错误,很容易便可以发现它。
|
||||
|
||||
**案例分析**
|
||||
|
||||
影片出租店应用程序,包括三个类:Movie、Rental 和 Customer,Rental 包含租赁的 Movie 以及天数。
|
||||
|
||||
![](https://github.com/CyC2018/InterviewNotes/blob/master/pics/a758c8b2-0ac7-438f-90c2-3923ffad6328.png)
|
||||
|
||||
最开始的实现是把所有的计费代码都放在 Customer 类中,在变化发生时,需要对这部分代码进行更改。本案例中可能发生的变化有:一种类别的计费方式发生改变;添加新的电影类别。考虑到计费代码可能存在于多处,一旦发生改变时,就需要对所有计费代码进行修改。
|
||||
|
||||
![](https://github.com/CyC2018/InterviewNotes/blob/master/pics/9e5e3cc6-3107-4051-b584-8ff077638fe6.png)
|
||||
|
||||
以下是继承 Movie 的多态方案。但是由于一部 Movie 的类别会动态改变,因此这种方案不可行。
|
||||
|
||||
![](https://github.com/CyC2018/InterviewNotes/blob/master/pics/2a502516-5d34-4eef-8f39-916298a60035.png)
|
||||
|
||||
引入 Price 来反应类别信息,通过组合的方式在 Movie 中加入 Price 对象,这样每种类别的计费方式都封装在不同的 Price 子类中,并且 Movie 对象也可以动态改变类别。这种方式可以很好地适应上述提到的变化。
|
||||
|
||||
![](https://github.com/CyC2018/InterviewNotes/blob/master/pics/c02a83b8-a6b9-4d00-a509-6f0516beaf5e.png)
|
||||
|
||||
重构后的时序图和类图:
|
||||
|
||||
![](https://github.com/CyC2018/InterviewNotes/blob/master/pics/95f4559c-3d2a-4176-b365-4fbc46c76cf1.png)
|
||||
|
||||
![](https://github.com/CyC2018/InterviewNotes/blob/master/pics/293b9326-02fc-4ad8-8c79-b4a7b5ba60d3.png)
|
||||
|
||||
# 第二章 重构原则
|
||||
|
||||
重构是对软件内部结构的一种调整,目的是在不改变软件可观察行为的前提下,提高其可理解性,降低其修改成本。
|
||||
|
||||
重构的好处:改进软件设计;使软件更容易理解;帮助找到 bug;提高编程速度。
|
||||
|
||||
三次法则:第一次做某件事时只管去做;第二次做类似事情时可以去做;第三次再做类似的事,就应该重构。
|
||||
|
||||
间接层与重构:计算机科学中的很多问题可以通过增加一个间接层来解决,间接层具有以下价值:允许逻辑共享;分开解释意图和实现;隔离变化;封装条件逻辑。重构可以理解为在适当的位置插入间接层以及在不需要时移除间接层。
|
||||
|
||||
修改接口:可以保留旧接口,让旧接口去调用新接口,并且使用 Java 提供的 @deprecation 将旧接口标记为弃用。除非真有必要,不要发布接口,并且不要过早发布接口。
|
||||
|
||||
当现有代码过于混乱时,应当重写而不是重构。一个折中的办法是,将代码封装成一个个组件,然后对各个组件做重写或者重构的决定。
|
||||
|
||||
软件开发无法预先设计,因为开发过程有很多变化发生,在最开始不可能都把所有情况考虑进去。重构可以简化设计,重构在一个简单的设计上进行修修改改,当变化发生时,以一种灵活的方式去应对变化,进而带来更好的设计。
|
||||
|
||||
为了软代码更容易理解,重构可能会导致性能减低。在编写代码时,不用对性能过多关注,只有在最后性能优化阶段再考虑性能问题。应当只关注关键代码的性能,因为只有一小部分的代码是关键代码。
|
||||
|
||||
# 第三章 代码的坏味道
|
||||
|
||||
## 1. Duplicated Code(重复代码)
|
||||
|
||||
同一个类的两个函数有相同表达式,则用 Extract Method 提取出重复代码;
|
||||
|
||||
两个互为兄弟的子类含有相同的表达式,先使用 Extract Method,然后把提取出来的函数 Pull Up Method 推入超类。
|
||||
|
||||
如果只是部分相同,用 Extract Method 分离出相似部分和差异部分,然后使用 Form Template Method 这种模板方法设计模式。
|
||||
|
||||
如果两个毫不相关的类出现重复代码,则使用 Extract Class 方法将重复代码提取到一个独立类中。
|
||||
|
||||
## 2. Long Method(过长函数)
|
||||
|
||||
间接层的价值:解释能力、共享能力、选择能力;
|
||||
|
||||
分解函数的原则:当需要用注释来说明一段代码时,就需要把这部分代码写入一个独立的函数中。
|
||||
|
||||
Extract Method 会把很多参数和临时变量都当做参数,可以用 Replace Temp with Query 消除临时变量,Introduce Parameter Object 和 Preserve Whole Object 可以将过长的参数列变得更简洁。
|
||||
|
||||
条件和循环往往也需要提取到新的函数中。
|
||||
|
||||
## 3. Large Class(过大的类)
|
||||
|
||||
过大的类做了过多事情,需要使用 Extract Class 或 Extract Subclass。
|
||||
|
||||
先确定客户端如何使用它们,然后运用 Extract Interface 为每一种使用方式提取出一个接口。
|
||||
|
||||
## 4. Long Parameter List(过长的参数列)
|
||||
|
||||
## 5. Divergent Change(发散式变化)
|
||||
|
||||
一个类受到多种变化的影响;
|
||||
|
||||
针对某种原因的变化,使用 Extract Class 将它提炼到一个类中。
|
||||
|
||||
## 6. Shotgun Surgery(散弹式修改)
|
||||
|
||||
一个变化引起多个类修改;
|
||||
|
||||
使用 Move Method 和 Move Field 把所有需要修改地代码放到同一个类中。
|
||||
|
||||
## 7. Feature Envy(依恋情结)
|
||||
|
||||
一个函数对某个类的兴趣高于对自己所处类的兴趣,通常是过多访问其它类的数据。
|
||||
|
||||
使用 Move Method 将它移到该去的地方,如果对多个类都有 Feature Envy,先用 Extract Method 提取出多个函数。
|
||||
|
||||
## 8. Data Clumps(数据泥团)
|
||||
|
||||
有些数据经常一起出现,比如两个类具有相同的字段、许多函数有相同的参数。使用 Extract Class 将它们放在一起。
|
||||
|
||||
## 9. Primitive Obsession(基本类型偏执)
|
||||
|
||||
使用类往往比使用基本类型更好,使用 Replace Data Value with Object 将数据值替换为对象。
|
||||
|
||||
## 10. Switch Statements(switch 惊悚现身)
|
||||
|
||||
## 11. Parallel Inheritance Hierarchies(平行继承体系)
|
||||
|
||||
每当为某个类增加一个子类,必须也为另一个类相应增加一个子类。
|
||||
|
||||
这种结果会带来一些重复性,消除重复性的一般策略:让一个继承体系的实例引用另一个继承体系的实例。
|
||||
|
||||
## 12. Lazy Class(冗余类)
|
||||
|
||||
如果一个类没有做足够多的工作,就应该消失。
|
||||
|
||||
## 13. Speculative Generality(夸夸其谈未来性)
|
||||
|
||||
有些内容是用来处理未来可能发生的变化,但是往往会造成系统难以理解和维护,并且预测未来可能发生的改变很可能和最开始的设想相反。因此,如果不是必要,就不要这么做。
|
||||
|
||||
## 14. Temporary Field(令人迷惑的暂时字段)
|
||||
|
||||
某个字段仅为某种特定情况而设,这样的代码不易理解,因为通常认为对象在所有时候都需要它的所有字段。
|
||||
|
||||
把这种字段和特定情况的处理操作使用 Extract Class 提炼到一个独立类中。
|
||||
|
||||
## 15. Message Chains(过度耦合的消息链)
|
||||
|
||||
一个对象请求另一个对象,然后再向后者请求另一个对象,然后...,这就是消息链。采用这种方式,意味着客户代码将与对象间的关系紧密耦合。
|
||||
|
||||
改用函数链,用函数委托另一个对象来处理。
|
||||
|
||||
## 16. Middle Man(中间人)
|
||||
|
||||
中间人负责处理委托给它的操作,如果一个类中有过多的函数都委托给其它类,那就是过度运用委托,应当 Remove Middle Man,直接与负责的对象打交道。
|
||||
|
||||
## 17. Inappropriate Intimacy(狎昵关系)
|
||||
|
||||
两个类多于亲密,花费太多时间去探讨彼此的 private 成分。
|
||||
|
||||
## 18. Alernative Classes with Different Interfaces(异曲同工的类)
|
||||
|
||||
## 19. Incomplete Library Class(不完美的类库)
|
||||
|
||||
类库的设计者不可能设计出完美的类库,当我们需要对类库进行一些修改时,可以使用以下两种方法:如果只是修改一两个函数,使用 Introduce Foreign Method;如果要添加一大堆额外行为,使用 Introduce Local Extension。
|
||||
|
||||
## 20. Data Class(幼稚的数据类)
|
||||
|
||||
它只拥有一些数据字段。
|
||||
|
||||
找出字段使用的地方,然后把相应的操作移到 Data Class 中。
|
||||
|
||||
## 21. Refused Bequest(被拒绝的馈赠)
|
||||
|
||||
子类继承超类的所有函数和数据,但是它只想要一部分。
|
||||
|
||||
为子类新建一个兄弟类,不需要的函数或数据使用 Push Down Method 和 Push Down Field 下推给那个兄弟。
|
||||
|
||||
## 22. Comments(过多的注释)
|
||||
|
||||
使用 Extract Method 提炼出需要注释的部分,然后用函数名来解释函数的行为。
|
||||
|
||||
# 第四章 构筑测试体系
|
||||
|
||||
Java 可以使用 Junit 进行单元测试。
|
||||
|
||||
单元测试的对象是类的方法,而功能测以客户的角度保证软件正常运行。
|
||||
|
||||
应当集中测试可能出错的边界条件。
|
||||
|
||||
# 第五章 重构列表
|
||||
|
||||
小步前进,频繁测试。
|
||||
|
||||
# 第六章 重新组织函数
|
||||
|
||||
## 1. Extract Method(提炼函数)
|
||||
|
||||
将这段代码放进一个独立函数中,并让函数名称解释该函数的用途。
|
||||
|
||||
## 2. Inline Method(内联函数)
|
||||
|
||||
一个函数的本体与名称同样清楚易懂。
|
||||
|
||||
在函数调用点插入函数本体,然后移除该函数。
|
||||
|
||||
## 3. Inline Temp(内联临时变量)
|
||||
|
||||
一个临时变量,只被简单表达式赋值一次,而它妨碍了其它重构手法。
|
||||
|
||||
将所有对该变量的引用替换为对它赋值的那个表达式自身。
|
||||
|
||||
```java
|
||||
double basePrice = anOrder.basePrice();
|
||||
return basePrice > 1000;
|
||||
```
|
||||
|
||||
```java
|
||||
return anOrder.basePrice() > 1000;
|
||||
```
|
||||
|
||||
## 4. Replace Temp with Query(以查询取代临时变量)
|
||||
|
||||
以临时变量保存某一表达式的运算结果,将这个表达式提炼到一个独立函数中,将所有对临时变量的引用点替换为对新函数的调用。Replace Temp with Query 往往是 Extract Method 之前必不可少的一个步骤,因为局部变量会使代码难以提炼。
|
||||
|
||||
```java
|
||||
double basePrice = quantity * itemPrice;
|
||||
if(basePrice > 1000)
|
||||
return basePrice * 0.95;
|
||||
else
|
||||
return basePrice * 0.98;
|
||||
```
|
||||
|
||||
```java
|
||||
if(basePrice() > 1000)
|
||||
return basePrice() * 0.95;
|
||||
else
|
||||
return basePrice() * 0.98;
|
||||
|
||||
// ...
|
||||
double basePrice(){
|
||||
return quantity * itemPrice;
|
||||
}
|
||||
```
|
||||
|
||||
## 5. Introduce Explaining Variable(引起解释变量)
|
||||
|
||||
将复杂表达式(或其中一部分)的结果放进一个临时变量,以此变量名称来解释表达式用途。
|
||||
|
||||
```java
|
||||
if((platform.toUpperCase().indexOf("MAC") > -1) &&
|
||||
(browser.toUpperCase().indexOf("IE") > -1) &&
|
||||
wasInitialized() && resize > 0) {
|
||||
// do something
|
||||
}
|
||||
```
|
||||
|
||||
```java
|
||||
final boolean isMacOS = platform.toUpperCase().indexOf("MAC") > -1;
|
||||
final boolean isIEBrower = browser.toUpperCase().indexOf("IE") > -1;
|
||||
final boolean wasResized = resize > 0;
|
||||
|
||||
if(isMacOS && isIEBrower && wasInitialized() && wasResized) {
|
||||
// do something
|
||||
}
|
||||
```
|
||||
|
||||
## 6. Split Temporary Variable(分解临时变量)
|
||||
|
||||
某个临时变量被赋值超过一次,它既不是循环变量,也不是用于收集计算结果。
|
||||
|
||||
针对每次赋值,创造一个独立、对应的临时变量,每个临时变量只承担一个责任。
|
||||
|
||||
## 7. Remove Assigments to Parameters(移除对参数的赋值)
|
||||
|
||||
以一个临时变量取代对该参数的赋值。
|
||||
|
||||
```java
|
||||
int discount (int inputVal, int quentity, int yearToDate){
|
||||
if (inputVal > 50) inputVal -= 2;
|
||||
```
|
||||
|
||||
```java
|
||||
int discount (int inputVal, int quentity, int yearToDate){
|
||||
int result = inputVal;
|
||||
if (inputVal > 50) result -= 2;
|
||||
```
|
||||
|
||||
## 8. Replace Method with Method Object(以函数对象取代函数)
|
||||
|
||||
当对一个大型函数采用 Extract Method 时,由于包含了局部变量使得很难进行该操作。
|
||||
|
||||
将这个函数放进一个单独对象中,如此一来局部变量就成了对象内的字段。然后可以在同一个对象中将这个大型函数分解为多个小型函数。
|
||||
|
||||
## 9. Subsititute Algorithn(替换算法)
|
||||
|
||||
# 第七章 在对象之间搬移特性
|
||||
|
||||
## 1. Move Method(搬移函数)
|
||||
|
||||
类中的某个函数与另一个类进行更多交流:调用后者或者被后者调用。
|
||||
|
||||
将这个函数搬移到另一个类中。
|
||||
|
||||
## 2. Move Field(搬移字段)
|
||||
|
||||
类中的某个字段被另一个类更多地用到,这里的用到是指调用取值设值函数,应当把该字段移到另一个类中。
|
||||
|
||||
## 3. Extract Class(提炼类)
|
||||
|
||||
某个类做了应当由两个类做的事。
|
||||
|
||||
应当建立一个新类,将相关的字段和函数从旧类搬移到新类。
|
||||
|
||||
## 4. Inline Class(将类内联化)
|
||||
|
||||
与 Extract Class 相反。
|
||||
|
||||
## 5. Hide Delegate(隐藏“委托关系”)
|
||||
|
||||
建立所需的函数,隐藏委托关系。
|
||||
|
||||
```java
|
||||
class Person{
|
||||
Department department;
|
||||
|
||||
public Department getDepartment(){
|
||||
return department;
|
||||
}
|
||||
}
|
||||
|
||||
class Department{
|
||||
private Person manager;
|
||||
|
||||
public Person getManager(){
|
||||
return manager;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
如果客户希望知道某人的经理是谁,必须获得 Department 对象,这样就对客户揭露了 Department 的工作原理。
|
||||
|
||||
```java
|
||||
Person manager = john.getDepartment().getManager();
|
||||
```
|
||||
|
||||
通过为 Peron 建立一个函数来隐藏这种委托关系。
|
||||
|
||||
```java
|
||||
public Person getManager(){
|
||||
return department.getManager();
|
||||
}
|
||||
```
|
||||
|
||||
## 6. Remove Middle Man(移除中间人)
|
||||
|
||||
与 Hide Delegate 相反,本方法需要移除委托函数,让客户直接调用委托类。
|
||||
|
||||
Hide Delegate 有很大好处,但是它的代价是:每当客户要使用受托类的新特性时,就必须在服务器端添加一个简单的委托函数。随着受委托的特性越来越多,服务器类完全变成了一个“中间人”。
|
||||
|
||||
## 7. Introduce Foreign Method(引入外加函数)
|
||||
|
||||
需要为提供服务的类添加一个函数,但是无法修改这个类。
|
||||
|
||||
可以在客户类中建立一个函数,并以第一参数形式传入一个服务类的实例,让客户类组合服务器实例。
|
||||
|
||||
## 8. Introduce Local Extension(引入本地扩展)
|
||||
|
||||
和 Introduce Foreign Method 目的一样,但是 Introduce Local Extension 通过建立新的类来实现。有两种方式:子类或者包装类,子类就是通过继承实现,包装类就是通过组合实现。
|
||||
|
||||
# 第八章 重新组织数据
|
||||
|
||||
## 1. Self Encapsulate Field(自封装字段)
|
||||
|
||||
为字段建立取值/设值函数,并用这些函数来访问字段。只有当子类想访问超类的一个字段,又想在子类中将对这个字段访问改为一个计算后的值,才使用这种方式,否则直接访问字段的方式简洁明了。
|
||||
|
||||
## 2. Replace Data Value with Object(以对象取代数据值)
|
||||
|
||||
在开发初期,往往会用简单的数据项表示简单的情况,但是随着开发的进行,一些简单数据项会具有一些特殊行为。比如一开始会把电话号码存成字符串,但是随后发现电话号码需要“格式化”、“抽取区号”之类的特殊行为。
|
||||
|
||||
## 3. Change Value to Reference(将值对象改成引用对象)
|
||||
|
||||
将彼此相等的实例替换为同一个对象。这就要用一个工厂来创建这种唯一对象,工厂类中需要保留一份已经创建对象的列表,当要创建一个对象时,先查找这份列表中是否已经存在该对象,如果存在,则返回列表中的这个对象;否则,新建一个对象,添加到列表中,并返回该对象。
|
||||
|
||||
## 4. Change Reference to value(将引用对象改为值对象)
|
||||
|
||||
以 Change Value to Reference 相反。值对象有个非常重要的特性:它是不可变的,不可变表示如果要改变这个对象,必须用一个新的对象来替换旧对象,而不是修改旧对象。
|
||||
|
||||
需要为值对象实现 equals() 和 hashCode() 方法
|
||||
|
||||
## 5. Replace Array with Object(以对象取代数组)
|
||||
|
||||
有一个数组,其中的元素各自代表不同的东西。
|
||||
|
||||
以对象替换数组,对于数组中的每个元素,以一个字段来表示,这样方便操作,也更容易理解。
|
||||
|
||||
## 6. Duplicate Observed Data(赋值“被监视数据”)
|
||||
|
||||
一些领域数据置身于 GUI 控件中,而领域函数需要访问这些数据。
|
||||
|
||||
将该数据赋值到一个领域对象中,建立一个 Oberver 模式,用以同步领域对象和 GUI 对象内的重复数据。
|
||||
|
||||
![](https://github.com/CyC2018/InterviewNotes/blob/master/pics/e024bd7e-fb4e-4239-9451-9a6227f50b00.jpg)
|
||||
|
||||
## 7. Change Unidirectional Association to Bidirectional(将单向关联改为双向关联)
|
||||
|
||||
当两个类都需要对方的特性时,可以使用双向关联。
|
||||
|
||||
有两个类,分别为订单 Order 和客户 Customer,Order 引用了 Customer,Customer 也需要引用 Order 来查看其所有订单详情。
|
||||
|
||||
```java
|
||||
class Order{
|
||||
private Customer customer;
|
||||
public void setCustomer(Customer customer){
|
||||
if(this.customer != null)
|
||||
this.customer.removeOrder(this);
|
||||
this.customer = customer;
|
||||
this.customer.add(this);
|
||||
}
|
||||
}
|
||||
```
|
||||
```java
|
||||
class Curstomer{
|
||||
private Set<Order> orders = new HashSet<>();
|
||||
public void removeOrder(Order order){
|
||||
orders.remove(order);
|
||||
}
|
||||
public void addOrder(Order order){
|
||||
orders.add(order);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
注意到,这里让 Curstomer 类来控制关联关系。有以下原则来决定哪个类来控制关联关系:如果某个对象是组成另一个对象的部件,那么由后者负责控制关联关系;如果是一对多关系,则由单一引用那一方来控制关联关系。
|
||||
|
||||
## 8. Change Bidirectional Association to Unidirectional(将双向关联改为单向关联)
|
||||
|
||||
和 Change Unidirectional Association to Bidirectiona 为反操作。
|
||||
|
||||
双向关联维护成本高,并且也不易于理解。大量的双向连接很容易造成“僵尸对象”:某个对象本身已经死亡了,却保留在系统中,因为它的引用还没有全部完全清除。
|
||||
|
||||
## 9. Replace Magic Number with Symbolic Constant(以字面常量取代魔法数)
|
||||
|
||||
创建一个常量,根据其意义为它命名,并将字面常量换位这个常量。
|
||||
|
||||
## 10. Encapsulate Field(封装字段)
|
||||
|
||||
public 字段应当改为 private,并提供相应的访问函数。
|
||||
|
||||
## 11. Encapsulate Collection(封装集合)
|
||||
|
||||
函数返回集合的一个只读副本,并在这个类中提供添加/移除集合元素的函数。如果函数返回集合自身,会让用户得以修改集合内容而集合拥有者却一无所知。
|
||||
|
||||
## 12. Replace Record with Data Class(以数据类取代记录)
|
||||
|
||||
## 13. Replace Type Code with Class(以类取代类型码)
|
||||
|
||||
类中有一个数值类型码,但它并不影响类的行为,就用一个新类替换该数值类型码。如果类型码出现在 switch 语句中,需要使用 Replace Conditional with Polymorphism 去掉 switch,首先必须运用 Replace Type Code with Subcalss 或 Replace Type Code with State/Strategy 去掉类型码。
|
||||
|
||||
![](https://github.com/CyC2018/InterviewNotes/blob/master/pics/27c2e0b3-8f95-453d-bedc-6398a8566ce9.jpg)
|
||||
|
||||
## 14. Replace Type Code with Subcalsses(以子类取代类型码)
|
||||
|
||||
有一个不可变的类型码,它会影响类的行为,以子类取代这个类型码。
|
||||
|
||||
![](https://github.com/CyC2018/InterviewNotes/blob/master/pics/c41d3977-e0e7-4ee4-93e1-d84f1ae3e20e.jpg)
|
||||
|
||||
## 15. Replace Type Code with State/Strategy (以 State/Strategy 取代类型码)
|
||||
|
||||
有一个可变的类型码,它会影响类的行为,以状态对象取代类型码。
|
||||
|
||||
和 Replace Type Code with Subcalsses 的区别是 Replace Type Code with State/Strategy 的类型码是动态可变的,前者通过继承的方式来实现,后者通过组合的方式来实现。因为类型码可变,如果通过继承的方式,一旦一个对象的类型码改变,那么就要改变用新的对象来取代旧对象,而客户端难以改变新的对象。但是通过组合的方式,改变引用的状态类是很容易的。
|
||||
|
||||
![](https://github.com/CyC2018/InterviewNotes/blob/master/pics/81fd1d6f-a3b2-4160-9a0a-1f7cb50ba440.jpg)
|
||||
|
||||
## 16. Replace Subclass with Fields(以字段取代子类)
|
||||
|
||||
各个子类的唯一差别只在“返回常量数据”的函数上。
|
||||
|
||||
![](https://github.com/CyC2018/InterviewNotes/blob/master/pics/f2e0cee9-ecdc-4a96-853f-d9f6a1ad6ad1.jpg)
|
||||
|
||||
# 第九章 简化条件表达式
|
||||
|
||||
## 1. Decompose Conditional(分解条件表达式)
|
||||
|
||||
对于一个复杂的条件语句,可以从 if、then、else 三个段落中分别提炼出独立函数。
|
||||
|
||||
```java
|
||||
if(data.befor(SUMMER_START) || data.after(SUMMER_END))
|
||||
charge = quantity * winterRate + winterServiceCharge;
|
||||
else charge = quantity * summerRate;
|
||||
```
|
||||
|
||||
```java
|
||||
if(notSummer(date))
|
||||
charge = winterCharge(quantity);
|
||||
else charge = summerCharge(quantity);
|
||||
```
|
||||
|
||||
## 2. Consolidate Conditional Expression(合并条件表达式)
|
||||
|
||||
有一系列条件测试,都得到相同结果。
|
||||
|
||||
将这些测试合并为一个条件表达式,并将这个条件表达式提炼成为一个独立函数。
|
||||
|
||||
```java
|
||||
double disabilityAmount(){
|
||||
if (seniority < 2) return 0;
|
||||
if (monthsDisabled > 12 ) return 0;
|
||||
if (isPartTime) return 0;
|
||||
// ...
|
||||
}
|
||||
```
|
||||
```java
|
||||
double disabilityAmount(){
|
||||
if (isNotEligibleForDisability()) return 0;
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
## 3. Consolidate Duplicate Conditional Fragments (合并重复的条件片段)
|
||||
|
||||
在条件表达式的每个分支上有着相同的一段代码。
|
||||
|
||||
将这段重复代码搬移到条件表达式之外。
|
||||
|
||||
```java
|
||||
if (isSpecialDeal()){
|
||||
total = price * 0.95;
|
||||
send();
|
||||
} else {
|
||||
total = price * 0.98;
|
||||
send();
|
||||
}
|
||||
```
|
||||
|
||||
```java
|
||||
if (isSpecialDeal()) {
|
||||
total = price * 0.95;
|
||||
} else {
|
||||
total = price * 0.98;
|
||||
}
|
||||
send();
|
||||
```
|
||||
|
||||
## 4. Remove Control Flag(移除控制标记)
|
||||
|
||||
在一系列布尔表达式中,某个变量带有“控制标记”的作用。
|
||||
|
||||
用 break语 句或 return 语句来取代控制标记。
|
||||
|
||||
## 5. Replace Nested Conditional with Guard Clauses (以卫语句取代嵌套条件表达式)
|
||||
|
||||
如果某个条件极其罕见,就应该单独检查该条件,并在该条件为真时立刻从函数中返回,这样的单独检查常常被称为“卫语句”(guard clauses)。
|
||||
|
||||
条件表达式通常有两种表现形式。第一种形式是:所有分支都属于正常行为。第二种形式则是:条件表达式提供的答案中只有一种是正常行为,其他都是不常见的情况,可以使用卫语句表现所有特殊情况。
|
||||
|
||||
```java
|
||||
double getPayAmount() {
|
||||
double result;
|
||||
if (isDead) result = deadAmount();
|
||||
else {
|
||||
if (isSeparated) result = separatedAmount();
|
||||
else {
|
||||
if (isRetired) result = retiredAmount();
|
||||
else result = normalPayAmount();
|
||||
};
|
||||
}
|
||||
return result;
|
||||
};
|
||||
```
|
||||
|
||||
```java
|
||||
double getPayAmount() {
|
||||
if (isDead) return deadAmount();
|
||||
if (isSeparated) return separatedAmount();
|
||||
if (isRetired) return retiredAmount();
|
||||
return normalPayAmount();
|
||||
};
|
||||
```
|
||||
|
||||
## 6. Replace Conditional with Polymorphism (以多态取代条件表达式)
|
||||
|
||||
将这个条件表达式的每个分支放进一个子类内的覆写函数中,然后将原始函数声明为抽象函数。需要先使用 Replace Type Code with Subclass 或 Replace Type Code with State/Strategy 来建立继承结果。
|
||||
|
||||
```java
|
||||
double getSpeed() {
|
||||
switch (type) {
|
||||
case EUROPEAN:
|
||||
return getBaseSpeed();
|
||||
case AFRICAN:
|
||||
return getBaseSpeed()- getLoadFactor()* numberOfCoconuts;
|
||||
case NORWEGIAN_BLUE:
|
||||
return isNailed ? 0 : getBaseSpeed(voltage);
|
||||
}
|
||||
throw new RuntimeException("Should be unreachable");
|
||||
}
|
||||
```
|
||||
|
||||
![](https://github.com/CyC2018/InterviewNotes/blob/master/pics/1c8432c8-2552-457f-b117-1da36c697221.jpg)
|
||||
|
||||
## 7. Introduce Null Object(引入Null对象)
|
||||
|
||||
将 null 值替换为 null 对象。这样做的好处在于,不需要询问对象是否为空,直接调用就行。
|
||||
|
||||
```java
|
||||
if (customer == null) plan = BillingPlan.basic();
|
||||
else plan = customer.getPlan();
|
||||
```
|
||||
|
||||
## 8. Introduce Assertion(引入断言)
|
||||
|
||||
以断言明确表现某种假设。断言只能用于开发过程中,产品代码中不会有断言。
|
||||
|
||||
```java
|
||||
double getExpenseLimit() {
|
||||
// should have either expense limit or a primary project
|
||||
return (expenseLimit != NULL_EXPENSE) ? expenseLimit : primaryProject.getMemberExpenseLimit();
|
||||
}
|
||||
```
|
||||
|
||||
```java
|
||||
double getExpenseLimit() {
|
||||
Assert.isTrue (expenseLimit != NULL_EXPENSE || primaryProject != null);
|
||||
return (expenseLimit != NULL_EXPENSE) ? expenseLimit : primaryProject.getMemberExpenseLimit();
|
||||
}
|
||||
```
|
||||
|
||||
# 第十章 简化函数调用
|
||||
|
||||
## 1. Rename Method(函数改名)
|
||||
|
||||
使函数名能解释函数的用途。
|
||||
|
||||
## 2. Add Parameter(添加参数)
|
||||
|
||||
使函数不需要通过调用获得某个信息。
|
||||
|
||||
## 3. Remove Parameter(移除参数)
|
||||
|
||||
与 Add Parameter 相反,改用调用的方式来获得某个信息。
|
||||
|
||||
## 4. Separate Query from Modifier(将查询函数和修改函数分离)
|
||||
|
||||
某个函数即返回对象状态值,又修改对象状态。
|
||||
|
||||
应当建立两个不同的函数,其中一个负责查询,另一个负责修改。任何有返回值的函数,都不应该有看得到的副作用。
|
||||
|
||||
```java
|
||||
getTotalOutstandingAndSetReadyForSummaries();
|
||||
```
|
||||
|
||||
```java
|
||||
getTotalOutstanding();
|
||||
setReadyForSummaries();
|
||||
```
|
||||
|
||||
## 5. Parameterize Method(令函数携带参数)
|
||||
|
||||
若干函数做了类似的工作,但在函数本体中却包含了不同的值。
|
||||
|
||||
建立单一函数,以参数表达那些不同的值。
|
||||
|
||||
```java
|
||||
fivePercentRaise();
|
||||
tenPercentRaise();
|
||||
```
|
||||
```java
|
||||
raise(percentage);
|
||||
```
|
||||
|
||||
## 6. Replace Parameter with Explicit Methods(以明确函数取代参数)
|
||||
|
||||
有一个函数,完全取决于参数值而采取不同行为。
|
||||
|
||||
针对该参数的每一个可能值,建立一个独立函数。
|
||||
|
||||
```java
|
||||
void setValue(String name, int value){
|
||||
if (name.equals("height")){
|
||||
height = value;
|
||||
return;
|
||||
}
|
||||
if (name.equals("width")){
|
||||
width = value;
|
||||
return;
|
||||
}
|
||||
Assert.shouldNeverReachHere();
|
||||
}
|
||||
```
|
||||
|
||||
```java
|
||||
void setHeight(int arg){
|
||||
height = arg;
|
||||
}
|
||||
void setWidth(int arg){
|
||||
width = arg;
|
||||
}
|
||||
```
|
||||
|
||||
## 7. Preserve Whole Object(保持对象完整)
|
||||
|
||||
从某个对象中取出若干值,将它们作为某一次函数调用时的参数。
|
||||
|
||||
改为传递整个对象。
|
||||
|
||||
```java
|
||||
int low = daysTempRange().getLow();
|
||||
int high = daysTempRange().getHigh();
|
||||
withinPlan = plan.withinRange(low,high);
|
||||
```
|
||||
|
||||
```java
|
||||
withinPlan = plan.withinRange(daysTempRange());
|
||||
```
|
||||
|
||||
## 8. Replace Parameter with Methods(以函数取代参数)
|
||||
|
||||
对象调用某个函数,并将所得结果作为参数,传递给另一个函数。而接受该参数的函数本身也能够调用前一个函数。
|
||||
|
||||
让参数接收者去除该项参数,而是直接调用前一个函数。
|
||||
|
||||
```java
|
||||
int basePrice = _quantity * _itemPrice;
|
||||
discountLevel = getDiscountLevel();
|
||||
double finalPrice = discountedPrice (basePrice, discountLevel);
|
||||
```
|
||||
|
||||
```java
|
||||
int basePrice = _quantity * _itemPrice;
|
||||
double finalPrice = discountedPrice (basePrice);
|
||||
```
|
||||
|
||||
## 9. Introduce Parameter Object(引入参数对象)
|
||||
|
||||
某些参数总是很自然地同时出现,这些参数就是 Data Clumps。
|
||||
|
||||
以一个对象取代这些参数。
|
||||
|
||||
![](https://github.com/CyC2018/InterviewNotes/blob/master/pics/08738dd0-ae8e-404a-ba78-a6b1b7d225b3.jpg)
|
||||
|
||||
## 10. Remove Setting Method(移除设值函数)
|
||||
|
||||
类中的某个字段应该在对象创建时被设值,然后就不再改变。
|
||||
|
||||
去掉该字段的所有设值函数,并将该字段设为 final。
|
||||
|
||||
## 11. Hide Method(隐藏函数)
|
||||
|
||||
有一个函数,从来没有被其他任何类用到。
|
||||
|
||||
将这个函数修改为 private。
|
||||
|
||||
## 12. Replace Constructor with Factory Method (以工厂函数取代构造函数)
|
||||
|
||||
希望在创建对象时不仅仅是做简单的建构动作。
|
||||
|
||||
将构造函数替换为工厂函数。
|
||||
|
||||
## 13. Encapsulate Downcast(封装向下转型)
|
||||
|
||||
某个函数返回的对象,需要由函数调用者执行向下转型(downcast)。
|
||||
|
||||
将向下转型动作移到函数中。
|
||||
|
||||
```java
|
||||
Object lastReading(){
|
||||
return readings.lastElement();
|
||||
}
|
||||
```
|
||||
```java
|
||||
Reading lastReading(){
|
||||
return (Reading)readings.lastElement();
|
||||
}
|
||||
```
|
||||
|
||||
## 14. Replace Error Code with Exception (以异常取代错误码)
|
||||
|
||||
某个函数返回一个特定的代码,用以表示某种错误情况。
|
||||
|
||||
改用异常,异常将普通程序和错误处理分开,使代码更容易理解。
|
||||
|
||||
## 15. Replace Exception with Test(以测试取代异常)
|
||||
|
||||
面对一个调用者可以预先检查的条件,你抛出了一个异常。
|
||||
|
||||
修改调用者,使它在调用函数之前先做检查。
|
||||
|
||||
```java
|
||||
double getValueForPeriod(int periodNumber) {
|
||||
try {
|
||||
return values[periodNumber];
|
||||
} catch (ArrayIndexOutOfBoundsException e) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
```
|
||||
```java
|
||||
double getValueForPeriod(int periodNumber) {
|
||||
if (periodNumber >= values.length) return 0;
|
||||
return values[periodNumber];
|
||||
```
|
||||
|
||||
# 第十一章 处理概括关系
|
||||
|
||||
## 1. Pull Up Field(字段上移)
|
||||
|
||||
两个子类拥有相同的字段。
|
||||
|
||||
将该字段移至超类。
|
||||
|
||||
## 2. Pull Up Method(函数上移)
|
||||
|
||||
有些函数,在各个子类中产生完全相同的结果。
|
||||
|
||||
将该函数移至超类。
|
||||
|
||||
## 3. Pull Up Constructor Body(构造函数本体上移)
|
||||
|
||||
你在各个子类中拥有一些构造函数,它们的本体几乎完全一致。
|
||||
|
||||
在超类中新建一个构造函数,并在子类构造函数中调用它。
|
||||
|
||||
```java
|
||||
class Manager extends Employee...
|
||||
|
||||
public Manager(String name, String id, int grade) {
|
||||
this.name = name;
|
||||
this.id = id;
|
||||
this.grade = grade;
|
||||
}
|
||||
```
|
||||
|
||||
```java
|
||||
public Manager(String name, String id, int grade) {
|
||||
super(name, id);
|
||||
this.grade = grade;
|
||||
}
|
||||
```
|
||||
|
||||
## 4. Push Down Method(函数下移)
|
||||
|
||||
超类中的某个函数只与部分子类有关。
|
||||
|
||||
将这个函数移到相关的那些子类去。
|
||||
|
||||
## 5. Push Down Field(字段下移)
|
||||
|
||||
超类中的某个字段只被部分子类用到。
|
||||
|
||||
将这个字段移到需要它的那些子类去。
|
||||
|
||||
## 6. Extract Subclass(提炼子类)
|
||||
|
||||
类中的某些特性只被某些实例用到。
|
||||
|
||||
新建一个子类,将上面所说的那一部分特性移到子类中。
|
||||
|
||||
## 7. Extract Superclass(提炼超类)
|
||||
|
||||
两个类有相似特性。
|
||||
|
||||
为这两个类建立一个超类,将相同特性移至超类。
|
||||
|
||||
## 8. Extract Interface(提炼接口)
|
||||
|
||||
若干客户使用类接口中的同一子集,或者两个类的接口有部分相同。
|
||||
|
||||
将相同的子集提炼到一个独立接口中。
|
||||
|
||||
## 9. Collapse Hierarchy(折叠继承体系)
|
||||
|
||||
超类和子类之间无太大区别。
|
||||
|
||||
将它们合为一体。
|
||||
|
||||
## 10. Form Template Method(塑造模板函数)
|
||||
|
||||
你有一些子类,其中相应的某些函数以相同顺序执行类似的操作,但各个操作的细节上有所不同。
|
||||
|
||||
将这些操作分别放进独立函数中,并保持它们都有相同的签名,于是原函数也就变得相同了。然后将原函数上移至超类。(模板方法模式)
|
||||
|
||||
## 11. Replace Inheritance with Delegation (以委托取代继承)
|
||||
|
||||
某个子类只使用超类接口中的一部分,或是根本不需要继承而来的数据。
|
||||
|
||||
在子类中新建一个字段用以保存超类,调整子类函数,令它改而委托超类,然后去掉两者之间的继承关系。
|
||||
|
||||
## 12. Replace Delegation with Inheritance (以继承取代委托)
|
||||
|
||||
你在两个类之间使用委托关系,并经常为整个接口编写许多极简单的委托函数。
|
||||
|
||||
让委托类继承受托类。
|
135
notes/黑客与画家.md
Normal file
135
notes/黑客与画家.md
Normal file
|
@ -0,0 +1,135 @@
|
|||
<!-- GFM-TOC -->
|
||||
* [译者序](#译者序)
|
||||
* [为什么书呆子不受欢迎](#为什么书呆子不受欢迎)
|
||||
* [黑客与画家](#黑客与画家)
|
||||
* [不能说的话](#不能说的话)
|
||||
* [另一条路](#另一条路)
|
||||
* [设计与研究](#设计与研究)
|
||||
<!-- GFM-TOC -->
|
||||
|
||||
# 译者序
|
||||
|
||||
作者:美国互联网创业之父
|
||||
|
||||
最开始 hack 指的是难题的解决办法,那些最能干的人被成为 hacker。黑客包含以下三个特点:好玩、高智商、探索精神。黑客的核心价值观:分享、开放、民主、计算机的自由使用、进步。因此黑客指的是那些专家级程序员。
|
||||
|
||||
由于黑客常常入侵系统,因此被与那些破坏计算机系统的人联系起来。然而破坏计算机系统的人称为骇客,真正的黑客不这么做,反而是让世界变得更好。
|
||||
|
||||
# 为什么书呆子不受欢迎
|
||||
|
||||
作者认为,“书呆子”和“高智商”正相关,而“书呆子”和“受欢迎”负相关。
|
||||
|
||||
提出问题:既然书呆子智商高,那么为什么不想出受欢迎的方法?因为要想受欢迎,需要投入很大的时间精力去塑造个人魅力。而书呆子没有那么多的时间和精力,相比于受欢迎,他们更在乎其它更有趣的事,比如设计奇妙的火箭等。
|
||||
|
||||
欺负书呆子的原因:
|
||||
|
||||
- 青少年的孩子更加残忍,书呆子不受欢迎意味着被歧视和受欺负。
|
||||
> 11 岁以前,小孩的生活由家长主导,其他孩子的影响有限。 孩子们不是不关心小学里其他同学的想法,但是后者不具有决定性影响。小学毕业以后,这种情形开始发生变化。到了 11 岁左右,孩子们逐渐把家庭生活当作寻常事了。 他们在同伴中开辟了一个新的世界,并认为那个世界才是重要的,比家里的世界更重要。实际上,如果他们在家里与父母发生冲突, 反而能在那个新的世界中挣得面子,而他们也确实更在乎那个世界。但是,问题在于,孩子们自己创造出来的世界是一个非常原始的世界。青少年在心理上还没有摆脱儿童状态, 许多人都会残忍地对待他人。他们折磨书呆子的原因就像拔掉一条蜘蛛腿一样,觉得很好玩。在一个人产生良知之前,折磨就是一种娱乐。
|
||||
|
||||
- 为了凸显自己。
|
||||
> 在任何社会等级制度中,那些对自己没自信的人就会通过虐待他们眼中的下等人来突显自己的身份。我已经意识到,正是因为这个原因,在美国社会中底层白人是对待黑人最残酷的群体。最受欢迎的孩子并不欺负书呆子,他们不需要靠踩在书呆子身上来垫高自己。大部分的欺负来自处于下一等级的学生,那些焦虑的中间层。
|
||||
|
||||
- 为了和受欢迎的人结成同盟。
|
||||
> 没有什么比一个共同的敌人更能使得人们团结起来了。这就好比一个政客,他想让选民忘记糟糕的国内局势,方法就是为国家找出一个敌人,哪怕敌人并不真的存在,他也可以创造一个出来。一群人在一起,挑出一个书呆子,居高临下地欺负他,就会把彼此联系起来。一起攻击一个外人,所有人因此就都成了自己人。这就是为什么最恶劣的以强凌弱的事件都与团体有关的原因。随便找一个书呆子,他都会告诉你,一群人的虐待比一个人的虐待残酷得多。
|
||||
|
||||
学校老师的只是把教书当成工作。
|
||||
> 公立学校的老师很像监狱的狱卒。看管监狱的人主要关心的是犯人都待在自己应该待的位置。然后,让犯人有东西吃,尽可能不要发生斗殴和伤害事件,这就可以了。
|
||||
|
||||
到社会之后,书呆子会被友好对待,因为他们做的事能够产生更多的影响,而社会正需要这些影响。
|
||||
|
||||
成年人把孩子在学校受苦受难归结为青春期的激素影响,但是实际上这是学校这种体制的问题,学校只是把孩子圈养起来,不让他们到处乱跑,然而学校内部存在着很多残忍。
|
||||
|
||||
过去的孩子更早投入社会,也更能够真正学会本领。
|
||||
> 过去的社会中,青少年扮演着一个更积极的角色。工业化时代到来前,青少年都是某种形式的学徒,不是在某个作坊,就是在某个农庄,甚至在某艘军舰上。他们不会被扔到一旁,创造自己的小社会。他们是成年人社会的低级成员。以前的青少年似乎也更尊敬成年人,因为成年人都是看得见的专家,会传授他们所要学习的技能。如今的大多数青少年,对他们的家长在遥远的办公室所从事的工作几乎一无所知。他们看不到学校作业与未来走上社会后从事的工作有何联系。
|
||||
|
||||
学校的使命是教书育人,而并没有外界的压力去监督他们这么做,所以老师和学生双方往往都是敷衍了事。
|
||||
|
||||
没有外在的对手,孩子们就互相把对方当作对手。
|
||||
|
||||
成年人贬低了很多美好的东西,比如“人格”。
|
||||
> 许多书呆子可能都与我一样,直到高中毕业多年后,才去读中学里的指定读物。但是,我错过的绝不仅仅只是几本书而已。我对许多美好的字眼都嗤之以鼻,比如“人格”、“正直”,因为成年人贬低了这些词。在他们嘴里,这些词似乎都是同一个意思——“听话”。一些孩子因为具备所谓的“人格”和“正直”,而受到夸奖,可是他们不是呆得像一头大笨牛,就是轻浮得像一个不动脑筋的吹牛者。如果“人格”和“正直”就是这种样子,我宁愿不要它们。
|
||||
|
||||
# 黑客与画家
|
||||
|
||||
黑客和画家一样,都是艺术家,都在进行创作。
|
||||
|
||||
黑客比较喜欢脚本语言那种可以动态扩展,并且可以像铅笔画画一样修修改改。
|
||||
> 编程语言首要的特性应该是允许动态扩展(malleable)。编程语言是用来帮助思考程序的,而不是用来表达你已经想好的程序。它应该是一支铅笔,而不是一支钢笔。如果大家都像学校教的那样编程,那么静态类型(static typing)是一个不错的概念。但是,我认识的黑客,没有一个人喜欢用静态类型语言编程。我们需要的是一种可以随意涂抹、擦擦改改的语言,我们不想正襟危坐,把一个盛满各种变量类型的茶杯,小心翼翼放在自己的膝盖上,为了与一丝不苟的编译器大婶交谈,努力地挑选词语,确保变量类型匹配,好让自己显得礼貌又周到。
|
||||
|
||||
编码并不是科学研究,因此数学家不一定写一手好代码。
|
||||
> 创作者不同于科学家,明白这一点有很多好处。除了不用为静态类型烦恼以外,还可以免去另一个折磨科学家的难题,那就是“对数学的妒忌”。科学界的每一个人,暗地里都相信数学家比自己聪明。我觉得,数学家自己也相信这一点。最后的结果就是科学家往往会把自己的工作尽可能弄得看上去像数学。对于物理学这样的领域,这可能不会有太大不良影响。但是,你越往自然科学的方向发展,它就越成为一个严重的问题。
|
||||
|
||||
黑客为了能做自己喜欢做的事并且可以养活自己,往往白天工作晚上做自己的事。
|
||||
|
||||
热爱编程的人不可避免的会开发自己的项目。
|
||||
> 我们面试程序员的时候,主要关注的事情就是业余时间他们写了什么软件。因为如果你不爱一件事,你不可能把它做得真正优秀,要是你很热爱编程,你就不可避免地会开发你自己的项目。
|
||||
|
||||
黑客不是机械工作的技术工人,而是一位创作者。
|
||||
> 如果黑客只是一个负责实现领导意志的技术工人,职责就是根据规格说明书写出代码,那么他其实与一个挖水沟的工人是一样的,从这头挖到那头,仅此而已。但是,如果黑客是一个创作者,他从事的就不是机械性的工作,他必须具备灵感。
|
||||
|
||||
# 不能说的话
|
||||
|
||||
流行的道德观念虽然在后世看来会是荒诞的,但是如果在现今去违背这些道德观念,说一些不合适的言论,会让自己处于麻烦之中。
|
||||
|
||||
自己会深深赞同自己的观点,并且确信会得到别人的认同,很可能是,这些观点是别人给自己灌输的,别人说什么,自己就相信什么。
|
||||
|
||||
不能说的话往往是正确的话,因为具有争议性,会让某些人暴跳如雷。
|
||||
|
||||
那些不一定正确,极富争议性的言论被成为“异端邪说“,人们常常会给它们加上标签,目的是为了封杀它们。
|
||||
> 亵渎神明、冒犯圣灵、异端都是西方历史上常见的标签,当代的标签则是有伤风化、不得体、破坏国家利益等。
|
||||
|
||||
禁忌的推崇者是那些处于中等阶层的人,他们不像高等阶层那样有足够自信保护自己的利益,又不同于低等阶层,他们有实力去推行。
|
||||
|
||||
流行观点的第一批追逐者是为了让自己与众不同,而第二批则是怕自己与众不同。
|
||||
|
||||
科学家往往需要打破流行观点,从而取得突破。
|
||||
|
||||
如果因为说了某些话而让干扰了自己的生活,最好还是别说。
|
||||
> 这时你要明白,自由思考比畅所欲言更重要。如果你感到一定要跟那些人辩个明白,绝不咽下这口气,一定要把话说清楚,结果很可能是从此你再也无法自由理性地思考了。我认为这样做不可取,更好的方法是在思想和言论之间划一条明确的界线。在心里无所不想,但是不一定要说出来。我就鼓励自己在心里默默思考那些最无法无天的想法。你的思想是一个地下组织,绝不要把那里发生的事情一股脑说给外人听。“格斗俱乐部”的第一条规则,就是不要提到格斗俱乐部。
|
||||
|
||||
当狂热分子质问你的观点时,最好是说自己还没想好。
|
||||
|
||||
人们会误认为自己思想开放,但是他们心里早就有一根界限,认为什么是正确的什么是错误的。
|
||||
|
||||
更深入的认识自己。
|
||||
> 儿童精疲力竭时,可能会大发脾气,因为他不知道为了什么;成年人则会了解是个人的身体状况问题,与外界无关,说一句“没关系,我只是累了”。
|
||||
|
||||
# 另一条路
|
||||
|
||||
互联网软件的优点
|
||||
|
||||
1. 不需要用户来管理系统,因此不需要专门的计算机知识,使用浏览器即可使用;
|
||||
2. 需要的硬件资源很少,只需要能运行浏览器并且联网的设备就行;
|
||||
3. 更方便,随时随地就可以使用;
|
||||
4. 不用担心数据丢失,因为数据保存在云端;
|
||||
5. 易于对软件进行升级,而用户可以在完全不知情的情况下继续使用;
|
||||
6. 更容易的 bug 发现,更快的 bug 修改;
|
||||
7. 有专门的技术人员管理服务器,因为更加安全;
|
||||
|
||||
|
||||
bug 越快发现越好,而互联网软件因为发布周期短,用户反馈快,因此 bug 的发现也更快。
|
||||
|
||||
高级用户很乐意帮助寻找 bug。
|
||||
> 因为软件发布以后,大多数 bug 都是罕见情况下才会发生的个案,受到影响的用户往往都是高级使用者,他们喜欢试验那些不常用的、难度大的操作。高级使用者对 bug 的容忍度比较高,尤其如果这些 bug 是在开发新功能的过程中引入的,而这些新功能又正是他们所需要的,他们就更能理解了。事实上,因为 bug 不多,你只有经过一些复杂的过程以后才会遇到它们,所以高级使用者往往因为发现了 bug 感到很得意。他们打电话给客服时,多半是一副胜利者的口吻,而不是怒气冲冲的样子,好像他们击败我们得分了一样。
|
||||
|
||||
好的技术支持是:客服人员和技术人员离得很近,并且当场修复 bug。
|
||||
|
||||
一个好的想法在实现过程中能引起更多想法,因此尽早去实现它比将它束之高阁能够带来更多好处。
|
||||
|
||||
不会购买软件的用户使用了盗版软件能够让软件更具有市场影响力。
|
||||
> 一定数量的盗版对软件公司是有好处的。不管你的软件定价多少,有些用户永远都不会购买。如果这样的用户使用盗版,你并没有任何损失。事实上,你反而赚到了,因为你的软件现在多了一个用户,市场影响力就更大了一些,而这个用户可能毕业以后就会出钱购买你的软件。
|
||||
|
||||
# 设计与研究
|
||||
|
||||
设计追求的是好,而研究追求的是新。
|
||||
|
||||
用户要求和用户需求有区别,用户可能并不了解自己要什么,所以用户要求的内容往往不是用户真正需求的东西。
|
||||
|
||||
艺术设计与人为本,而科学研究追求简洁正确。比如数学家不会为了让读者更容易懂而采用一种更麻烦的证明,而是会选择最直接简洁的证明。编程语言也是与人为本的,因此编程语言也是通过设计产生的。
|
||||
|
||||
大理石做出的东西漂亮,但是制作过程却很难,无法不断的进行雕琢。编程语言也需要这种雕琢过程,如果一种编程语言像大理石一样,只是结果好看,那么它就不是一种好的编程语言。
|
||||
|
||||
设计软件应当尽快拿出原型来。就像画画一样,是先用几根线画出一个大致准确的轮廓,然后再逐步加工。
|
||||
> 还有另一种软件设计思想,也许可以被称为“圣母玛丽亚”模式。它不要求尽快拿出原型,然后再逐步优化,它的观点是你应该等到完整的成品出来以后再一下子隆重地推向市场,就像圣母玛丽亚降临一样,哪怕整个过程漫长得像橄榄球运动员长途奔袭也没有关系。在互联网泡沫时期,无数创业公司因为相信了这种模式而自毁前程。
|
||||
|
||||
如果觉得做某件事很乏味,那么做出来的东西就会很乏味。快速的原型能够让程序员有更高的士气,从而让他们更有兴趣去实现。也可以理解为快速反馈很重要。
|
Loading…
Reference in New Issue
Block a user