代码整洁之道
军规: 让营地比你来时更干净.
整洁代码
Leblanc : Later equals never.
(勒布朗法则: 稍后等于永不)
对代码的每次修改都影响到其他两三处代码.
修改无小事.
如同医生不能遵从病人的意愿, 程序员遵从不了解混乱风险的经理的意愿, 也是不专业的做法.
赶上期限的唯一方法, 做的快的唯一方法, 就是始终尽可能保持代码整洁.
破窗理论: 环境中的不良现象如果被放任存在, 会诱使人们仿效, 甚至变本加厉.== 一幢有少许破窗的建筑为例, 如果那些窗不被修理好, 可能将会有破坏者破坏更多的窗户. 最终他们甚至会闯入建筑内, 如果发现无人居住, 也许就在那里定居或者纵火. 一面墙, 如果出现一些涂鸦没有被清洗掉, 很快的, 墙上就布满了乱七八糟, 不堪入目的东西; 一条人行道有些许纸屑, 不久后就会有更多垃圾, 最终人们会视若理所当然地将垃圾顺手丢弃在地上 ==. 这个现象, 就是犯罪心理学中的破窗效应. 结论: 及时矫正和补救正在发生的问题. 启发: 影响的大小并不能改变行为错误的本质, 别人的错误更不会是证明你无错的理由.
整洁的代码只做好一件事: 整洁的代码力求集中. 每个函数, 每个类和每个模块都全神贯注于一事, 完全不受四周细节的干扰和污染.
在意代码.
简单代码: 能通过所有测试; 没有重复代码; 体现系统中的全部设计理念; 包括尽量少的实体, 比如类, 方法, 函数等.
不要重复代码, 只做一件事, 表达力, 小规模抽象.
不读周边代码的话就没法写代码. 编写代码的难度, 取决于读周边代码的难度.
军规: 让营地比你来时更干净.
练习, 练习, 练习
有意义的命名
名副其实: 如果名称需要注释来补充, 那就不算是名副其实.
避免误导: 慎用 - List; 名称区分度要大; 避免 0,1,l,o,O.
做有意义的区分: 以数字系列命名 (a1,a2...aN) 纯属误导. 废话都是冗余,-Data,-Info 等都属于冗余信息.
使用读得出来的名称
使用可搜索的名称: 名称长短应与其作用域大小相对应.
避免使用编码: 不必包含类型, 不必使用前缀 / 后缀
避免思维映射: 明确是王道.
类名应该是名词或名词短语
方法名应该是动词或动词短语(使用方法来初始化对象通常好于直接只用构造器, 所以建议将构造器设置为 private)
别扮可爱
言到意到. 意到言到.
每个概念对应一个词: 给每个抽象概念选一个词, 并且一以贯之.
别用双关语: 一词一意
使用解决方案领域名称
使用源自所涉问题领域的名称: 优秀的程序员和设计师, 其工作之一就是分离解决方案领域和问题领域的概念.
增加有意义的语境, 如前缀
不要添加没用的语境: 精确正是命名的要点
取好名字最难的地方在于需要良好的描述技巧和共有文化背景.
函数
短小: 函数的第一规则. 每行都不应该有 150 个字符那么长, 函数也不该有 100 行那么长. 函数的缩进层级不该多于一层.
只做一件事: 如果函数只是做了该函数名下同一抽象层上的步骤, 则函数还是只做了一件事.(判断函数是否不止做了一件事, 还有就是看是否能再拆出一个函数, 该函数不仅只是单纯地重新诠释其实现. 只做一件事的函数无法被合理地切分为多个区段.)
每个函数一个抽象层级: 我们想要让代码拥有自顶向下的阅读顺序. 让代码读起来像是一系列自顶向下的 TO 起头段落是保持抽象层级协调一致的有效技巧.
switch 语句: 多态 - 将 switch 语句埋到抽象工厂底下, 不让任何人看大.
使用描述性名称
函数参数: 参数越少越好, 参数越少越便于理解
一元函数的普遍形式: 函数名称应能区分出来
问关于参数的问题, 如 boolean fileExist("myFile");
将参数转换为其他什么东西, 在输出之: 如 InputStream fileOpen("myFile");
事件, 有输入参数而无输出参数, 程序将函数看做一个事件, 使用该参数修改系统状态
标识参数丑陋不堪, 即如果标识为 true 将会这样做, 标识为 false 将会这样做: 向函数传入布尔值简直是骇人听闻的做法.
二元函数: 单个值的有序组成部分; 可以把某个参数转换成当前类的成员变量, 从而无需再传递它.
三元函数: 排序, 琢磨, 忽略的问题都会加倍体现, 写三元函数之前一定要想清楚
参数列表: 一个数量可变的参数等同于一个参数
函数命名应与参数形成动词 / 名词对, 如 writeField(name)
无副作用: 函数承诺只做一件事, 但还是会做其他被藏起来的事, 例如时序性耦合
输出参数: 面向对象语言中对输出参数的大部分需求已经消失了. 应避免使用输出参数, 如果函数必须要修改某种状态, 就修改所属对象的状态吧.
分隔指令与询问: 函数要么做什么事(do), 要么回答什么事(boolean), 但二者不可兼得.
使用异常替代返回错误码:
使用异常替代错误码返回错误码, 错误处理代码就能从主路径代码中分离出来, 得到简化;
抽离 try/catch 代码块: try/catch 代码块丑陋不堪; 最好把 try 和 catch 代码块的主体部分抽离出来, 另外形成函数.
错误处理就是一件事: 如果 try 在某个函数中存在, 它就该是这个函数的第一个单词, 而且在 catch/finally 代码块后面也不该有其他内容.
Error.java 依赖磁铁: 返回错误码通常暗示某处有个类或是枚举定义了所有错误码. 使用异常替代错误码, 新异常就可以从异常类派生出来, 无需重新编译或重新部署.
别重复自己: 重复可能是软件中一切邪恶的根源.
结构化编程: 每个函数, 函数中的每个代码块都应该有一个入口, 一个出口, 遵循这些规则, 意味着在每个函数中只该有一个 return 语句, 循环中不能有 break 或 continue 语句, 而且永永远远不能有任何 goto 语句. 对于小函数, 这些规则助益不大, 只有在大函数中, 这些规则才会有明显的好处.
如果写出这样的函数: 先想什么就写什么, 然后再打磨它.
小结: 编程艺术是且一直就是语言设计的艺术. 大师级程序员把系统当做故事来讲, 而不是当做程序来写. 如果你遵循这些规则, 函数就会短小, 有个好名字, 而且被很好地归置. 真正的目的在于讲述系统的故事, 而你编写的函数必须干净利落的拼装到一起, 形成一种精确而清晰的语言, 帮助你讲故事.
注释
注释不能美化糟糕的代码
用代码来阐述
好注释:
法律信息: 可指向一份标准许可或其他外部文档
提供信息的注释:
对意图的解释:
阐释:
警示: 例如为什么用某种设计模式
TODO 注释
放大
公共 API 中的 doc
坏注释:
喃喃自语
多余注释
误导性注释
循轨式注释
日志式废话
废话注释: 用整理代码的决心替代创造废话的冲动吧
可怕的废话
能用函数或变量时就别用注释
位置标记:///////////////////(少用, 只在有价值的地方用)
括号后面的注释
归属与命名
注释掉的代码
html 注释
非本地信息
信息过多
不明显的联系
函数头
非公共 API 中的 javadoc
范例
格式
格式的目的: 代码格式关乎沟通, 而沟通是专业开发者的头等大事.
垂直格式: 有可能用大多数为 200 行, 最长 500 行的单个文件构造出色的系统(Fitness 总长约为 50000 行).(书中用到 K 线图)
向报纸学习: 名称应当简单一目了然, 细节应该往下渐次展开, 报纸由许多篇文章组成, 多数短小精悍.
概念间垂直方向上的区隔: 空白行
垂直方向上的靠近:
垂直距离: 关系密切的概念应该互相靠近, 变量声明应尽可能靠近其使用位置, 实体变量应该在类的顶部声明, 若某个函数调用了另外一个, 就应该把它们放到一起, 而且调用者应该尽可能放在被调用者上面, 概念相关的代码应该放到一起
垂直顺序: 自上向下, 被调用的函数应该放在执行调用的函数下面
横向格式: 应尽力保持代码行短小, 遵循无需拖动滚动条到右边的原则, 保持在 80 下最好, 120 最多.
水平方向上的区隔与靠近: 如 = 左右的空格字符, 运算符号左右的空格
水平对齐:
缩进: 源文件是一种继承结构, 而不是一种大纲结构, 要让这种范围式继承结构可见, 依赖缩进.
空范围:
团队规则: 遵循团队规则, 绝对不要用各种不同的风格来编写源代码
鲍勃大叔的格式规则:
对象和数据结构
数据抽象: 隐藏实现关乎抽象, 这并不只是用接口和 / 或赋值器, 取值器就万事大吉. 要以最好的方式呈现某个对象包含的数据, 需要严肃思考.
数据, 对象的反对称性: 对象与数据结构之间的二分原理 -- 过程式代码便于在不改动既有数据结构的前提下增加新函数. 面向对象代码便于在不改动既有函数的前提下添加新类. 过程式代码难以添加新数据结构, 因为必须修改所有函数. 面向对象代码难以添加新函数, 因为必须修改所有类.
德墨忒尔律: 模块不应了解它所操作对象的内部情形.
该定律认为, 类 C 的方法 f 只应该调用以下对象的方法:
C
由 f 创建的对象
作为参数传递给 f 的对象
由 C 的实体变量持有的对象
即方法不应调用由任何函数返回的对象的方法.(拒绝链式调用)
火车失事: 拒绝链式调用
混杂: 无论出于怎样的初衷, 公共访问器及改值器都把私有变量公开化, 诱导外部函数以过程式程序使用数据结构的方式使用这些变量. 这增加了增加新函数的难度, 也增加了添加新数据结构的难度.
隐藏结构
数据传送对象: DTO(Data Transfer Objects): 是一个只有公共变量, 没有函数的类; Active Record 是一种特殊的 DTO 形式, 拥有公共变量的数据结构, 通常也会有类似 save 和 find 这样的可浏览方法.
小结: 对象暴露行为, 隐藏数据. 数据结构暴露数据, 没有明显的行为.
错误处理
错误处理很重要, 但如果它搞乱了代码逻辑, 就是错误的做法.
使用异常而非返回错误码: 错误码搞乱了调用者代码
先写 try-catch-finally 语句: 尝试编写强行抛出异常的测试, 再往处理器中添加行为, 使之满足测试要求. 结果就是你要先构造 try 代码块的事务范围, 而且也会帮助你维护好该范围的事务特征.
使用不可控异常: 可控异常的代价就是违反开放 / 闭合原则
如果你在方法中抛出可控异常, 而 catch 语句在三个层级之上, 你就得在 catch 语句和抛出异常之间的每个方法签名中声明该异常
给出异常发生的环境说明: 你抛出的每个异常, 都应当提供足够的环境说明, 以便判断错误的来源和处所. 在 java 中, 你可以从任何异常里得到堆栈踪迹, 然而堆栈踪迹却无法告诉你该失败操作的初衷. 如果你的应用程序有日志系统, 传递足够的信息给 catch 块, 并记录下来.
依调用者需要定义异常类:
定义常规流程: 业务逻辑和错误处理代码之间就会有良好的间隔; 你来处理特例, 客户代码就不用应付异常行为了, 异常行为被封装到特例对象中.
特例模式(Special Case Pattern): 创建一个类或配置一个对象, 用来处理特例.
别返回 null 值:
别传递 null 值:
小结: 将错误处理隔离对待, 独立于主要逻辑之外, 就能写出强固而整洁的代码
边界
保持软件边界整洁
使用第三方代码: 如果你使用类似 Map 这样的边界接口, 就把它保留在类或近亲类中. 避免从公共 API 中返回边界接口, 或将边界接口作为参数传递给公共 API.(即进一步封装公共 API, 放置公共 API 改动时, 需要系统做大范围修改)
浏览和学习边界:
8.3 没有
学习性测试的好处不只是免费:
使用尚不存在的代码: 编写我们想得到的接口, 好处之一是它在我们控制之下.
整洁的边界: 在使用我们控制不了的代码时, 必须加倍小心投资, 确保未来的修改不至于代价太大. 应该避免我们的代码过多地了解第三方代码中的特定信息. 依靠你能控制的东西, 好过依靠你控制不了的东西, 免得日后受他控制. 我们通过代码中少数几处引用第三方接口的位置来管理第三方边界; 也可以使用 ADAPTER 模式将我们的接口转换为第三方提供的接口. 在边界两边推动内部一致的用法, 当第三方代码有改动时, 修改点也会更少.
单元测试
TDD 三定律:
在编写不能通过的单元测试前, 不可编写生产代码.
只可编写刚好无法通过的单元测试, 不能编译也算不通过.
只可编写刚好足以通过当前失败测试的生产代码
保持测试整洁: 测试代码和生产代码一样重要.
整洁的测试: 构造 ->操作 ->检验 (Build->Operate->check):
构造测试数据; 操作测试数据; 检验操作是否得到期望的结果
守规矩的开发者将它们的测试代码重构为更简洁和具有表达力的形式.
双重标准: 测试代码应当简单, 精悍, 足具表达力. 双重标准指的是在关乎内存或 CPU 效率的问题.
每个测试一个断言: 每个测试函数都应该有且只有一个断言语句, 或者说, 单个测试中的断言数量应该最小化, 每个测试函数只测试一个概念.
F.I.R.S.T. : 快速(Fast), 独立(Independent), 可重复(Repeatable), 自足验证(Self-Validating), 及时(Timely).
测试应该够快: 测试运行缓慢你就不会想要频繁的运行它; 测试应该相互独立; 测试应当可在任何环境中重复通过; 测试应该有布尔值输出, 你不应该查看日志来确认是否通过, 不应该手工对比两个不同文本来确认测试是否通过; 测试应及时编写: 单元测试应该恰好在使其通过的生产代码之前编写.
小结: 如果你坐视测试腐败, 那么代码也会跟着腐坏.
类
类的组织: 公共静态常量 -> 私有静态常量 -> 私有实体变量 -> 最好没有公共变量 -> 公共函数
对于封装: 测试说了算
类应该短小: 类的第一条规则是类应该短小; 第二条规则是还要更短小. 对于函数, 我们通过计算代码行数衡量大小. 对于类, 我们通过计算权责 (Responsibility) 来衡量.
单一权责原则(SRP): 类或模块应有且只有一条加以修改的理由 -- 类只应有一个权责, 只有一条修改的理由. 让软件能工作和让软件保持整洁, 是两种截然不同的工作; SRP 在编程行为中的重要程度等同于在程序中的重要程度.
每个达到一定规模的系统都会包括大量逻辑和复杂性, 管理这种复杂性的首要目标是加以组织, 以便开发者知道到哪能找到东西, 并且在某个特定时间只需要理解直接有关的复杂性.
系统应该由许多短小的类而不是少量的巨大类组成. 每个类封装一个权责, 只有一个修改的原因, 并与少数其他类一起协同达成期望的系统行为.
内聚: 内聚性高, 意味着类中的方法和变量互相依赖, 互相结合成一个逻辑整体.
保持内聚性就会得到许多短小的类: 当类失去了内聚性, 就拆分它.
每改动一次, 就执行一次, 确保程序的行为没有发生变化.
为了修改而组织: 在整洁的系统中, 我们对类加以组织, 以降低修改的风险. 开放 - 闭合原则(OCP): 类应当对扩展开放, 对修改封闭. 我们希望将系统打造成在添加或修改特性时尽可能少惹麻烦的架子. 在理想的系统中, 我们通过扩展系统而非修改现有代码来添加新特性.
隔离修改: 需求会改变, 所以代码也会改变. 具体类包含实现细节, 而抽象类则只呈现概念. 可以借助接口和抽象类来隔离这些细节带来的影响.
部件之间的解耦代表着系统中的元素相互隔离得很好.
降低连接度, 我们的类就遵循了另一个类设计原则: 依赖倒置原则
依赖倒置原则(Dependency Inversion Principle, DIP): 类应当依赖于抽象而不是依赖于具体细节. 程序要依赖于抽象接口, 不要依赖于具体实现. 简单的说就是要求对抽象进行编程, 不要对实现进行编程, 这样就降低了客户与实现模块间的耦合.
系统
复杂要人命. 它消磨开发者的生命, 让产品难以规划, 构建和测试.
---------------------(Ray Ozzie, 微软公司首席技术官)
如何建造一个城市: 整洁的代码帮助我们在较低层的抽象层级上达成这一目标. 本章将讨论如何在较高的抽象层级 -- 系统层级 -- 上保持整洁.
将系统的构造和使用分开: 软件系统应将启动过程和启动过程之后的运行时逻辑分离开, 在启动过程中构建应用对象, 也会存在相互缠结的依赖关系.
分解 main: 将构造与使用分开的方法之一是将全部构造过程搬迁到 main 或被称之为 main 的模块中, 设计系统的其余部分时, 假设所有对象都已正确构造和设置.
工厂: 抽象工厂模式让应用自行控制何时创建实体.
依赖注入, 控制反转: 分离构造和使用. 控制反转将第二权责从对象中拿出来, 转移到另一个专注于此的对象中, 从而遵循了单一权责原则. 什么是依赖注入?
扩容:"一开始就做对系统" 纯属神话. 反之, 我们应该只去实现今天的用户故事, 然后重构, 明天再扩展系统, 实现新的用户故事. 这就是迭代和增量敏捷的精髓所在. 测试驱动开发, 重构以及他们打造出的整洁代码, 在代码层面保证了这个过程的实现.
软件系统与物理系统可以类比. 它们的架构都可以递增式地增长, 只要我们持续将关注面恰当地切分.
横贯式关注面: 原则上, 你可以从模块, 封装的角度推理持久化策略. 但在实践上, 你却不得不将实现了持久化策略的代码铺展到许多对象中.
Java 中的三个 "方面":
- Java 代理: 适用于简单的情况, 例如在单独的对象或类中包装方法调用.
纯 JAVA AOP 框架: Spring AOP, JBoss AOP
AspectJ 的方面: AspectJ 提供了一套用以切分关注面的丰富而强有力的工具.
测试驱动系统架构: 通过方面式手段切分关注面的威力不可低估, 假使你能用 POJO 编写应用程序的领域逻辑, 在代码层面与架构关注面分离开, 就有可能真正地用测试来驱动架构. 采用一些新技术, 就能将架构按需从简单演化到精细. 没必要先做大设计(Big Design Up Front, BDUF). 实际上, BDUF 甚至是有害的, 它阻碍改进, 因为心理上会抵制丢弃既成之事, 也因为架构上的方案选择影响到后续的设计思路.
最佳的系统架构由模块化的关注面领域组成, 每个关注面均用纯 Java(或其他语言)对象实现. 不同的领域之间用最不具有侵害性的方面或类方面工具整合起来. 这种架构能测试驱动, 就像代码一样.
优化决策: 模块化和关注面切分成就了分散化管理和决策. 最好是授权给最有资格的人, 延迟决策至最后一刻也是好手段, 让我们能够基于最有可能的信息作出选择. 提前决策是一种预备知识不足的决策. 如果决策太早, 就会缺少太多客户反馈, 关于项目的思考和实施经验.
拥有模块化关注面的 POJO 系统提供的敏捷能力, 允许我们基于最新的知识做出优化的, 时机刚好的决策. 决策的复杂性也降低了.
明确使用添加了可论证价值的标准: 有了标准, 就更易复用想法和组件, 雇用拥有相关经验的人才, 封装好点子, 以及将组件连接起来. 不过, 创立标准的过程有时却漫长到行业等不及的程度, 有些标准没能与它要服务的采用者的真实需求相结合.
系统需要领域特定语言: 领域特定语言允许所有抽象层级和应用程序中的所有领域, 从高级策略到底层细节使用 POJO 来表达.
小结: 系统应该是整洁的. 侵害性架构会湮灭领域逻辑, 冲击敏捷能力. 在所有的抽象层级上, 意图都应该更加清晰可辨. 只有在编写 POJO 并使用类方面的机制来无损地组合其他关注面时, 这种事情才会发生.
无论是设计系统或单独的模块, 别忘了使用大概可工作的最简单方案.
迭进
通过迭进设计达到整洁目的: 根据 Kent 所述的简单设计的四条规则, 只要遵循以下规则, 设计就能变得 "简单":
运行所有测试: 设计必须制造出如预期一般工作的系统, 这是首要因素. OO 的目标是低耦合度, 高内聚度. 有了测试, 就能保证代码和类的整洁, 方法就是递增式地重构代码. 测试消除了对清理代码就会破坏代码的恐惧.(提高内聚性, 降低耦合度, 切分关注面, 模块化系统关注面, 缩小函数和类的尺寸, 选用更好的名称...)
不可重复: 重复是拥有良好设计系统的大敌.
表达力: 软件项目的主要成本在于长期维护. 作者把代码写的越清晰, 其他人花在理解代码上的时间也就越少, 从而减少缺陷, 缩减维护成本. 做到有表达力的最重要的方式是尝试.
尽可能减少类和方法的数量: 运用以上三个规则可能导致过多的类和方法, 此条规则优先级最低. 尽管使类和函数的数量尽量少是很重要的, 但更重要的却是测试, 消除重复和表达力.
小结: 不会有能替代经验的一套简单实践手段.
并发编程
对象是过程的抽象. 线程是调度的抽象.
为什么要并发: 并发是一种解耦策略. 它帮助我们把做什么 (目的) 和何时 (时机) 做分解开. 解耦目的与时机能明显地改进应用程序的吞吐量和结构. 应用程序看起来更像是许多台协同工作的计算机, 而不是一个大循环. 例如 Servlet, 当有 web 请求时, servlet 就会异步执行, 每次 Servlet 是在自己的小世界中执行, 与其他 servlet 的执行是分离的.
并发会在性能和编写额外代码上增加一些开销;
正确的并发是复杂的, 即便对于简单的问题也是如此;
并发缺陷并非总能重现, 所以常被看做偶发事件而忽略, 未被当做真的缺陷看待;
并发常常需要对设计策略的根本性修改.
挑战: 需要理解 Just-In-Time 编译器如何对待生成的字节码, 理解 Java 内存模型认为什么东西具有原子性.
并发防御原则:
单一权责原则(SRP): 方法, 类, 组件应当只有一个修改的理由. 并发设计自身足够复杂到成为修改的理由, 所以也该从其他代码中分离出来. 建议: 分离并发相关代码与其他代码
并发相关代码有自己的开发, 修改和调优生命周期;
并发相关代码有自己要对付的挑战, 和非并发相关代码不同, 而且往往更为困难;
即便没有周边应用程序增加的负担, 写的不好的并发代码可能的出错方式数量也已经足具挑战性.
推论: 限制数据作用域 -- 采用 synchronized 关键字在代码中保护一块使用共享对象的临界区. 限制临界区的数量很重要. 建议: 谨记数据封装, 严格限制对可能被共享的数据的访问.
推论: 使用数据复本: 避免共享数据的好方法之一就是一开始就避免共享数据. 如果有避免共享数据的简易手段, 结果代码就会大大减少导致错误的可能.
推论: 线程应尽可能地独立: 让每个线程在自己的世界中存在, 不与其他线程共享数据. 建议: 尝试将数据分解到可被独立线程 (可能在不同处理器上) 操作的独立子集.
了解 Java 库:
使用类库提供的线程安全群集;
使用 executor 框架执行无关任务;
尽可能使用非锁定解决方案;
有几个类并不是线程安全的.
线程安全群集:
- java.util.concurrent.*
- java.utl.concurrent.atomic.*;
- java.util.concurrent.locks.*;
ReentrantLock, 可在一个方法中获取, 在另一个方法中释放的锁;
Semaphore 经典的 "信号" 的一种实现, 有计数器的锁
CountDownLatch, 在释放所有等待的线程之前, 等待制定数量事件发生的锁. 这样, 所有的线程都平等地几乎同时启动.
了解执行模型:
- | 定义 |
---|---|
限定资源 | 有着固定尺寸或数量的资源, 如数据库连接或固定尺寸的读 / 写缓存等 |
互斥 | 每个时刻仅有一个线程能访问共享数据数据或共享资源 |
线程饥饿 | 一个或一组线程在很长时间内或永久被禁止 |
死锁 | 两个或多个线程互相等待执行结束 |
活锁 | 执行次序一致的线程,每个都想起步,但发现其他线程已经 “在路上”。由于竞步的原因,线程会持续尝试起步,但在很长时间内却无法如愿,甚至永远无法启动 (一直重复尝试—失败—尝试—失败的过程) |
生产者 - 消费者模型: 生产者消费者之间的队列是一种限定资源;
读者 - 作者模型: 主要为读者提供信息源, 偶尔被作者线程更新的共享资源, 吞吐量是一个问题.
宴席哲学家: 企业级应用中进程竞争资源的情形.
建议: 学习这些基础算法, 理解其解决方案, 则能在遇到并发问题时你就能有解决问题的准备了
警惕同步方法之间的依赖:
建议: 避免使用一个共享对象的多个方法
有时必须使用一个共享对象的多个方法, 有 3 种方法:
基于客户端的锁定: 客户端代码在调用第一个方法前锁定服务端, 确保锁的范围覆盖了调用最后一个方法的代码;
基于服务端的锁定: 在服务端内创建锁定服务端的方法, 调用所有方法, 然后解锁. 让客户端代码调用新方法.
适配服务端: 创建执行锁定的中间层. 这是一种基于服务端锁定的例子, 但不修改原始服务端代码.
保持同步区域微小: synchronized 锁是昂贵的, 会带来延迟和额外开销. 应该尽可能少的设计临界区.
建议: 尽可能减小同步区域
很难编写正确的关闭代码:
编写永远运行的系统, 与编写运行一段时间后平静地关闭的系统是两码事.
建议: 尽早考虑关闭问题, 尽早令其正常工作. 这会花费比你预期更多的时间. 检视既有算法, 因为这可能比想象中难的多.
测试线程代码:
建议: 编写有潜力暴露问题的测试, 在不同的编程配置, 系统配置和负载条件下频繁运行. 如果测试失败, 跟踪错误. 别因为后来测试通过了后来的运行就忽略失败.
精炼的建议:
将伪失败看作可能的线程问题: 不要将系统错误归咎于偶发事件.
先使非线程代码可工作: 不要同时追踪非线程缺陷和线程缺陷. 确保代码在线程之外可工作.
编写可插拔的线程代码:
单线程与多个线程在执行时不同的情况;
线程代码与实物或测试替身互动;
用运行快速, 缓慢和有变动的测试替身执行;
将测试配置为能运行一定数量的迭代
编写可调整的线程代码: 允许线程数量可调整, 在系统运行时允许线程发生变动. 允许线程依据吞吐量和系统使用率自我调整.
运行多于处理器数量的线程: 系统在切换任务时会发生一些事, 为了促使任务交换的发生, 运行多于处理器或处理器核心数量的线程. 任务交换越频繁, 越有可能找到错过临界区或导致死锁的代码.
在不同平台上运行: 不同的操作系统有着不同的线程策略. 尽早并经常地在所有目标平台上运行线程代码.
装置试错代码: 两种装置代码的方法
硬编码: 手工向代码中插入 wait(),sleep(),yield(),priority().
自动化:(要点是让代码 "异动")使用 Aspect-Oriented Framework,CGLIB 或 ASM 之类工具通过编程来装置代码: 一个有单个方法 jiggle()的类, 第一种方法 jiggle()什么都不做, 第二种实现生成一个随机数, 在睡眠, 让步或径直执行间做选择. 上千次的做这种随机测试, 大概就能找到一些缺陷的根源.
调整代码并强迫错误发生:
小结: 要编写并发代码, 就得严格地编写整洁的代码, 否则将面临微细和不频繁发生的失败. 第一要诀是遵循单一权责原则. 将系统分离了线程相关代码和线程无关代码的 POJO
逐步改进
要写整洁的代码, 必须先写肮脏的代码, 然后再清理它.
毁坏程序的最好方法之一就是以改进之名大动其结构.
优秀的软件设计, 大都关乎分隔 -- 创建合适的空间放置不同种类的的代码.
保持代码持续整洁和简单, 永不让腐坏有机会开始.
Junit 内幕
否定式比肯定式难理解
重构常会导致另一次推翻推翻此次重构的重构. 重构是一种不断试错的迭代过程.
重构 SerialDate
味道与启发
代码修改的原因
注释:
不恰当的信息: 注释只应该描述有关代码和设计的技术性信息.
废弃的注释: 过时, 无关或不正确的注释就是废弃的注释.
冗余注释: 注释应该谈及代码自身没提到的东西
糟糕的注释: 值得编写的注释, 也值得好好写.
注释掉的代码
环境:
需要多步才能实现的构建: 构建系统应该是单步的小操作.
需要多步才能做到的测试: 应当能够发出单个指令就可以运行全部单元测试. 能够运行全部测试是如此基础和重要, 应该快速, 轻易和直截了当地做到.
函数
过多的参数, 没参最好
输出参数: 输出参数就违反直觉. 如果函数非要修改什么东西的状态不可, 就修改它所在对象的状态好了.
标识参数: 布尔值参数大声宣告函数做了不止一件事.
死函数: 永不被调用的方法应该丢弃. 别害怕删除函数.
一般性问题:
一个源文件中存在多种语言: 理想的源文件包括且只包括一种语言, 应该尽力减少源文件中额外语言的数量和范围.
明显的行为未被实现: 最小惊异原则 - 函数或类应该实现其他程序员有理由期待的行为.
不正确的边界行为: 别依赖直觉. 追索每种边界条件, 并编写测试.
忽视安全: 关闭失败测试, 告诉自己过后再处理, 这和假装刷信用卡不用还钱一样坏.
重复: 核心原则. 每次看到重复代码, 都代表遗漏了抽象.
在错误的抽象层级上的代码: 只与细节实现有关的常量, 变量或工具函数不应该在基类中出现.
基类依赖于派生类: 例外情况是派生类数量严格固定.
信息过多: 设计良好的模块有着非常小的接口, 让你能事半功倍. 设计良好的接口并不提供许多需要依靠的函数, 所以耦合度也较低. 设计低劣的接口提供大量你必须调用的的函数, 耦合度较高. 隐藏你的数据. 隐藏你的工具函数. 隐藏你的常量和你的临时变量. 不要创建拥有大量方法或大量实体变量的类. 不要为子类创建大量受保护变量和函数. 尽量保持接口紧凑. 通过限制信息来控制耦合度.
死代码: 死代码就是不执行的代码.
垂直分隔: 变量和函数应该在靠近被使用的地方定义.
前后不一致: 从一而终, 小心选择约定, 一旦选中, 就小心持续遵循.
混淆视听:
人为耦合: 不互相依赖的东西不该耦合.
特性依恋: 类的方法只应对其所属类中的变量和函数感兴趣, 不该垂青其他类中的变量和函数.
"选择算子" 参数:"选择算子" 参数只是一种避免把大函数切分为多个小函数的偷懒做法. 选择算子不一定是 boolean 类型, 可能是枚举元素, 整数或任何一种用于选择函数行为的参数.
晦涩的意图: 代码要尽可能具有表达力.
位置错误的权责: 软件开发者做出的最重要决定之一就是在哪里放代码.(最小惊异原则)代码应该放在读者自然而然期待它所在的地方.
不恰当的静态方法: 恰当的静态方法不应在单个实体上操作.
应当使用解释性变量: 让程序可读的最有力的方法之一就是将计算过程打散成在用有意义的单词命名的变量中放置的中间值.
函数名称应该表达其行为:
理解算法:"可以工作" 是不行的, 必须知道解决方案是正确的.
应当把逻辑依赖改为物理依赖: 依赖者模块不应对被依赖者模块有假定.
应当用多态替代 if/else 或 switch/case: 在使用 if/else 或 switch/case 前, 先考虑使用多态.
遵循标准约定:
用命名常量替代魔术数:
准确:
结构甚于约定: 坚守结构甚于约定的设计决策. 命名约定很好, 但却次于强制性的结构.
封装条件: 应该把解释了条件意图的函数抽离出来.
避免否定性条件:
函数只做一件事:
不要掩蔽时序耦合: 通过创建时序队列暴露时序耦合.
应当封装边界条件:
函数应当只在一个抽象层级上:(拆分不同抽象层级是重构的最重要的功能之一)
应当在较高层级放置可配置数据
避免传递浏览: 确保模块只了解其直接协作者.
Java
通过使用通配符避免过长的导入清单:(这一项由 IDE 来实现)
不要继承常量:
常量 VS. 枚举 : 优先用枚举
采用描述性名称:
名称应与抽象层级相符
尽可能使用标准命名法:
无歧义的名称:
避免编码: 不应在名称中包括类型或作用范围信息.
名称应该说明副作用: 名称应该说明函数, 变量或类的一切信息.
测试:
测试不足:
使用覆盖率工具: 覆盖率工具能汇报你测试策略中的缺口.
别略过小测试
被忽略的测试就是对不确定事物的疑问: 需求不明确而不能确定某个行为细节, 可以用注释掉的测试或者用 @Ignore 标记的测试来表达我们对于需求的疑问.
测试边界条件
全面测试相近的缺陷
测试失败的模式有启发性
测试覆盖率的模式有启发性
测试应该快速
来源: https://juejin.im/entry/5bef5bf66fb9a049b77ff2cc