一、整洁代码
A.混乱的代价
1.有些团队在项目初期进展迅速,但有那么一两年的时间却慢去蜗行。对代码的每次修改都影响到其他两三处代码
2.花时间保持代码整洁不但有关效率,还有关生存
3.程序员遵从不了解混乱风险经理的意愿,也是不专业的做法
4.Bjarne Stroustrup,C++发明者:我喜欢优雅和高效的代码。代码逻辑应该直接了当,叫缺陷难以隐藏;尽量减少依赖关系,使之便于维护;依据某种分层战略完善错误处理代码;性能调至最优,省得引诱别人做没规矩的优化,搞出一堆混乱来。整洁的代码只做好一件事。
5.Grady Booch,《面向分析与设计》:整洁的代码简单直接。整洁的代码如同优美的散文。整洁的代码从不隐藏设计者的意图,充满了干净利落的抽象和直接了当的控制语句。
6.Dave Thomas,OTI公司创始人:整洁的代码应可由作者之外的开发者阅读和增补。它应有单元测试和验收测试。它使用有意义的命名。它只提供一种而非多种做一件事的途径。它只有尽量少的依赖关系,而且要明确地定义和提供清晰、尽量少的API。代码应通过其字面表达含义,因为不同的语言导致并非所有必须信息均可通过代码自身清晰表达。
7.Michael Feathers,《修改代码的艺术》:我可以列出我留意到的整洁代码的所有特点,但其中有一条是根本性的。整洁的代码总是看起来像是某位特别在意它的人写的。几乎没有改进的余地。代码作者什么都想到了,如果你企图改进它,总会回到原点,赞叹某人留给你的代码——全心投入的某人留下的代码。
8.Ron Jeffries,《极限编程实施》:简单代码,依其重要顺序:能通过所有测试;没有重复代码;体现系统中的全部设计理念;包括尽量少的实体,比如类、方法、函数等
9.Ward Cunningham,Wiki发明者:如果每个例程都让你感到深合已意,那就是整洁代码。如果代码让编程语言看起来像是专为解决那个问题而存在,就可以称之为漂亮的代码。
B.思想流派
1.读与写花费时间的比例起过10:1
C.童子军军规
1.“让营地比你来时更干净”
2.如果每次签入时,代码都比签出时干净,那么代码就不会腐坏
二、有意义的命名
A.名副其实
1.变量、函数或类的名称应该已经答复了所有的大问题,如果名称需要注释来补充,那就不算名副其实
2.代码的模糊度:即上下文在代码中未被明确体现的程度
B.避免误导
1.程序员必须避免留下掩藏代码本意的错误线索。应当避免使用与本意相悖的词
2.以同样的方式拼写出同样的概念才是信息,拼写前后不一致就是误导
3.要注意使用小写字母i和大写字母O作为变量名,看起来像“壹”和“零”
C.做有意义的区分
1.同一作用范围内两样不同的东西不能重名,如果名称必须相异,那其意思也应该不同才对
2.废话是另一种没意义的区分。假设你有一个Product类,如果还有一个ProductInfo或ProductData类,那它们的名称虽然不同,意思却无区别
3.只要体现出有意义的区分,使用a和the这样的前缀就没错
4.废话都是冗余。Variable一词记录不应当出现在变量名中,Table一词永远不应当出现在表名中
D.使用读得出来的名称
E.使用可搜索的名称
1.单字母名称和数字常量有个问题,就是很难在一大篇文字中找出来
F.避免使用编码
1.把类型或作用域编进名称里面,徒然增加了解码的负担
2.也不必用m_前缀来标明成员变量,应当把类和函数做得足够小,消除对成员前缀的需要
3.不加修饰的接口,不要用前导字母I
G.避免思维映射
1.不应当让读者在脑中把你的名称翻译为他们熟知的名称,单字母变量名就是个问题
2.专业程序员了解,明确是王道
H.类名
1.类名和对象名应该是名词或名词短语,类名不应当是动词
I.方法名
1.方法名应该是动词或动词短语。属性访问器、修改器和断言应该根据其值命名,并依Javabean标准加上get、set和is前缀
2.可以考虑将相应构造器设置为private,强制使用这种命名手段
J.别扮可爱
1.言到意到,意到言到
K.别用双关语
1.避免将同一单词用于不同目的
2.应尽力写出易于理解的代码,把代码写得让别人能一目尽览而不必殚精竭虑地研究
L.使用解决方案领域名称
1.尽管用那些计算机科学术语、算法名、模式名、数学术语
M.使用源自所涉问题领域的名称
1.如果不能用程序员熟悉的术语来给手头的工作命名,就采用从所涉问题领域而来的名称
2.优秀的程序员和设计师,其工作之一就是分离解决方案领域和问题领域的概念
N.添加有意义的语境
1.你需要用有良好命名的类、函数或名称空间来放置名称,给读者提供语境
2.如果没这么做,给名称添加前缀就是最后一招了
O.不要添加没用的语境
1.只要短名称足够清楚,就要比长名称好
P.最后的话
1.取好名字最难的地方在于需要良好的描述技巧和共有文化背景
三、函数
A.短小
1.函数的第一规则是要短小,第二条规则是还要更短小
2.if语句、else语句、while语句等,其中的代码块应该只有一行,该行大抵是一个函数调用语句
3.函数不应该大到足以容纳嵌套结构,所以,函数的缩进层级不该多于一层或两层
B.只做一件事
1.函数应该做一件事。做好这件事,只做这一件事
2.要判断函数是否不止做了一件事,就是看看是否能再拆出一个函数,该函数不仅只是单纯地重新诠释其实现
3.只做一件事的函数无法被合理地切分为多个区段
C.每个函数一个抽象层级
1.要确保函数只做一件事,函数中的语句都要在同一抽象层级上
2.自顶向下读代码:向下规则,让代码拥有自顶向下的阅读顺序,让每个函数后面都跟着下一抽象层级的函数
D.switch语句
1.写出短小的switch语句很维,写出只做一件事的switch语句也很难,Switch天生要做N件事
2.将switch语句埋到抽象工厂底下,不让任何人看到
3.如果只出现一次,用于创建多态对象,而且隐藏在某个继承关系中,在系统其他部分看不到,就还能容忍
E.使用描述性的名称
1.沃德原则:“如果每个例程都让你感到深合已意,那就是整洁代码”
2.函数越短小,功能越集中,就越便于取个好名字
3.别害怕长名称,长而具有描述性的名称,要比短而令人费解的名称好
4.命名方式要保持一致。使用与模块名一脉相承的短语、名词和动词给函数命名
F.函数参数
1.最理想的参数数量是零,有足够的理由才能用三个以上参数
2.事件:在这种形式中,有输入参数而无输出参数,程序将函数看作一个事件,使用该参数修改系统状态
3.对于转换,使用输出参数而非返回值令人迷惑,如果函数要对输入参数进行转换操作,转换结果就该体现为返回值
4.向函数传入布尔值会使方法签名立刻变得复杂起来,大声宣布函数不止做一件事
5.如果函数看来需要两个、三个或三个以上参数,就说明其中一些参数应该封装为类了
6.有可变参数的函数可能是一元、二元甚至三元,超过这个数量就可能要犯错了
7.对于一元函数,函数和参数应当形成一种非常良好的动词/名词对形式
G.无副作用
1.函数承诺只做一件事,但还是会做其他被藏起来的事,会导致古怪的时序性耦合及顺序依赖
2.参数多数会被自然而希地看作是函数的输入
H.分隔指令与询问
1.函数要么做什么事,要么回答什么事,但二者不可得兼
I.使用异步替代返回错误码
1.从指令式函数返回错误码轻微违反了指令与询问分隔的规则。它鼓励了在if语句判断中把指令当作表达式使用
2.try/catch代码块把错误处理与正常流程混为一谈,最好把try和catch代码块的主体部分抽离出来,另外形成函数
3.错误处理就是一件事,处理错误的函数不该做其他事
4.依赖磁铁(dependency magnet):其他许多类都得导入和使用它
J.别重复自己
1.重复可能是软件中一切邪恶的根源,许多原则与实践规则都是为控制与消除重复而创建
K.结构化编程
1.每个函数、函数中的每个代码块都应该有一个入口、一个出口。遵循这些规则,意味着在每个函数中只该有一个return语句,循环中不能有break或者continue语句,而且永永远远不能有任何的goto语句
2.只有在大函数中这些规则才会有明显好处,因为,只要函数保持短小,偶尔出现的return、break或continue语句没有坏处,goto语句尽量避免
L.如何写出这样的函数
1.打磨代码,分解函数、修改名称、消除重复
2.缩短和重新安置方法、拆散类、保持测试通过
四、注释
1.若编程语言足够有表达力,就不需要注释
2.注释的恰当用法是弥补我们在用代码表达意图时遭遇的失败。注释总是一种失败
3.程序员应当负责将注释保持在可维护、有关联、精确的高度,更应该把力气用在写清楚代码上,直接保证无须编写注释
4.不准确的注释要比没注释坏得多
A.注释不能美化糟糕的代码
1.带有少量注释的整洁而有表达力的代码,要比带有大量注释的零碎而复杂的代码像样得多
2.与其花时间编写解释你搞出的糟糕的代码的注释,不如花时间清洁那堆糟糕的代码
B.用代码来阐述
1.用代码解释你大部分的意图,很多时候,简单到只需要创建一个描述与注释所言同一事物的函数即可
C.好注释
1.法律信息
2.提供信息的注释
3.对意图的解释:提供某个决定后面的意图
4.阐释:注释把某些晦涩难懂的参数或返回值的意义翻译为某种可读形式
5.警示
6.TODO注释:注意要清理
7.放大:放大某种看来不合理之物的重要性
8.公共API中的Javadoc
D.坏注释
1.喃喃自语
2.多余的注释
3.误导性注释
4.循规式注释
5.日志式注释
6.废话注释
7.可怕的废话
8.能用函数或变量时就别用注释
9.位置标记:如果标记栏不多,就会显而易见,所以,尽量少用标记栏,只在特别有价值的时候用
10.括号后面的注释
11.归属与署名
12.注释掉的代码
13.HTML注释
14.非本地信息
15.信息过多
16.不明显的联系
17.函数头
18.非公共代码中的Javadoc
19.范例
五、格式
A.格式的目的
1.代码格式关乎沟通,而沟通是专业开发者的头等大事
B.垂直格式
1.短文件通常比长文件易于理解
2.源文件也要像报纸文章那样 ,名称应当简单且一目了然,最顶部应该给出高层次概念和算法,细节应该往下渐次展开
3.几乎所有的代码都是从上往下读,从左往右读。每行展现一个表达式或一个子句,每代码行展示一条完整的思路。这些思路用空白行区隔开来。
4.如果说空白行隔开了概念,靠近的代码行则暗示了它们之间的紧密关系
5.除非有很好的理由,否则就不要把关系密切的概念放到不同的文件中,实际上,这也是避免使用protected变量的理由之一,应避免迫使读者在源文件和类中跳来跳去
6.变量声明应尽可能靠近其使用位置,在函数顶部出现,循环的控制变量总是在循环语句中声明
7.实体变量在类的顶部声明
8.相关函数,若某个函数调用了另外一个,就应该把它们放到一起,而且调用者应该尽可能放在被调用者上面
9.概念相关的代码应该放到一起,相关性越强,彼此之间的距离就该越短
10.我们想自上向下展示函数调用依赖顺序,被调用的函数应该放在执行调用的函数下面,这就建立了一种自顶向下贯穿源代码模块的良好信息流
C.横向格式
1.尽力保持代码行短小,遵循无需拖动滚动条到右边的原则,最好不超过120个
2.我们使用空格字符将彼此紧密相关的事物连接到一起,也用空格字符把相关性较弱的事物分隔开
3.对齐,像是在强调不重要的东西,把目光从真正的意义上拉开
4.如果有较长的列表需要做对齐处理,那问题就是在列表的长度上而不是对齐上
5.程序员相当依赖缩进模式
6.有时,while或for语句的语句体为空,如果无法避免,就确保空范围体的缩进,用括号包围起来
D.团队规则
1.一组开发者应当认同一种模式风格,每个成员都应该采用那种风格
2.好的软件系统是由一系列读起来不错的代码文件组成的,需要拥有一致和顺畅的风格
六、对象和数据结构
A.数据抽象
1.隐藏实现关乎抽象,类并不简单地用取值器和赋值器将其变量推向外部,而是曝露抽象接口,以便用户无需了解数据的实现就能操作数据本体
B.数据、对象的反对称性
1.对象把数据隐藏于抽象之后,曝露操作数据的函数。数据结构曝露其数据,并没有提供有意义的函数
2.对象与数据结构之间的二分原理:
* 过程式代码(使用数据结构的代码)便于在不改动既有数据结构的前提下添加新函数。面向对象代码便于在不改动既有函数的前提下添加新类
* 过程式代码难以添加新数据结构,因为必须修改所有函数。面向对象代码难以添加新函数,因为必须修改所有类
C.得墨忒耳律
1.得墨忒耳律(The Law of Demeter):模块不应了解它所操作对象的内部情形,意味着对象不应通过存取器曝露其内部结构,因为这样更像是曝露而非隐藏其内部结构
2.混合结构,一半是对象,一半是数据结构,应避免这种结构
D.数据传送对象
1.最为精练的数据结构,是一个只有公共变量、没有函数的类,这种被称为数据传送对象,或DTO(Data Transfer Objects)。在与数据库通信、或解析套接字传递的消息之类场景中
2.JavaBean或Active Record
3.不要塞进业务规则方法,把Active Record当做数据结构,并创建包含业务规则、隐藏内部数据(可能就是Active Record的实体)的独立对象
七、错误处理
1.错误处理很重要,但如果它搞乱了代码逻辑,就是错误的做法
A.使用异常而非返回码
1.遇到错误时,最好抛出一个异常。调用代码很整洁,其逻辑不会被错误处理搞乱
B.先写Try-Catch-Finally语句
1.异常的妙处之一是,它们在程序中定义了一个范围。执行try-catch-finally语句中try部分的代码时,你是在表明可随时取消执行,并在catch语句中接续
2.在某种意义上,try代码块就像是事务,catch代码块将程序维持在一种持续状态
3.在编写可能抛出异常的代码时,最好先写try-catch-finally语句,能帮你定义代码的用户应该期待什么,无论try代码块中执行的代码出什么错都一样
C.使用不可控异常
1.可控异常的代价就是违反开放/闭合原则,得在catch语句和抛出异常处之间的每个方法签名中声明该异常
2.可控异常意味着对软件中较低层级的修改,都将波及较高层级的签名
D.给出异常发生的环境说明
1.抛出的每个异常,都应当提供足够的环境说明,以便判断错误的来源和处所
2.应创建信息充分的错误消息,并和异常一起传递出去
E.依调用者需要定义异常类
1.最重要的考虑是它们如何被捕获
2.将第三方API打包是个良好的实践手段,降低了对每个第三方的依赖,也有助于模拟第三方调用
F.定义常规流程
1.特例模式(SPECIAL CASE PATTERN,[Fowler]),创建一个类或配置一个对象,用来处理特例,异常行为被封装到特例对象中
G.别返回null值
1.返回null值,基本是在给自己增加工作量,也是在给调用者添乱,只要有一处没检查null值,应用程序就会失控
H.别传递null值
1.将null值传递给其他方法更糟糕,除非API要求你向它传递null值,否则就要尽可能避免传递null值
八、边界
A.使用第三方代码
1.第三方程序包和框架提供者追求普适性,这样就能在多个环境中工作,吸引广泛的用户
2.我们建议不要将Map(或在边界上的其他接口)在系统中传递,把它保留在类或近亲类中,避免从API中返回边界接口,或将接口作为参数传递给公共API
B.浏览和学习边界
C.学习性测试的好处不只是免费
1.学习性测试毫无成本,编写测试是获得这些知识(要使用的API)的容易而不会影响其他工作的途径
2.学习性测试确保第三方程序包按照我们想要的方式工作
D.使用尚不存在的代码
1.编写我们想得到的接口,好处之一是它在我们控制之下,有助于保持客户代码更可读,且集中于它该完成的工作
E.整洁的边界
1.边界上的改动,有良好的软件设计,无需巨大投入和重写即可进行修改
2.边界上的代码需要清晰的分割和定义了期望的测试。依靠你能控制的东西,好过依靠你控制不了的东西,免得日后受它控制
3.可以使用ADAPTER模式将我们的接口转换为第三方提供的接口
九、单元测试
A.TDD三定律
1.在编写能通过的单元测试前,不可编写生产代码
2.只可编写刚好无法通过的单元测试,不能编译也算不通过
3.只可编写刚好足以通过当前失败测试的生产代码
B.保持测试整洁
1.脏测试等同于没测试,测试必须随生产代码的演进而修改,测试越脏,就越难修改
2.测试代码和生产代码一样重要,它需要被思考、被设计和被照料,它该像生产代码一般保持整洁
3.如果测试不能保持整洁,你就会失去它们,没有了测试,你就会失去保证生产代码可扩展的一切要素
C.整洁的测试
1.三个要素:可读性、可读性和可读性,明确、简洁还有足够的表达力
2.构造-操作-检验(BUILD-OPERATE-CHECK)模式,第一个环节构造测试数据,第二个环节操作测试数据,第三个部分检验操作是否得到期望的结果
3.守规矩的开发者也将他们的测试代码重构为更简洁和具有表达力的形式
D.每个测试一个断言
1.JUnit中每个测试函数都应该有且只有一个断言语句
2.最好的说法是单个测试中的断言数量应该最小化
3.更好一些的规则或许是每个测试函数中只测试一个概念
4.最佳规则是应该尽可能减少每个概念的断言数量,每个测试函数只测试一个概念
E.F.I.R.S.T
1.快速(Fast)测试应该够快
2.独立(Independent)测试应该相互独立
3.可重复(Repeatable)测试应当可在任何环境中重复通过
4.自足验证(Self-Validating)测试应该有布尔值输出
5.及时(Timely)测试应及时编写
十、类
A.类的组织
1.类应该从一级变量列表开始,如果有公共静态变量,应该先出现,然后是私有静态变量,以及实体变量,很少会有公共变量
2.公共函数应该跟在变量列表之后
3.保持变量和工具函数的私有性,但并不执着于此
B.类应该短小
1.第一规则是类应该短小,第二规则是还要更短小
2.衡量方法,计算权责(responsibility)
3.类的名称应当描述其权责,如果无法为某个类命以精确的名称,这个类大概就太长了,类名越含混,该类越有可能拥有过多权责
4.单一权责原则(SRP)认为,类或模块应有且只有一条加以修改的理由
5.系统应该由许多短小的类而不是少量巨大的类组成,每个小类封装一个权责,只有一个修改的原因,并与少数其他类一起协同达成期望的系统行为
6.方法操作的变量越多,就越黏聚到类上,如果一个类的每个变量都被每个方法所使用,则该类具有最大的内聚性
7.保持函数和参数列表短小的策略,有时会导致为一组子集方法所用的实体变量数量增加。出现这种情况时,往往意味着至少有一个类要从大类中挣扎出来。你应当尝试将这些变量和方法分拆到两个或多个类中,让新的类更为内聚
8.将大函数拆为许多小函数,往往也是将类拆分为多个小类的时机
C.为了修改而组织
1.在整洁的系统中,我们对类加以组织,以降低修改的风险
2.开放-闭合原则(OCP):类应当对扩展开放,对修改封闭
3.在理想系统中,我们通过扩展系统而非修改现有代码来添加新特性
4.依赖倒置原则(Dependency Inversion Principle,DIP),类应该依赖于抽象而不是依赖于具体细节
十一、系统
A.如何建造一个城市
1.每个城市都有一组人管理不同的部分,有人负责全局,其他人负责细节
2.深化出恰当的抽象等级和模块,好让个人和他们所管理的“组件”即便在不了解全局时也能有效地运转
B.将系统的构造与使用分开
1.构造与使用是非常不一样的过程
2.软件系统应将启始过程和启始过程之后的运行时逻辑分离开,在启始过程中构建应用对象,也会存在互相缠结的依赖关系
3.将构造与使用分开的方法之一是将全部构造过程搬迁到main或被称为main的模块中,设计系统的其余部分时,假设所有对象都已正确构造和设置
4.可以使用抽象工厂模式让应用自行控制何时创建对象,但构造的细节却隔离于应用程序代码之外
5.控制反转将第二权责从对象中拿出来,转移到另一个专注于此的对象中,从而遵循了单一权责原则。在依赖管理情景中,对象不应负责实体化对自身的依赖,反之,它应当将这份权责移交给其他“有权力”的机制,从而实现控制的反转
C.扩容
1.“一开始就做对系统”纯属神话,反之,我们应该只去实现今天的用户故事,然后重构,明天再扩展系统、实现新的用户故事,这就是迭代和增量敏捷的精髓所在。测试驱动开发、重构以及它们打造出的整洁代码,在代码层面保证了这个过程的实现
2.软件系统与物理系统可以类比。它们的架构都可以递增式的增长,只要我们持续将关注面恰当地切分
3.持久化之类关注面倾向于横贯某个领域的天然对象边界
D.Java代理
1.适用于简单情况,例如在单独的对象或类中包装方法调用
2.旧式的Java对象(Plain-Old Java Object, POJO)
E.纯Java AOP框架
F.AspectJ的方面
G.测试驱动系统架构
1.通过方面式(AOP)的手段切分关注面的威力不可低估。假使你能用POJO编写应用程序的领域逻辑,在代码层面与架构关注面分离开,就有可能真正地用测试来驱动架构
2.没必要先做大设计(Big Design Up Front,BDUF),BDUF甚至是有害的,它阻碍改进,因为心理上会抵制丢弃即成之事,也因为架构上的方案选择影响到后续的设计思路
3.我们可以从“简单自然”但切分良好的架构开始做软件项目,快速交付可工作的用户故事,随着规模的增长添加更多基础架构
4.最佳的系统架构由模块化的关注面领域组成,每个关注面均用纯Java(或其他语言)对象实现,不同的领域之间用最不具有侵害性的方面或类方面工具整合起来,这种架构能测试驱动,就像代码一样
H.优化决策
1.模块化和关注面切分成就了分散化管理和决策
2.延迟决策至最后一刻也是好手段,它让我们能够基于最有可能的信息做出选择
3.拥有模块化关注面的POJO系统提供的敏捷能力,允许我们基于最新的知识做出优化的、时机刚好的决策,决策的复杂性也降低了
I.明智使用添加了可论证价值的标准
1.有了标准,就更易复用想法和组件、雇用拥有相关经验的人才、封装好点子,以及将组件连接起来。不过,创立标准的过程有时却漫长到行业等不及的程度,有些标准没能与它要服务的采用者的真实需求相结合
J.系统需要领域特定语言
1.领域特定语言(Domain-Specific Language, DSL)是一种单独的小型脚本语言或以标准语言写就的API,领域专家可以用它编写读像是组织严谨的散文一般的代码
2.领域特定语言允许所有抽象层级和应用程序中的所有领域,从高级策略到底层细节,使用POJO来表达
十二、迭进
A.通过迭进设计达到整洁目的
1.“简单规则”:
* 运行所有测试
* 不可重复
* 表达了程序员的意图
* 尽可能减少类和方法的数量
* 以上规则按其重要程序排列
B.简单设计原则1:运行所有测试
1.设计必须制造出如预期一般工作的系统,这是首要因素
2.全面测试并持续通过所有测试的系统,就是可测试的系统,不可验证的系统,绝不应部署
3.只要系统可测试,就会导向保持类短小且目的单一的设计方案
4.紧耦合的代码难以编写测试
5.遵循有关编写测试并持续运行测试的简单、明确的规则,系统就会更贴近OO低耦合度、高内聚度的目标,编写测试引致更好的设计
C.简单设计原则2-4:重构
1.有了测试,就能保持代码和类的整洁,方法就是递增式地重构代码
2.测试消除了对清理代码就会破坏代码的恐惧
D.不可重复
1.重复是拥有良好设计系统的大敌
2.极其雷同的代码行当然是重复,还有实现上的重复等其他一些形态
3.“小规模复用”可大量降低系统复杂性,要想实现大规模复用,必须理解如何实现小规模复用
4.模板方法模式是一种移除高层级重复的通用技巧
E.表达力
1.软件项目的主要成本在于长期维护,代码应当清晰地表达其作者的意图
2.可以通过选用好名称来表达
3.可以通过保持函数和类尺寸短小来表达
4.可以通过采用标准命名法来表达
5.编写良好的单元测试也具有表达性
6.做到有表达力的最重要方式是尝试
F.尽可能少的类和方法
1.类和方法的数量太多,有时是由毫无意义的教条主义导致的,应该采用更实用的手段
2.目标是在保持函数和类短小的同时,保持整个系统短小精悍
十三、并发编程
A.为什么要并发
1.并发是一种解耦策略,它帮助我们把做什么(目的)和何时(时机)做分解开
2.解耦目的与时机能明显地改进应用程序的吞吐量和结构
3.单线程程序许多时间花在等待web套接字I/O结束上面,通过采用同时访问多个站点的多线程算法,就能改进性能
4.常见的迷思和误解
* 并发总能改进性能:只在多个线程或处理器之间能分享大量等待时间的时候管用
* 编写并发程序无需修改设计:可能与单线程系统的设计极不相同
* 在采用web或ejb容器时,理解并发问题并不重要
5.有关编写并发软件的中肯的说法:
* 并发会在性能和编写额外代码上增加一些开销
* 正确的并发是复杂的,即使对于简单的问题也是如此
* 并发缺陷并非总能重现,所以常被看做偶发事件而忽略,未被当做真的缺陷看待
* 并发常常需要对设计策略的根本性修改
B.挑战
1.线程在执行代码时有许多可能路径可行,有些路径会产生错误的结果
C.并发防御原则
1.单一权责原则(SRP):方法/类/组件应当只有一个修改的理由
* 并发相关代码有自己的开发、修改和调优生命周期
* 开发相关代码有自己要对付的挑战,和非并发相关代码不同
* 即使没有周边应用程序增加的负担,写得不好的并发代码可能的出错方式数量也已经足具有挑战性
* 建议:分离并发相关代码与其他代码
2.推论:限制数据作用域
* 采用synchronized关键字在代码中保护一块使用共享对象的临界区(critical section)
* 建议:谨记数据封闭;严格限制对可能被共享的数据的访问
3.推论:使用数据复本
* 一开始就避免共享数据,复制对象并以只读方式对待,或复制对象,从多个线程收集所有复本的结果,并在单个线程中合并这些结果
4.推论:线程应尽可能地独立
* 让每个线程在自己的世界中存在,不与其他线程共享数据
* 建议:尝试将数据分解到可被独立线程(可能在不同处理器上)操作的独立子集
D.了解Java库
1.要注意:
* 使用类库提供的线程安全群集
* 使用executor框架(executor framework)执行无关任务
* 尽可能使用非锁定解决方案
* 有几个类并不是线程安全的
E.了解执行模型
1.一些基础定义
* 限定资源:并发环境中有着固定尺寸或数量的资源
* 互斥:每一时刻仅有一个线程能访问共享数据或共享资源
* 线程饥饿:一个或一组线程在很长时间内或永久被禁止
* 死锁:两个或多个线程互相等待执行结束。每个线程都拥有其他线程需要的资源,行不到其他线程拥有的资源,就无法终止
* 活锁:执行次序一致的线程,每个都想要起步,但发现其他线程已经“在路上”。由于竞步的原因,线程会持续尝试起步,但在很长时间内却无法如愿,甚至永远无法启动
2.生产者-消费者模型:一个或多个生产者线程创建某些工作,并置于缓存或队列中。一个或多个消费者线程从队列中获取并完成这些工作。生产者消费者之间的队列是一种限定资源
3.读者-作者模型:协调读者线程,不去读作者线程正在更新的信息(反之亦然),这是一种辛苦的平衡工作,作者线程倾向于长期锁定许多读者纯种,从而导致吞吐量问题
4.宴席哲学家
5.建议学习这些基础算法,理解其解决方案
F.警惕同步方法之间的依赖
1.同步方法之间的依赖会导致并发代码中的狡猾缺陷,建议避免使用一个共享对象的多个方法
2.基于客户端的锁定:客户端代码在调用第一个方法前锁定服务端,确保锁的范围覆盖了调用最后一个方法的代码
3.基于服务端的锁定:在服务端内创建锁定服务端的方法,调用所有方法,然后解锁。让客户端代码调用新方法
4.适配服务端:创建执行锁定的中间层。这是一种基于服务端的锁定的例子,但不修改原始服务端代码
G.保持同步区域微小
1.同一个锁维护的所有代码区域在任一时刻保证只有一个线程执行,因为它们带来了延迟和额外开销,临界区应该被保护起来,应该尽可能少地设计临界区
H.很维编写正确的关闭代码
1.平静关闭很难做到,常见问题与死锁有关,线程一直等待永远不会到来的信号
2.建议:尽早考虑关闭问题,尽早令其工作正常
I.测试线程代码
1.建议:编写有潜力曝露问题的测试,在不同的编程配置、系统配置和负载条件下频繁运行。如果测试失败,跟踪错误。别因为后来测试通过了后来的运行就忽略失败
2.将伪失败看作可能的线程问题:线程代码导致“不可能失败的”失败,不要将系统错误归咎于偶发事件
3.先使非线程代码可工作:不要同时追踪非线程缺陷和线程缺陷,确保代码在线程之外可工作
4.编写可插拔的线程代码,能在不同的配置环境下运行
5.编写可调整的线程代码:允许线程依据吞吐量和系统使用率自我调整
6.运行多于处理器数量的线程:任务交换越频繁,越有可能找到错过临界区域导致死锁的代码
7.在不同平台上运行:尽早并经常地在所有目标平台上运行线程代码
8.装置试错代码:增加对Object.wait()、Object.sleep()、Object.yield()、Object.priority()等方法的调用,改变代码执行顺序,硬编码或自动化
十四、逐步改进
1.要编写清洁代码,必须先写肮脏代码,然后再清理它
2.毁坏程序的最好方法之一就是以改进之名大动其结构
十五、JUnit内幕
十六、重构SerialDate
十七、味道与启发
A.注释
1.不恰当的信息:注释只应该描述有关代码和设计的技术性信息
2.废弃的注释
3.冗余的注释
4.糟糕的注释:别闲扯,别画蛇添足,保持简洁
5.注释掉的代码:删除它
B.环境
1.需要多步才能实现的构建:构建系统应该是单步的小操作
2.需要多步才能做到的测试:应当能够发出单个指令就可以运行全部单元测试
C.函数
1.过多的参数:参数量应该少,三个以上的参数非常值得质疑
2.输出参数:输出参数违反直觉,直接修改它所有对象的状态
3.标识参数:布尔值参数大声宣告函数做了不止一件事,应该消灭掉
4.死函数:永不被调用的方法应该丢弃
D.一般性问题:
1.一个源文件中存在多种语言:尽力减少源文件中额外语言的数量和范围
2.明显的行为未被实现:遵循“最小惊异原则”(The principle of Least Surprise),函数或类应该实现其他程序员有理由期待的行为
3.不正确的边界行为:别依赖直觉,追索每种边界条件,并编写测试
4.忽视安全
5.重复:看到重复代码,都代表遗漏了抽象
6.在错误的抽象层级上的代码:创建分离较高层级一般性概念(抽象类)与较低层级细节概念(派生类)的抽象模型
7.基类依赖于派生类:基类对派生类应该一无所知
8.信息过多:设计良好的模块有着非常小的接口,限制类或模块中暴露的接口数量,类中的方法越少越好,隐藏你的数据,隐藏你的工具函数,隐藏常量和临时变量
9.死代码:删除掉
10.垂直分隔:变量和函数应该在靠近被使用的地方定义,垂直距离要短
11.前后不一致:从一而终,可以追溯到最小惊异原则,让代码更加易于阅读和修改
12.混淆视听:保持源文件整洁,良好地组织,不被搞乱
13.人为耦合:不互相依赖的东西不该耦合
14.特性依恋:类的方法只应对其所属类中的变量和函数感兴趣,不该垂青其他类中的变量和函数
15.选择算子参数:使用多个函数,通常优于向单个函数传递某些代码来选择函数行为
16.晦涩的意图:代码要尽可能具有表达力
17.位置错误的权责:代码应该放在读者自然而然期待它所有的地方
18.不恰当的静态方法:如果的确需要静态函数,确保没机会打算让它有多态行为
19.使用解释性变量:让程序可读的最有力方法之一就是将计算过程打散成用有意义的单词命名的变量中放置的中间值
20.函数名称应该表达其行为
21.理解算法:在你认为自己完成某个函数之前,确认自己理解了它是怎么工作的,你必须知道解决方案是正确的
22.把逻辑依赖改为物理依赖:依赖模块不应对被依赖者模块有假定,它应当明确地询问后者全部信息
23.用多态替代if/Else或Switch/Case,“单个switch”规则:对于给定的选择类型,不应有多于一个switch语句
24.遵循标准约定,遵循基于通用行业规范的一套编码标准
25.用命名常量替代魔术数,在代码中出现原始形态数字通常来说是坏现象,有些常量与非常具有自我解释能力的代码协同工作时,就不必总是需要命名常量来隐藏了。“魔术数”泛指任何不能自我描述的符号
26.准确,在代码中做决定时,确认自己足够准确,明确自己为何要这么做,如果遇到异常情况如何处理
27.结构甚于约定
28.封装条件,如果没有if或while语句的上下文,布尔逻辑就难以理解,应该把解释了条件意图的函数抽离出来
29.避免否定性条件,尽可能将条件表示为肯定形式
30.函数只该做一件事
31.掩蔽时序耦合,排列函数参数,好让它们被调用的次序显而易见
32.别随意,构建代码需要理由,而且理由应与代码结构相契合
33.封装边界条件,把处理边界条件的代码集中到一处,不要散落于代码中
34.函数应该只在一个抽象层级上,函数中的语句应该在同一抽象层级上,该层级应该是函数名所示操作的下一层
35.在较高层级放置可配置数据,如果你有个已知并该在较高抽象层级的默认常量或配置值,不要将它埋藏到较低层级的函数中
36.避免传递浏览,让直接协作者提供所需的全部服务,不必逛遍系统的对象全图,搜寻我们要调用的方法
E.Java
1.通过使用通配符避免过长的导入清单
2.不要继承常量,应该直接导入常量类
3.常量 vs. 枚举,放心使用枚举
F.名称
1.采用描述性名称,事物的意义随着软件的演化而变化,要经常性地重新估量名称是否恰当
2.名称应与抽象层级相符,不要取沟通实现的名称;取反映类或函数抽象层级的名称
3.尽可能使用标准命名法
4.无歧义的名称,选用不会混淆函数或变量意义的名称
5.为较大作用范围选用较长名称
6.避免编码,不要用匈牙利命名法污染你的名称
7.名称应该说明副作用
G.测试
1.测试不足,一套测试应该测到所有可能失败的东西
2.使用覆盖率工具,能汇报你测试策略中的缺口
3.别略过小测试
4.被忽略的测试就是对不确定事物的疑问
5.测试边界条件
6.全面测试相近的缺陷
7.测试失败的模式有启发性,完整的测试用例,按合理的顺序排列,能暴露出模式
8.测试覆盖率的模式有启发性
9.测试应该快速