代码精进之路¶
01第一模块: 代码 “规范” 篇 (16 讲)¶
02 | 把错误关在笼子里的五道关卡¶
五道关卡:
1. 程序员
2. 编译器
3. 回归测试 (Regression Testing)
软件测试会尽可能地覆盖关键逻辑和负面清单,以确保:
关键功能能够正确执行,关键错误能够有效处理
4. 代码评审 (Code Review)
一个有效的在软件研发过程中抵御人类缺陷的制度
代码评审是通过阅读代码变更进行的
工具: Webrev
5. 代码分析 (Code Analysis)
a. 静态代码分析(Static Code Analysis)
商业软件 Coverity
开源软件 FindBugs, spotbugs
b. 代码覆盖率(Code Coverage)
03 | 优秀程序员的六个关键特质¶
硬性指标:
1. 掌握一门编程语言
2. 解决现实的问题
a. 外功: 各种工具、脚手架
b. 内功: 思维能力和行为能力
3. 发现关键的问题
意味着我们可以从一个被动的做事情的程序员,升级为一个主动找事情的程序员。
软性指标:
4. 沉静的前行者
懂得妥协,懂得选择,一步一步把事情沉静地朝前推动的人
要坦诚地面对别人的问题,也要坦然地面对自己的问题。
在解决问题和帮助别人解决问题中,把一个产品变得越来越好,问题越来越少。
5. 可以依赖的伙伴
编程语言、花样工具、逻辑思维、解决问题这些 “硬技能” 可以决定我们的起点
影响力、人际关系这些 “软技能” 通常影响着我们可以到达的高度
6. 时间管理者
要做只有你才能做的事情
要坚持做需要做的事情
04 | 代码规范的价值¶
备注
【定义】编码规范指的是针对特定编程语言约定的一系列规则,通常包括文件组织、缩进、注释、声明、语句、空格、命名约定、编程实践、编程原则和最佳实践等。
1. 规范的代码,可以降低代码出错的几率
复杂是代码质量的敌人
在编码的时候,我们应该尽量使代码风格直观、逻辑简单、表述直接
2. 规范的代码,可以提高编码的效率
在代码制造的每一道关卡,规范执行得越早,问题解决得越早,整个流水线的效率也就越高
对于违反强制规约的,报以错误;对于违反推荐或者规约参考的,报以警告
3. 规范的代码,降低软件维护成本
在一个软件生命周期里,软件维护阶段花费了大约 80% 的成本
很多软件代码,其生命的旅程超越了它的创造者,超越了团队的界限,
超越了组织的界限,甚至会进入我们难以预想的领域
4. 编码规范越使用越高效
尽早地使用编码规范,尽快地培养对代码风格的敏感度。
良好的习惯越早形成,我们的生活越轻松
05 | 经验总结: 如何给你的代码起好名字¶
一个好的命名规范是非常重要的,我们都能获得哪些好处:
1. 为标识符提供附加的信息,赋予标识符现实意义
帮助我们理顺编码的逻辑,减少阅读和理解代码的工作量
2. 使代码审核变得更有效率,专注于更重要的问题
而不是争论语法和命名规范这类小细节,提高开发效率
3. 提高代码的清晰度、可读性以及美观程度
4. 避免不同产品之间的命名冲突
几种常见的命名方法:
1. 驼峰命名法(CamelCase)
2. 蛇形命名法(snake_case)
3. 串式命名法(kebab-case)
4. 匈牙利命名法
标识符由一个或者多个小写字母开始,这些字母用来标识标识符的类型或者用途。
标识符的剩余部分,可以采取其他形式的命名法
已是历史遗留产物
取名字需要遵守三条原则:
1. 要有准确的意义
2. 严格遵守命名规范
3. 可读性优先
06 | 代码整理的关键逻辑和最佳案例¶
给代码分块:
1. 保持代码块的单一性,一个代码块只能有一个目标
2. 注意代码块的完整性
3. 代码块数量要适当
使用空白空间:
1. 同级别代码块靠左对齐
2. 同级别代码块空行分割
3. 下一级代码块向右缩进
4. 同行内代码块空格区隔
一行一个行为:
每一行代码仅仅表示一个行为。这样每一行的代码才是一个常规大小的、可以识别的基础信息块。
if (variable != null) variable.doSomething();
这行代码就包含了两个行为,一个是判断行为,一个是执行行为
基本的换行原则:
每行代码字符数的限制。
如果一行不足以容纳一个表达式,就需要换行;
一般的换行原则包括以下五点:
1. 在逗号后换行
String variable = anObject.getSomething(longExpressionOne,
longExpressionTwo, longExpressionThree);
2. 在操作符前换行
String varibale = longStringOne + longStringTwo
+ longStringThree;
3. 高级别的换行优先
anObject.methodOne(parameterForMethodOne,
anObject.methodTwo(parameterForMethodTwo));
// conventional indentation✅
int runningMiles = runningSpeedOne * runningTimeOne
+ runningSpeedTwo * runningTimeTwo;
// confusing indentation❌
int runningMiles = runningSpeedOne
* runningTimeOne + runningSpeedTwo
* runningTimeTwo;
4. 新的换行与上一行同级别表达式的开头对齐
anObject.methodOne(parameterOne,
parameterTwo,
parameterTwo);
5. 如果上述规则导致代码混乱或者代码太靠右,使用 8 个空格作为缩进(两个缩进单位)
anObject.methodOne(parameterForMethodOne,
anObject.methodTwo(parameterOneForMethodTwo,
parameterTwoForMethodTwo,
parameterThreeForMethodTwo));
// bad indentation❌
if ((conditionOne && conditionTwo)
|| (conditionThree && conditionFour)) {
doSomething();
}
// a better indentation, using 8 spaces for the indentation✅
if ((conditionOne && conditionTwo)
|| (conditionThree && conditionFour)) {
doSomething();
}
07 | 写好注释, 真的是小菜一碟吗¶
注释是无奈的妥协:
1. 注释无需运行,所以没有常规的办法来测试它。注释难以维护,这是它带来的最大的麻烦
2. 注释为我们提供了一个借口,有时会让代码更糟糕。
3. 注释的滥用
备注
注释是代码的一部分,是需要阅读的内容,目的是让其他人能更好地理解我们的代码,写注释需要我们有 “用户思维”。虽然也有过度依赖注释的情况,但是,对于大部分程序员来说,问题还是注释太少,而不是太多。
几种常见注释类型:
1. 记录源代码版权和授权的
2. 用来生成用户文档
3. 用来解释源代码的
备注
如果一段代码不再需要,我会清理掉代码,而不会保留这个注释掉的代码块。不要在源代码里记录代码历史,那是代码版本管理系统该干的事情。
注释的三项原则:
1. 准确,错误的注释比没有注释更糟糕
2. 必要,多余的注释浪费阅读者的时间
3. 清晰,混乱的注释会把代码搞得更乱
08 | 写好声明的 “八项纪律”¶
大原则主要有两个:
1. 取好名字
2. 容易识别
09 | 怎么用好 Java 注解¶
三个基本的实践:
1. 重写的方法,总是使用
2. 过时的接口,尽早废弃
3. 废弃的接口,不要使用
10 | 异常处理都有哪些陷阱¶
在 Java 语言里,异常状况分为三类:
1. 非正常异常(Error)
2. 运行时异常(RuntimeException)
3. 非运行时异常
三条准则:
1. 不要使用异常机制处理正常业务逻辑
2. 异常的使用要符合具体的场景
3. 具体的异常要在接口规范中声明和标记清楚
11 | 组织好代码段¶
代码文件头部结构:
1. 版权和许可声明
2. 命名空间(package)
3. 外部依赖(import)
代码文件对象结构:
1. 类的规范
2. 类的声明
3. 类的属性和方法
类的内部代码结构:
1. 类的属性
2. 构造方法
3. 工厂方法
4. 其他方法
方法的代码结构:
1. 方法的规范
2. 方法的声明
3. 方法的实现
一个典型的方法规范,应该包含以下十个部分:
1. 方法的简短介绍
2. 方法的详细介绍(可选项)
3. 规范的注意事项 (使用 apiNote 标签,可选项)
4. 实现方法的要求 (使用 implSpec 标签,可选项)
5. 实现的注意事项 (使用 implNote 标签,可选项)
6. 方法参数的描述
7. 返回值的描述
8. 抛出异常的描述:注意:抛出异常的描述部分,不仅要描述检查型异常,还要描述运行时异常
9. 参考接口索引(可选项)
10. 创始版本(可选项)
使用空行分割如下的代码块:
1. 版权和许可声明代码块
2. 命名空间代码块
3. 外部依赖代码
4. 类的代码块
5. 类的属性与方法之间
6. 类的方法之间
7. 方法实现的信息块之间
12 丨组织好代码文件¶
备注
如果你需要自己制定组织形式,我建议参考一些成功项目的组织方式。比如,如果你要做一个中间件,为客户提供类库,就可以参考 OpenJDK 的文件组织方式。
如果没有什么现成的项目可以参考借鉴的,请记住以下两点:
1. 文件的组织要一目了然,越直观,越有效率
2. 可维护性要优先考虑。这要求文件组织要层次分明,合理区隔、照应、使用不同的空间
13 | 接口规范¶
备注
接口规范是使用者和实现者之间的合约。
1. 区分外部接口和内部实现
外部接口,就是协作的界面,要简单规矩;内部实现,可以是千变万化的复杂小世界。
2. 接口规范是协作合约
合约的四个原则:成文、清楚、稳定、变更要谨慎
14 | 怎么写好用户指南¶
备注
最好的用户指南,是产品本身。用户指南,不能超越用户的理解能力和操作能力。
要建立下面的意识:
1. 从用户的角度出发来思考用户指南,用户指南要容易上手
2. 用户指南和源代码一样,也有开发周期,也是需要维护的
15 | 编写规范代码的检查清单¶
为什么需要编码规范:
1. 提高编码的效率
2. 提高编码的质量
3. 降低维护的成本
4. 扩大代码的影响
编码规范的心理因素¶
两种思维模式:
自主模式(快系统)和控制模式(慢系统) 自主模式的运行是无意识的、快速的、不怎么耗费脑力; 控制模式需要集中注意力,耗费脑力,判断缓慢,如果注意力分散,思考就会中断。 自主模式在熟悉的环境中是精确的,所作出的短期预测是准确的, 遇到挑战时,会第一时间做出反应 但它存在成见,容易把复杂问题简单化,在很多特定的情况下,容易犯系统性的错误。 比如说,第一印象、以貌取人,就是自主模式遗留的问题。 控制模式能够解决更复杂的问题。 但刻意掌控会损耗大脑能量,而且很辛苦。 处于控制模式中太长时间,人会很疲惫,丧失一部分动力,也就不愿启动控制模式了 比如,很多人害怕数学,多是因为控制模式确实很吃力。 自主模式和控制模式的分工合作是高效的,损耗最小,效果最好。 快速的、习惯性的决断交给勤快省力的自主模式, 复杂的、意外的决断由耗时耗力的控制模式接管。 编码规范中很大一部分内容是: 增加共识、减少意外,扩大自主思维模式覆盖的范围, 减少控制模式必须参与的内容。 熟练掌握编码规范可以逐渐让这一套规则存在于人们的下意识中,简单又自然。
识别模式:
程序员很容易理解和自己编码风格类似的代码。 如果编码风格和自己的大相径庭,就会感到焦躁和烦恼。 编码风格一旦形成,就难以更改,转变很痛苦。 幸运的是,一旦努力转换成新习惯,我们就会喜欢上新的编码风格。 一份好的编码规范,刚开始接受会有些困难。我们甚至会找很多借口去拒绝。 但是,一旦接受下来,我们就成功地改进了我们的识别模式。
猜测模式:
对于既定模式的识别,是通过猜测进行的 在编写代码时,我们要有意识地提供足够的线索和背景,使用清晰的结构,加快模式的识别, 避免造成模式匹配过程中的模糊和混淆带来的理解障碍。
记忆模式:
我们的记忆模式有四种,包括感官、短期、工作和长期记忆。 a. 感官记忆是对感官体验的记忆,非常短暂(大约三秒钟),比如我们刚刚看到的和听到的。 b. 短期记忆是我们可以回忆的,刚刚接触到的信息的短暂记忆。 短期记忆很快,但是很不稳定,并且容量有限。 如果中途分心,即便只是片刻,我们也容易忘记短期记忆的内容。 c. 工作记忆是我们在处理认知任务时,对信息进行短暂存贮并且执行操作的记忆。 工作记忆将短期记忆和长期记忆结合起来,处理想法和计划,帮助我们做出决策。 d. 长期记忆涵盖的记忆范围从几天到几十年不等。 为了成功学习,信息必须从感官或短期记忆转移到长期记忆中。 和短期记忆相比,长期记忆记忆缓慢,但是保持长久,并且具有近乎无限的容量。 我们在组织代码时,不要让短期记忆超载,要使用短小的信息快,方便阅读; 要适当分割需要长期记忆和短期记忆的内容, 比如接口规范和代码实现,帮助读者在工作记忆和长期记忆中组织和归档信息。
眼睛的运动:
当我们看一样东西的时候,我们不是一下子就能看清它的全貌。 事实上,我们的眼睛一次只能专注于一个很小的区域,忽视该区域以外的内容。 有时候,我们需要反复研读一段代码。 如果这段代码可以在一个页面显示,我们的眼睛就很容易反复移动,寻找需要聚焦的目标。 如果这段代码跨几个页面,阅读分析就要费力得多。 当我们阅读时,我们的眼睛习惯从左到右,从上到下移动, 所以靠左的信息更容易被接受,而靠右的信息更容易被忽略。 当快速阅读或者浏览特定内容时(比如搜索特定变量),眼睛就会只喜欢上下移动,迅速跳过。 聚焦区域小,眼睛倾向于上下移动, 这就是报纸版面使用窄的版面分割,而不是整幅页面的原因之一。 在编码排版时,要清晰分块,保持布局明朗,限制每行的长度,这样可以方便眼睛的聚焦和浏览。
参考清单:
代码是按照编码指南编写的吗?
代码能够按照预期工作吗?
文件是不是在合适的位置?
支撑文档是不是充分?
代码是不是易于阅读、易于理解?
代码是不是易于测试和调试?
有没有充分的测试,覆盖关键的逻辑和负面清单?
名字是否遵守命名规范?
名字是不是拼写正确、简单易懂?
名字是不是有准确的意义?
代码的分块是否恰当?
代码的缩进是否清晰、整洁?
有没有代码超出了每行字数的限制?
代码的换行有没有引起混淆?
每一行代码是不是只有一个行为?
变量的声明是不是容易检索和识别?
变量的初始化有没有遗漏?
括号的使用是不是一致、清晰?
源代码的组织结构是不是一致?
版权信息的日期有没有变更成最近修改日期?
限定词的使用是不是遵循既定的顺序?
有没有注释掉的代码?
有没有执行不到的代码?
有没有可以复用的冗余代码?
复杂的表达式能不能拆解成简单的代码块?
代码有没有充分的注释?
注释是不是准确、必要、清晰?
不同类型的注释内容,注释的风格是不是统一?
有没有使用废弃的接口?
能不能替换掉废弃的接口?
不再推荐使用的接口,是否可以今早废弃?
继承的方法,有没有使用 Override 注解?
有没有使用异常机制处理正常的业务逻辑?
异常类的使用是不是准确?
异常的描述是不是清晰?
是不是需要转换异常的场景?
转换异常场景,是不是需要保留原异常信息?
有没有不应该被吞噬的异常?
外部接口和内部实现有没有区分隔离?
接口规范描述是不是准确、清晰?
接口规范有没有描述返回值?
接口规范有没有描述运行时异常?
接口规范有没有描述检查型异常?
接口规范有没有描述指定参数范围?
接口规范有没有描述边界条件?
接口规范有没有描述极端状况?
接口规范的起草或者变更有没有通过审阅?
接口规范需不需要标明起始版本号?
产品设计是不是方便用户使用?
用户指南能不能快速上手?
用户指南的示例是不是可操作?
用户指南和软件代码是不是保持一致?
16 丨代码 “规范” 篇用户答疑¶
InfoQ 有一篇文章《回归测试策略概览》要想发挥回归测试的最大作用,要把回归测试自动化。只需要简单的操作,就可以启动回归测试。比如使用 “make test” 命令行,或者其他集成工具的触发配置。这样,我们做的每一个更改,哪怕只是修改了一行代码,都可以跑一遍回归测试。
安全相关:
1. Java 平台安全(platform security, Java language)
2. 密码学 (Cryptography,JCA)
3. 认证和授权(Authentication and Access Control,JAAS)
4. 安全通信(Secure Communications,JSSE/JGSS/SASL)
5. 公开密钥基础设施(Public Key Infrastructure (PKI))
02第二模块: 代码 “经济” 篇 (14 讲)¶
17 | 为什么需要经济的代码¶
越早考虑性能问题,需要支付的成本就越小,带来的价值就越大,不要等到出现性能问题时,才去临时抱佛脚。另外,性能问题,大部分都是意识问题和见识问题。想得多了,见得多了,用得多了,技术就只是个选择的问题,不一定会增加我们的编码难度和成本。
备注
扩展硬件并不是总能够线性地提高系统的性能。出现性能问题,投入更多的设备,只是提高软件性能的一个特殊方法。而且,这不是一个廉价的方法。过去的经验告诉我们,提高一倍的性能,硬件投入成本高达四五倍;如果需要提高四五倍的性能,可能投入二三十倍的硬件也达不到预期的效果。硬件和性能的非线性关系,反而让代码的性能优化更有价值。
采用 性能工程 思维:
1. 架构师知道他们设计的架构支持哪些性能的要求;
2. 开发工程师清楚应该使用的基本技术,而不是选择性地忽略掉性能问题;
3. 项目管理人员能够在开发软件过程中跟踪性能状态;
4. 性能测试专家有时间进行负载和压力测试,而不会遇到重大意外。
实现性能要求的风险在流程早期得到确认和解决,
这样就能节省时间和金钱,减轻在预算范围内按时交付的压力。
现在很多公司的研发,完美地匹配了敏捷开发和性能工程这两种模式。降低研发成本的同时,也促进了员工的成长,减轻了程序员的压力。
18 丨思考框架: 什么样的代码才是高效的代码¶
用户的真实感受¶
1. 等待时间要短¶
应用程序性能指数(Apdex)。 根据任务的响应时间,应用程序性能指数定义了三个用户满意度的区间: 满意:如果任务的响应时间小于 T,用户感觉不到明显的阻碍,就会比较满意; 容忍:如果任务的响应时间大于 T,但是小于 F,用户能感觉到性能障碍,但是能够忍受,愿意等待任务的完成; 挫败:如果任务的响应时间大于 F 或者失败,用户就不会接受这样的等待。挫败感会导致用户放弃该任务。
备注
在互联网领域,最佳等待时间(T)和最大可容忍等待时间(F)的选择有着非常经典的经验值,那就是最佳等待时间是 2 秒以内,最大可容忍等待时间是最佳等待时间的 4 倍,也就是 8 秒以内。
应用程序性能指数可以按照下属的公式计算:
Apdex = (1 × 满意样本数 + 0.5 × 容忍样本数 + 0 × 挫败样本数) / 样本总数
假如有一个应用,100 个样本里,有 70 个任务的等待时间在 2 秒以内,20 个任务的等待时间大于 2 秒小于 8 秒,10 个任务的等待时间大于 8 秒。那么,这个指数的就是 80%。 Apdex = (1 × 70 + 0.5 × 20 + 0 × 10) / 100 = 0.8 通常来说,80 分的成绩还算过得去,90 分以上才能算是好成绩。
有了这个指数,我们就知道快是指多块,慢是指多慢;什么是满意,什么是不满意。这样我们就可以量化软件性能这个指标了,可以给软件性能测试、评级了。
体验要一致¶
备注
一致性原则是一个非常基本的产品设计原则,它同样也适用于性能的设计和体验。一个服务,如果 10 次访问有 2 次不满意,用户就很难对这个服务有一个很高的评价。
有一个服务在一年 12 个月的时间里,有 11 个月的服务都特别流畅,人人都很满意。但是有半个月,网站经常崩溃或者处于崩溃的边缘,平常需要 2 秒就搞定的服务,此时需要不停地刷屏排队,甚至 30 分钟都完成不了。但这项服务特别重要,没有可替代的,不能转身走开,只好隔几秒就刷一次屏。自动刷屏软件出现,1 千万个人的活动,制造出了 100 亿个人的效果。
代码的资源消耗¶
管理好计算机资源主要包括两个方面,一个方面是把有限的资源使用得更有效率,另一个方面是能够使用好更多的资源。
1. 把资源使用得更有效率¶
有时候我们说效率的时候,其实我们说的是分配。计算机资源的使用,也是一个策略。不同的计算场景,需要匹配不同的策略。只有这样,才能最大限度地发挥计算机的整体的计算能力,甚至整个互联网的计算能力。
2. 能够使用好更多的资源¶
不是所有的应用程序设计都能够用好更多的资源。这是我们在架构设计时,就需要认真考量的问题。
算法的复杂程度¶
如果给定了计算机资源,比如给定了内存,给定了 CPU,我们该怎么去衡量这些资源的使用效率?一个最重要、最常用、最直观的指标就是算法复杂度。对于计算机运算,算法复杂度又分为时间复杂度和空间复杂度。我们可以使用两个复杂度,来衡量 CPU 和内存的使用效率。
19 | 怎么避免过度设计¶
软件开发过程中,最让人痛苦的是“频繁的需求变更”
避免需求膨胀¶
为了限制无节制的需求变更,适应合理的需求进化,我们要使用两个工具,一个工具是识别最核心需求,另一个工具是迭代演进。
识别最核心需求¶
首先就必须满足的需求,是优先级最高的、最重要的事情,这些事情要小而精致,是我们的时间、金钱、智力投入效率最高的地方,也是回报最丰厚的地方。我们要把这些事情做到让竞争对手望尘莫及的地步。
迭代演进¶
迭代演进不仅仅需要考虑上一次没有完成的事情,还要考虑变化促生的新需求。所以,在这一步,还要像第一次一样,先找到最小的子集,也就是现在就必须满足的需求。然后,全力以赴地做好它。
备注
永远不要考虑不重要的需求。有时候,遏制住添加新功能、新接口的渴望,是一个困难的事情。我们需要学会放手,学会休假,以及拥有空闲时间。管理好需求,是提高我们的工作效率以及软件效率最有效路径。
避免过度设计¶
避免过度设计,和避免需求膨胀一样,我们要时刻准备提问和回答的两个问题:什么是必须做的?什么是现在就必须做的?
20 | 简单和直观, 是永恒的解决方案¶
为什么需要简单直观¶
简单直观是快速行动的唯一办法:编写简单直观的代码只是我们为了快速行动而不得不采取的手段。
简单直观减轻沟通成本
编写简单直观的代码只是我们为了快速行动而不得不采取的手段。
备注
真正能够使得产品获得成功,甚至扭转科技公司命运的,不是关键时刻能够救火的队员,而是从一开始就消除了火灾隐患的队员。
怎么做到简单直观¶
使用小的代码块:为了保持代码块的简单,给代码分块的一个重要原则就是,一个代码块只做一件事情。
遵守约定的惯例
花时间做设计
借助有效的工具
有的程序员,喜欢拿到一个问题,就开始写代码,通过代码的不断迭代、不断修复来整理思路,完成设计和实现。这种方法的问题是,他们通常非常珍惜自己的劳动成果,一旦有了成型的代码,就会像爱护孩子一般爱护它,不太愿意接受新的建议,更不愿意接受大幅度的修改。结果往往是,补丁摞补丁,代码难看又难懂。
有的程序员,喜欢花时间拆解问题,只有问题拆解清楚了,才开始写代码。这种方法的问题是,没有代码的帮助,我们很难把问题真正地拆解清楚。这样的方法,有时候会导致预料之外的、严重的架构缺陷。
大部分的优秀的程序员,是这两个风格某种程度的折中,早拆解、早验证,边拆解、边验证,就像剥洋葱一样。
拆解和验证的确很耗费时间。但是,如果我们从整个软件的开发时间来看,这种方式也是最节省时间的。如果拆解和验证做得好,代码的逻辑就会很清晰,层次会很清楚,缺陷也少。
备注
一个优秀的程序员,可能 80% 的时间是在设计、拆解和验证,只有 20% 的时间是在写代码。
21 | 怎么设计一个简单又直观的接口¶
备注
接口设计的困境,大多数来自于接口的稳定性要求。摆脱困境的有效办法不是太多,其中最有效的一个方法就是要保持接口的简单直观。
最基本的原则:
1. 从真实问题开始,把大问题逐层分解为 “相互独立,完全穷尽” 的小问题
2. 问题的分解过程,对应的就是软件的接口以及接口之间的联系
3. 一个接口,应该只做一件事情。如果做不到,接口间的依赖关系要描述清楚
备注
注意分解的问题一定要 “相互独立,完全穷尽”(Mutually Exclusive and Collectively Exhaustive)。这就是 MECE 原则。只有完全穷尽,才能把问题解决掉。否则,这个解决方案就是有漏洞的,甚至是无效的。只有相互独立,才能让解决方案简单。否则,不同的因素纠缠在一起,既容易导致思维混乱,也容易导致不必要的复杂。
从问题开始,是为了让我们能够找到一条主线。然后,围绕这条主线,去寻找解决问题的办法,而不是没有目标地让思维发散。这样,也可以避免需求膨胀和过度设计。
一个接口一件事情(对于一件事的划分,我们要注意三点):
1. 一件事就是一件事,不是两件事,也不是三件事
2. 这件事是独立的
3. 这件事是完整的
备注
“流水的营盘,铁打的将”.有研究表明,替换一个工程师,需要花费平均 6 到 9 个月的薪水,甚至是 1.5 到 2 年的薪水。
22 丨高效率, 从超越线程同步开始¶
线程的同步是学习一门编程语言的难点。刚开始线程同步的困难,主要在于了解技术;跨过了基本技术的门槛后,更难的是掌握最基本的概念。
备注
掌握好基本概念,几乎是我们学习所有技术背后的困境。
线程的并发执行和共享进程资源,是为了提高效率。可是线程间如何管理共享资源的变化,却是一个棘手的问题,甚至是一个损害效率的问题。
需要同步的场景,要同时满足三个条件:
1. 使用两个以上的线程
2. 关心共享资源的变化
3. 改变共享资源的行为
避免线程同步 的方法:打破上面其中的任何一个条件:
1. 使用单线程
2. 不关心共享资源的变化
3. 没有改变共享资源的行为
减少线程同步时间
在阻塞的这段时间里,做的事情越少,阻塞时间一般就会越短。
这是一个很值得花费时间去琢磨的地方。微小改进,效率就提高多倍。
23 | 怎么减少内存使用, 减轻内存管理负担¶
随着大数据、云计算以及物联网的不断演进,很多技术都面临着巨大的挑战。2010 年左右能解决 C10K(同时处理 1 万个用户连接)问题,感觉就可以高枕无忧了。现在有不少应用,需要开始考虑 C10M(同时处理 1 千万个用户连接)问题,甚至是更多的用户连接,以便满足用户需求。很多以前不用担心的问题,也会冒出来算旧账。
减少内存的使用,办法有且只有两个:
第一个办法是减少实例的数量。
第二个办法是减小实例的尺寸。
一个资源,如果不需要维护,那就太理想了。有两类理想的共享资源:
一类是一成不变(immutable)的资源,
一类是禁止修改(unmodifiable)的资源。
24 | 黑白灰, 理解延迟分配的两面性¶
减少内存使用的两个大方向,减少实例数量和减少实例的尺寸。如果我们把时间的因素考虑在内,还有一些重要的技术,可以用来减少运行时的实例数量。其中,延迟分配是一个重要的思路。
延迟分配的方案就是说:不到需要时候,不占用不必要的资源。
25 | 使用有序的代码, 调动异步的事件¶
同步和异步,是两个差距很大的编程模型。同步,就是很多事情一步一步地做,做完上一件,才能做下一件。异步,就是做事情不需要一步一步的,多件事情,可以独立地做。
异步的实现,依赖于底层的硬件和操作系统;如果操作系统不支持,异步也可以通过线程来模拟。
使用异步 I/O,每一个 CPU 分派一个线程就足以应付所有的连接。这时候,连接的效率就主要取决于硬件和操作系统的能力了。
26 | 有哪些招惹麻烦的性能陷阱¶
内存的泄露
未关闭的资源
27 | 怎么编写可伸缩性的代码¶
可伸缩性(Scalability)
可扩展性(Extensibility)
具有规模扩张能力的软件的一些最佳实践:
1. 把无状态数据分离出来,单独提供无状态服务
2. 把最基本的服务状态封装起来,利用客户端的缓存,实现无状态服务
3. 小心使用服务状态,编码时要考虑服务状态的规模水平扩张能力
28 | 怎么尽量 “不写” 代码¶
代码复用的一些基本概念。关键的有三点:
1. 要提高代码的复用比例,减少编码的绝对数量
2. 要复用外部的优质接口,并且推动它们的改进
3. 烂代码该放手时就放手,以免引起不必要的兼容问题
29 | 编写经济代码的检查清单¶
经济的代码的意义:
1. 提升用户体验
2. 降低研发成本
3. 降低运营成本
4. 防范可用性攻击
怎么编写经济的代码:
1. 避免过度设计
2. 选择简单直观
3. 超越线程同步
4. 减少内存使用
5. 规避性能陷阱
6. 规模扩张能力
经济代码的检查清单¶
需求评审:
1. 需求是真实的客户需求吗?
2. 要解决的问题真实存在吗?
3. 需求具有普遍的意义吗?
4. 这个需求到底有多重要?
5. 需求能不能分解、简化?
6. 需求的最小要求是什么?
7. 这个需求能不能在下一个版本再实现?
设计评审:
1. 能使用现存的接口吗?
2. 设计是不是简单、直观?
3. 一个接口是不是只表示一件事情?
4. 接口之间的依赖关系是不是明确?
5. 接口的调用方式是不是方便、皮实?
6. 接口的实现可以做到不可变吗?
7. 接口是多线程安全的吗?
8. 可以使用异步编程吗?
9. 接口需不需要频繁地拷贝数据?
10. 无状态数据和有状态数据需不需要分离?
11. 有状态数据的处理是否支持规模水平扩张?
代码评审:
1. 有没有可以重用的代码?
2. 新的代码是不是可以重用?
3. 有没有使用不必要的实例?
4. 原始数据类的使用是否恰当?
5. 集合的操作是不是多线程安全?
6. 集合是不是可以禁止修改?
7. 实例的尺寸还有改进的空间吗?
8. 需要使用延迟分配方案吗?
9. 线程同步是不是必须的?
10. 线程同步的阻塞时间可以更短吗?
11. 多状态同步会不会引起死锁?
12. 是不是可以避免频繁的对象创建、销毁?
13. 是不是可以减少内存的分配、拷贝和释放频率?
14. 静态的集合是否会造成内存泄漏?
15. 长时间的缓存能不能及时清理?
16. 系统的资源能不能安全地释放?
17. 依赖哈希值的集合,储存的对象有没有实现 hashCode () 和 equals () 方法?
18. hashCode () 的实现,会不会产生撞车的哈希值?
19. 代码的清理,有没有变更代码的逻辑?
30 丨 “代码经济篇” 答疑汇总¶
对性能和资源消耗有一定的意识其实就成功一大半了,“要有意识” 是我们首先要获得的能力。有了意识,我们就会去寻找技术,寻找工具,寻找解决的办法。
03第三模块: 代码 “安全” 篇 (14 讲)¶
31 | 为什么安全的代码这么重要¶
Equifax 的教训给我们带来三点启示:
1. 不起眼的代码问题,也可以造成巨大的破坏
2. 安全修复版本,一定要第一时间更新
3. 安全漏洞的破坏性,我们很难预料,每个人都可能是安全漏洞的受害者
启示:
1. 不起眼的小问题,也会有巨大的安全缺陷,造成难以估量的损失
2. 编写安全的代码,是我们必须要掌握的基础技能
3. 安全问题,既是技术问题,也是管理问题
过期版本一定要尽快升级:
刚刚暴露的漏洞,业界往往称为 1day 漏洞。 这种漏洞如果是高危的话会给公司造成极大的损失。 比如方面的 openssl 心脏滴血,造成无数网站的敏感信息泄露。
如何获取安全漏洞信息和安全更新信息:
依赖安全人员及时从各大媒体,安全厂商,主流厂商的官网和美国国家漏洞库搜集 cve 漏洞。
安全更新的策略:
一旦发现最新漏洞,往往依赖我们安全人员发布漏洞紧急预警,业界称为 sirt, 然后通过内部预警系统下发整个公司,每个产品会有相应的产品安全接口人, 及时反馈相应影响范围和修复情况。
32 | 如何评估代码的安全缺陷¶
三个问题:
1. 有什么事情是你必须要做的?
2. 哪些事情是只有你能做的?
3. 哪些事情是别人可以帮你做的?
隐含的意思是:
1. 识别并且选择最重要的事情
2. 确定自己最擅长的事情,全力以赴地做好
3. 选择你的帮手,充分信任并授权
缺陷¶
软件缺陷的定义方式和衡量方式有很多种。从用户感受的角度出发,定义和计量软件缺陷,是其中一个比较好的、常用的软件缺陷评估体系。我个人比较倾向一种观点,软件缺陷的严重程度应该和用户的痛苦程度成正比。
从用户感受出发,衡量软件缺陷有两个最常用的指标:
1. 缺陷影响的深度
软件缺陷带来的问题的严重性:软件缺陷导致的最严重的问题是什么
2. 缺陷影响的广度
软件缺陷带来问题的可能性:软件缺陷导致问题出现的频率是多大
软件缺陷的优先等级:
1. 高优先级 (P1): 高严重性、高可能性
2. 中优先级 (P2): 高严重性、低可能性; 低严重性、高可能性
3. 低优先级 (P3): 低严重性、低可能性
备注
软件缺陷优先等级的定义是为了帮助我们更好地解用户的感受程度,以及安排时间和处理事情。
安全¶
备注
由于编写安全代码本身的挑战性,以及消除安全漏洞的复杂性,业界通常需要进行大范围的合作,以便准确、快速、周全地解决安全缺陷问题。大规模协作需要标准的描述语言,以及对安全问题的准确认知。通用缺陷评分系统(CVSS)就是一种评判安全缺陷优先等级的标准。
对于安全缺陷的严重性,有四个互相独立的测量维度(量度):
1. 对私密性的影响(Confidentiality)
2. 对完整性的影响(Integrity)
3. 对可用性的影响(Availability)
4. 对授权范围的影响(Authorization Scope)
对于安全缺陷的可能性,有四个互相独立的测量维度(量度):
1. 安全攻击的路径(Attack Vector)
2. 安全攻击的复杂度(Attack Complexity)
3. 安全攻击需要的授权(Privileges Required)
4. 安全攻击是否需要用户参与(User Interaction)
33 | 整数的运算有哪些安全威胁¶
整数溢出的问题,曾经在 1995 年导致火箭的坠落; 在 2016 年导致错误地签发了四千多万美元(最高限额原为 1 万美元)的博彩奖券。还有绵延不绝的,你我知道抑或不知道的软件安全漏洞。
34 | 数组和集合, 可变量的安全陷阱¶
1. 可变量的传递,存在竞态危害的安全风险
2. 可变量局部化,是解决可变量竞态危害的一种常用办法
3. 变量的拷贝,有浅拷贝和深拷贝两种形式;可变量的浅拷贝无法解决竞态危害的威胁
35 | 怎么处理敏感信息¶
什么是敏感信息¶
私密性、完整性以及可用性是信息安全的三要素。其中,私密性指的是数据未经授权,不得访问,解决的是 “谁能看” 的问题。在这个框架下,我们可以把敏感信息理解为,未经授权不得泄漏的信息。反过来说,未经授权不得泄漏的信息,都算是敏感信息。
个人敏感信息:
a. 个人信息:姓名、性别、年龄、身份证号码、电话号码 b. 健康信息:健康记录、服药记录、健康状况 c. 教育记录:教育经历、学习课程、考试分数 d. 消费记录:所购货物、购买记录、支付记录 e. 账户信息:信用卡号、社交账号、支付记录 f. 隐私信息:家庭成员、个人照片、个人行程
商业敏感信息:
a. 商业秘密:设计程序、制作工艺、战略规划、商业模式 b. 客户信息:客户基本信息、消费记录、订单信息、商业合作和合同 c. 雇员信息:雇员基本信息、工资报酬
是否需要授权是敏感信息和普通信息的最关键差异。不同的人有不同的权限,不同的操作需要不同的权限:
a. 定义权限
b. 定义权限的主体
c. 定义权限的归属
总结¶
1. 要建立主动保护敏感信息的意识
2. 要识别系统的敏感信息,并且对敏感信息采取必要的、特殊的处理
36 | 继承有什么安全缺陷¶
变更一个可扩展类时,要极其谨慎小心,一个类如果可以不变更,就尽量不要变更。一个可扩展的类,子类和父类可能会相互影响,从而导致不可预知的行为。
当我们扩展一个类时,如涉及敏感信息的授权与保护,可以考虑使用代理的模式,而不是继承的模式
37 | 边界, 信任的分水岭¶
边界是信息安全里一个重要的概念。如果不能清晰地界定信任的边界,并且有效地守护好这个边界,那么编写安全的代码几乎就是一项不可能完成的任务。
备注
跨界的数据不可信任。无法识别来源的数据,不应该是可信任的数据。区分内部数据、外部数据的依据,就是数据的最原始来源,而不是数据在代码中的位置。
1. 外部输入数据,需要检查数据的合法性
2. 公开接口的输入和输出数据,还要考虑可变量的传递带来的危害
38 | 对象序列化的危害有多大¶
远程调用:
1. 远程过程调用(RPC)
2. 远程方法调用(RMI)
3. 分布式对象(Distributed Object)
4. 组件对象模型(COM)
5. 公共对象请求代理(CORBA)
6. 简单对象访问协议(SOAP)
备注
打包、传输、拆解是序列化技术的三个关键步骤。
序列化后的每一个环节,都有可能遭受潜在的攻击,大约有一半的 Java 漏洞和序列化技术有直接或者间接的关系。不要惦记序列化的好处,坚持不要使用序列化。
39 | 怎么控制好代码的权力¶
最小授权:
1. 最小权力的设计
2. 最小限度的授予
40 | 规范, 代码长治久安的基础¶
用户越广泛,部署越广泛,升级就越困难,安全变更面临的挑战就越大。芝麻蒜皮的小问题,都可能构筑困难的障碍,带来巨大的风险,从而造成严重的损失。
备注
代码安全管理的三个策略:代码规范、风险预案和纵深防御。
41 | 预案, 代码的主动风险管理¶
备注
世事无常,一个好的设计,需要有双引擎和降落伞。
2011 年 9 月 25 日,BEAST 攻击技术公开发表,针对 CBC 模式,链式加密模式不再安全了。然后发现,TLS 1.0 的设计真是周到,居然还有一个流加密技术可以使用,而且 RC4 算法被广泛支持。这真是一个可以救命的设计。但是,好景并不长。2013 年 3 月 13 日,一个研究小组公开了一个关于 RC4 算法的严重的安全漏洞。不同寻常的是,这一次并没有合适的修改 RC4 算法的补救措施。该研究小组建议,停止使用 RC4,TLS 1.0 和 1.1 版本的用户应该转化到 CBC 模式的加密算法。这算是一个不小的玩笑,很多应用刚从 CBC 模式切换到 RC4 算法不久,就要重新调整,再切换回去。
备注
双引擎不是备份计划,不是应急计划,不是 Plan B,两个引擎日常都要使用。如果其中一个引擎闲置,那么当真正需要它的时候,我们就不知道它的状态如何,是否可以承担重任。
备注
我们总是尽最大的可能使得软件程序简化、简化再简化。可是对于生死攸关的风险点,我们有时需要选择相反的方向,强化、强化再强化。不是所有的复杂都是必要的,也不是所有的复杂都是不必要的。软件的设计,是一个需要反复权衡、反复妥协的艺术。
42 | 纵深, 代码安全的深度防御¶
高效率,是一个让人不懈追求的目标。为了高效率,对于大部分数据的释放,我们可以采取撒手不管的策略;为了安全,对于小部分敏感数据的释放,我们需要采取非常保守的策略。敏感数据归零,就是其中的一个保守策略。。敏感数据归零是纵深防御体系中,非常具有深度的一个防线,但并不是唯一的防线。
每一道防线都考虑决策、执行、监督这三项权力的分配,以及计划、执行、检查、纠正这四项操作的实际执行。如果信息系统防护的多样性之间不能独立,多样性的防护实际上可能会产生多样性的漏洞。
1. 没有防御纵深的信息系统,其安全性是堪忧的
2. 一个防御体系,需要考虑纵深和多样性,更需要确保防御体系良性运转
43 | 编写安全代码的最佳实践清单¶
为什么需要安全的代码:
1. 代码质量是信息安全的基础
2. 安全漏洞的破坏性难以预料
3. 安全编码的规则可以学得到
编写安全代码的基本原则:
1. 清楚调用接口的行为
2. 跨界的数据不可信任
3. 最小授权的原则
4. 减小安全攻击面
5. 深度防御的原则
安全代码的检查清单¶
安全管理:
1. 有没有安全更新的策略和落实计划?
2. 有没有安全漏洞的保密共识和规范?
3. 有没有安全缺陷的评估和管理办法?
4. 软件是不是使用最新的安全修复版?
5. 有没有定义、归类和保护敏感信息?
6. 有没有部署多层次的安全防御体系?
7. 安全防御能不能运转良好、及时反应?
8. 不同的安全防御机制能不能独立运转?
9. 系统管理、运营人员的授权是否恰当?
10. 有没有风险管理的预案和长短期措施?
代码评审:
数值运算会不会溢出?
有没有检查数值的合理范围?
类、接口的设计,能不能不使用可变量?
一个类支持的是深拷贝还是浅拷贝?
一个接口的实现,有没有拷贝可变的传入参数?
一个接口的实现,可变的返回值有没有竞态危害?
接口的使用有没有严格遵守接口规范?
哪些信息是敏感信息?
谁有权限获取相应的敏感信息?
有没有定义敏感信息的授权方案?
授予的权限还能不能更少?
特权代码能不能更短小、更简单?
异常信息里有没有敏感信息?
应用日志里有没有敏感信息?
对象序列化有没有排除敏感信息?
高度敏感信息的存储有没有特殊处理?
敏感信息的使用有没有及时清零?
一个类,有没有真实的可扩展需求,能不能使用 final 修饰符?
一个变量,能不能对象构造时就完成赋值,能不能使用 final 修饰符?
一个方法,子类有没有重写的必要性,能不能使用 final 修饰符?
一个集合形式的变量,是不是可以使用不可修改的集合?
一个方法的返回值,能不能使用不可修改的变量?
类、方法、变量能不能使用 private 修饰符?
类库有没有使用模块化技术?
模块设计能不能分割内部实现和外部接口?
有没有定义清楚内部数据、外部数据的边界?
外部数据,有没有尽早地完成校验?
有没有标示清楚外部数据的校验点?
能不能跟踪未校验外部数据的传送路径?
有没有遗漏的未校验外部数据?
公开接口的输入,有没有考虑数据的有效性?
公开接口的可变化输出,接口内部行为有没有影响?
有没有完成无法识别来源的数据的校验?
能不能不使用序列化技术?
序列化的使用场景,有没有足够的安全保障?
软件还存在什么样风险?
有没有记录潜在的风险问题?
有没有消除潜在风险的长期预案?
有没有消除潜在风险的短期措施?
潜在的风险问题如果出现,能不能快速地诊断、定位、修复?
小结¶
代码不规范和效率不高,业务也可以运转,然后慢慢优化,逐渐演进。但代码一旦出现安全问题,遭受攻击,损失立即就会反映出来,而且破坏性极大。
代码不规范,看的人立刻就会觉得很难受。代码的效率不高,业务运转不通畅,同样会有及时的反馈。就代码的安全层面来说,一般情况下直到攻击发生之前,我们可能都不知道代码是否存在安全问题。等到攻击真实发生的时候,损失已经成为事实了。
代码的规范原则,是一个相对容易掌握的内容。高效的代码,也有很多成熟的经验可以学习。可是,代码的安全,却是一个攻易守难的问题。哪怕我们今天知道了所有的攻击和防护方法(这当然不可能),如果明天出现了一种新的攻击手段,而且全世界只有一个人知道,我们的系统都存在潜在的安全威胁。
掌握安全编码的技术,熟练修复软件漏洞的实践,需要先过三道关:
第一道关,是意识(Conscious)。也就是说,要意识到安全问题的重要性,以及意识到有哪些潜在的安全威胁。
第二道关,是知晓(Awareness)。要知道软件有没有安全问题,安全问题有多严重。
第三道关,是看到(Visible)。要了解是什么样的问题导致了安全漏洞,该怎么修复安全漏洞。
重要
最重要的资源是 NIST 的 安全漏洞数据库 <https://nvd.nist.gov/>
参考¶
陌陌安全开源的他们 Java、PHP 的安全编码规范及 SDK: * Java 安全 SDK 及编码规范:https://github.com/momosecurity/rhizobia_J * PHP 安全 SDK 及编码规范:https://github.com/momosecurity/rhizobia_P
44 | “代码安全篇” 答疑汇总¶
重要
要学习一点难的东西,这样才能走到更远的地方。
第一个建议是: 记住最基本的安全编码原理
第二个建议是: 学习编程语言的编码规范以及安全编码指南
第三个建议是: 跟踪、学习、使用最新的安全编码进展
备注
只有持续地了解、积累、训练,才能慢慢地到达一个期望的水准,才能建立、巩固自己的技术优势。