目录
1、整洁代码
2、有意义的命名
3、函数:
4、注释
5、格式
纵向格式:
横向格式:
团队规则:
6、对象和数据结构
7、错误处理
8、边界
9、单元测试
10、类
11、系统
12、迭进
13、并发编程
14、逐步改进
1)要有代码:因为代码呈现需求的细节,而这些细节无法被忽略或者抽象,因此需要规范。
2)糟糕代码:糟糕的代码就像沼泽一样,曾经说过有朝一日再回头清理——稍后等于永不。
3)混乱代价:随着时间发展,生产力会随着糟糕的代码趋向于零。
4)新设计:当旧系统糟糕到开发团队造反,重新开发新系统替代时候,新系统不久也会走老系统的老路。
5)态度:程序员遵从 不了解混乱风险的经理的意愿是不专业的做法,就像医生做手术按照病人说的来办。
6)谜底:制造混乱无助于赶上期限,赶上期限的唯一方法是:始终尽可能保持代码整洁。
7)整洁代码的艺术:像绘画,需要遵循大量小技巧,习得“整洁感”和“代码感”。
8)什么是整洁代码?
Bjarne Stroustrup(C++语言发明者):我喜欢优雅和高效的代码,代码逻辑应当直截了当,叫缺陷难以隐藏;尽量减少依赖关系,使之便于维护;依据某种分层战略完善错误处理代码;性能调至最优,省的引诱别人做没规矩的优化,搞出一堆混乱来。——整洁的代码只做好一件事。
Grady Booch(《面向对象分析与设计》作者):整洁的代码简单直接。整洁的代码如同优美的散文,整洁的代码从不隐藏设计者的意图,充满了干净利落的抽象和直接了当的控制语句。
Michael Feathers(《修改代码的艺术》作者):整洁的代码总是看起来像是某位特别在意的人写的,几乎没有改进的余地,代码作者什么都想到了,如果你企图改进它,总会回到原点,赞叹某人留给你的代码——全心投入的某人留下的代码。
Ron Jeffries(《极限编程实践》作者): <1>能通过所有测试;<2>没有重复代码;<3>体现系统中的全部设计理念;<4>包括尽量少的实体,比如类、方法、函数等。——有意义的命名;消除重复代码;只做一件事;表达力;小规模抽象;
Ward Cunningham(Wiki发明者,极限编程创始人之一):如果每个例程都让你感到深合己意,那就是整洁代码。如果代码让编程语言看起来像是专门解决那个问题而存在,就可以成为漂亮代码。——让编程语言像是专为解决那个问题而存在!
本书中设计原则引用:单一职责原则(SRP)、开放闭合原则(OCP)和依赖倒置原则(DIP)。
1)名副其实:变量、函数或类的名称应该回答为什么存在、做什么事情、怎么用等问题。
2)避免误导:应该避免使用与本意相悖的词。hp、aix、sco都不应该作为变量名,因为他们都是UNIX平台或类UNIX平台的专有名称。避免小写字母l和字母o作为变量名因为在某些字体下和1(一)以及0(零)太像。
3)做有意义的区分:以数字系列命名(a1、a2,...aN)没有提供正确信息,没有提供作者意图。废话也是无意义的表达,需要区分名称。
4)使用读的出来的名称:人类擅长记忆和使用单词,命名读起来顺口较好。
5)使用可以搜索的名称:单个字符a或数字常量7不太容易搜索,因此使用单词或MAX_CLASSES_PER_STUDENT更利于搜索。
6)避免使用编码:编码太多了,不应该增加解码负担。
7)避免思维映射:循环计数器常用i、j、k来计数,使用a,b、c不太好。——明确是王道
8)类名和方法名:类名和对象名应该是名词或名词短语;方法名应该为动词或动词短语。
9)别用双关语:代码作者应该写出易于理解的代码。
10)使用有意义的语境:添加前缀addFirstName、addLastName、addState等,不要添加没有用的语境,添加IDE不能自动添加的语境就是浪费生命。
错误函数写法:一个函数中包含多个不同层级的抽象,奇怪的字符串处理和函数调用,混用双重嵌套以及多个if语句等。
推荐函数写法:
1)短小:20行封顶最佳,不应该超过100行(一屏幕显示长度)。——if语句、else语句、while语句等,代码块应该只有一行,该行为一个函数调用语句
2)只做一件事:做好这件事,只做一件事情;包括:同一抽象层上的步骤。
3)每个函数一个抽象层级:自顶向下读代码(向下规则),一个函数相当于文章中一段,每段描述当前抽象层级,并引出下一抽象层级。
4)短小的switch语句:若遇到switch根据类型,new新的对象时候,可以将switch埋到抽象工厂底下,利用接口多态的new新的对象。
5)使用描述性的名称:长而有描述性的名称比短而令人费解的名称好;选择描述性的名称能帮助理清模块的设计思路,并帮助改进思路。
6)函数参数:最好是参数为0,其次是一个,两个就可能造成顺序问题。三个及以上最好使用结构体或类缩短参数个数;不要在参数列表地方弄输出参数,很容易出错。
7)使用异常代替返回错误码:错误码会带来更深的嵌套结构,异常能简化代码结构。函数应该只做一件事情,而错误处理就是一件事,因此错误处理try语句不应在函数中出现。应该让函数抛出异常。
8)不要重复自己:重复代码应该抽离出来,不然修改时候需要修改多个地方。
9)如何写出这样函数:写代码应该像写文章或写论文一样,先想到什么就写什么,然后再打磨他。
1)能用代码表达意思的用代码表达:注释的问题在于:代码在不断更新,注释很少有人维护。
2)好的注释:法律信息、对正则表达式的解释、警告信息、TODO工作列表、放大不合理地方的重要性。
3)坏的注释:不明所以、多余的注释、误导性注释、循规式注释(每个函数,每个参数都有注释是愚蠢可笑的)、日志式注释、废话注释、位置标记、括号后面的注释、归属或署名、注释掉的代码(应该删除,版本控制系统可以帮助找回历史代码)、HTML注释、非本地信息、信息过多、不明显的联系、函数头(利用好的函数名字解决)、过长的注释(读起来浪费时间)。
格式的目的:代码格式关乎沟通,沟通是开发者的头等大事。代码阅读一般都是从上往下,从左往右,代码行应展示一条完整的思路。
1)垂直方向上的分割:利用空行分割垂直方向上的代码,因为目光总会停留在空白行之后那一行。
2)垂直方向上的靠近:有密切关系的代码行应相互靠近,例如相关变量声明应放在一起。
3)变量声明:应尽可能靠近其使用位置,本地变量应放在函数的顶部。
4)实体变量:C++习惯放在底部,java习惯放在顶部。重点是放在谁都知道的地方声明,尊重习惯。
5)相关函数:应该按照调用的顺序,从上往下的自然顺序。
6)概念相关:概念相关的代码应该放在一起。相关性越强,彼此之间的距离就应该越短。
1)一行长度:循序无需拖动滑动条到右边的原则,一般在100字符以内。
2)水平间隔和靠近:使用空格字符将彼此紧密相关的事物连接在一起,也可以使用空格把相关性较弱的事物分隔开。人第一眼比较容易看到空格后一个字符。在赋值操作符周围加空格以此达到强调作用;不在函数名和左括号之间加空格,因为函数和参数密切相关;参数一 一隔开,使用逗号强调参数是分离的。
3)水平对齐:变量水平对齐没有啥用,有自动格式化工具使用自动格式化工具对齐即可。
4)缩进:花括号缩进有利于查看代码工作范围。没有缩进的代码难以阅读。
5)空范围:while或for语句的语句体为空时候,应该确保分号在下一行缩进位置。或者使用花括号。
1)每个人都有自己喜欢的格式规则,但若在一个团队中工作,应该统一使用一种格式风格。
2)好的软件系统应该读起来是一致和顺畅的风格。
3)该书作者风格:见CodeAnalyzer.java
1)对象:把数据藏于抽象之后,暴露操作数据的函数;数据结构:直接暴露数据,没有提供有意义的函数。
2)过程式代码(使用数据结构):便于在不改动数据结构的前提下添加新函数;面向对象代码:便于在不改动既有函数的前提下添加新类。
3)迪米特法则:只和朋友谈话,不与陌生人谈话。类C的方法f只应该调用以下对象:C、由f创建的对象、作为参数传递给f的对象、由C的实体变量持有的对象。不应该调用任何函数返回的对象的方法。
4)数据传送对象(DTO):类似于java bean结构,对bean结构进行半封装,只暴露get接口。
1)使用异常而非返回值:每调用一个函数之后,就得判断该函数返回值是否有问题的方式,搞乱了代码逻辑,而且这个判断很容易忘记没写。遇到错误时,最好跑出一个异常,这样调用代码就很整洁,逻辑不会被错误处理搞乱。见下图:DeviceController.java
//使用返回值
public class DeviceController {
...
public void sendShutDown() {
DeviceHandle handle = getHandle(DEV1);
if(handle != DeviceHandle.INVALID) {
DeviceRecord record = retrieveDeviceRecord(handle);
if(record.getStatus() != DEVICE_SUSPENDED) {
pauseDevice(handle);
clearDeviceWorkQueue(handle);
closeDevice(handle);
} else {
logger.log("Device suspended. Unable to shut down");
}
} else {
logger.log("Invalid handle for : " + id.toString());
}
}
}
//使用异常代替返回值
public class DeviceController {
...
public void sendShutDown() {
try {
tryToShutDown();
} catch(DeviceShutDownError e) {
logger.log(e);
}
}
private void tryToShutDown() throws DeviceShutDownError {
DeviceHandle handle = getHandle(DEV1);
DeviceRecord record = retrieveDeviceRecord(handle);
pauseDevice(handle);
clearDeviceWorkQueue(handle);
closeDevice(handle);
}
private DeviceHandle getHandle(DeviceID id) {
...
throw new DeviceShutDownError("Invalid handle for : " + id.toString());
...
}
}
2)使用不可控异常:使用可控异常必须在catch语句和抛出异常处之间的每个方法签名中声明该异常。这意味着较低层级的修改会波及到较高层次的签名,破坏了封装。
3)给出异常发生的环境说明:可以从异常那里得到堆栈踪迹stack trace,从而了解异常的原因。
4)依调用者需要定义异常类:我们应用程序定义异常类时候,最重要考虑是:他们如何被捕获。
5)特例模式:创建一个类或配置对象,用来处理特例。你来处理特例,客户代码就不用应付异常行为了,异常行为被封装到特例对象中。
//原代码
try {
MealExpenses expenses = expenseReportDAO.getMeals(employee.getID());
m_total += expenses.getTotal();
} catch(MealExpensesNotFound e) {
m_total += getMealPerDiem();
}
//使用特例模式
MealExpenses expenses = expenseReportDAO.getMeals(employee.getID());
m_total += expenses.getTotal();
public class PerDiemMealExpenses impleMents MealExpenses {
public int getTotal() {
//return the per diem default
}
}
6)别返回null值:每行代码都在检查null值的应用程序糟糕透了,只要有一处没有检查null值,程序就会失控。
//原代码
List employees = getEmployees();
if(employees != null) {
for(Employee e : employees) {
totalPay += e.getPay();
}
}
//修改后
List employees = getEmployees();
for(Employee e : employees) {
totalPay += e.getPay();
}
//修改getEmployees方法,特殊情况返回空列表
public List getEmployees() {
if( ... there are no employees ...)
return Collections.emptyList();
}
7)别传递null值:当有人传递null值参数给一个函数,改如何处理,可以创建一个新异常类型并抛出;使用assert断言;但这两种方法均未解决问题,运行时候仍会错误。恰当的做法是禁止传入null值。
1)使用第三方代码:第三方库追求普适性,而使用者想集中满足特定需求的接口,这种差异可能导致系统边界上出现问题。例如java.util.Map中提供丰富接口,而我们希望使用者不要删除Map中的任何东西,但是java.util.Map中提供了clear接口。较好的方法是:使用Sensors类封装Map,隐藏细节,不暴露clear接口。——不建议总是用这种方式封装Map使用,而是建议不要将Map在系统中传递,当Map在这种边界时候,使用封装的方式更好。
2)整洁的边界:边界上的代码需要清晰的分割和定义了期望的测试。应该避免我们的代码过多的了解第三方代码中的特定信息。依靠你能控制的东西,好过依靠你控制不了的东西,免得日后受它控制。
1)TDD(测试驱动开发)三定律:
定律一:测试先行(在写功能代码之前,先写测试用例代码);
定律二:只写刚好无法通过的单元测试,不能编译也算不通过。
定律三:只写刚好通过当前失败测试用例的生产代码。
这三条定律将限制你在大概30秒的一个循环中。测试与生产代码一起编写,测试只比生产代码早些几秒钟。
2)保持测试整洁:
测试代码应该与生产代码一样重要,需执行同样的质量标准,脏测试等同于没有测试。测试代码必须随着生产代码的演进而修改。测试代码需要被思考、被设计、被照料,应该像生产代码一样保持整洁。
3)整洁的测试:三要素:可读性、可读性、可读性。单元测试用例中需要呈现:构造——操作——检验(Build——Operate——Check)模式。避免用细节误导或吓到使用者。
4)双重标准:测试代码在不考虑性能和内存的情况下,可以和生成代码不同,但是测试代码应当简单、精悍、足具表达力。
5)每个测试一个概念:每个测试一个断言是个好准则,但是每个测试只测试一个概念更好。
6)FIRST:整洁测试5条规则:快速(Fast)、独立(Independent)、可重复(Repeatable)、自足验证(Self-Validating)、及时(Timely)。
1)类应该短小:类的名称应该描述其职责,如果无法为某个类命以精确的名称,则这个类大概率太长了。类名中包括模糊的词:如Processor或Manager或Super等往往说明职责不清。
2)单一职责原则(SRP):类或模块应有且只有一条需要修改的理由。这句话说明了类的长度限制。
3)内聚:类应该只有少量实体变量,类中的每个方法都应该操作一个或多个这种变量。方法操作的变量越多,就越黏聚到类上。因此尽量保持方法和实体变量的粘性,若无法保证,则应该将这些变量和方法拆分到两个或多个类中,让新的类更为内聚。
4)为了修改而组织:对于多数系统,修改将一直持续。每一处的修改都冒着其他部分不能如期工作的风险。整洁系统中应该避免"打开"类进行修改,应该拓展系统而不是修改原有代码。
5)隔离修改:需求会改变,因此代码也会改变。具体类包含实现细节,而抽象类只呈现概念。需求改变往往会带来细节的改变,因此我们应该借助接口和抽象类隔离这些细节。
6)依赖倒置原则(DIP):类应该依赖于抽象而不是依赖于具体的细节。
1)将系统的构造与使用分开:构造和使用是非常不一样的过程。例如如下典型场景:就是所谓的“延迟初始化”,有一点好处是启动时间会更短,还能保证永远不会返回null值。
public Service getService() {
if(service == null)
service = new MyServiceImpl(...);
return service;
}
然而,我们也得到了 MyserviceImpl 及其构造器所需的一切的硬编码依赖。不分解这些依赖关系就无法编译。如果 MyserviceImpl 是个重型对象,则测试也会是个问题。
当然,仅出现一次的延迟初始化不算什么严重问题。但应用程序中往往许多类似的情况出现,我们不应该让这种小技巧影响整个系统设计。
2)分解main:将构造和使用分开的方法之一就是:将全部构造过程迁移到main或称为main的模块中。设计系统其余部分时候,假设所有对象都已经正确构造和设置。
3)依赖注入:对象不应负责实体化对自身的依赖,应将这个职责交给其他有权利的机制,从而实现控制的反转。
4)扩容:我们应该只去实现今天的用户故事,然后重构,明天在扩展系统实现新的用户故事。这是迭代和增量敏捷的精髓所在。
5)测试驱动系统架构:软件设计师,不需要像建筑设计师一样 "先做大设计(Big Design Up Front--BDUF)"。实际上对于软件设计师,BFUF是有害的,它会阻碍改进,因为架构上的方案会影响后续的设计思路。
6)优化决策:在巨大的系统中,不管是一座城市或一个软件项目,无人能做所有决策。最好是授权给最优资格的人决策,但是延迟决策至最后一刻也是好手段。如果决策太早,会缺少客户反馈、关于项目的思考和实施经验。
7)系统需要领域特定语言:在软件领域,领域特定语言(DSL)是一种段杜的小型脚本语言或以标准语言写的API,领域专家可以用它编写读起来像组织严谨的散文一般的代码。
1)“简单设计”的四条规则,一下规则按重要程度排列:
运行所有测试:只要系统可测试,就会导向保持类短小且目的单一的设计方案。测试消除了对清理代码就会破坏代码的恐惧。
不可重复:重复是拥有良好设计系统的大敌,“小规模复用”可大量降低系统复杂性,想要实现大规模复用,必须理解如何实现小规模复用。
表达了程序员的意图(表达力);软件项目的主要成本在于长期维护。代码应该清晰地表达作者的意图,作者把代码写的越清晰,其他人花在理解代码上的时间就越少,从而减少缺陷,缩减维护成本。
尽可能减少类和方法的数量;类和方法数量太多,有时是由毫无意义的教条主义导致。我们的目标是在保持函数和类短小的同时,保持整个系统的短小精悍。
1)为什么要并发:并发是一种解耦策略,它帮助我们把做什么(目的)和何时做(时机)分解开。解耦目的和时机明显的改进应用程序的吞吐量和结构。
2)迷思与误解:
对并发的误解:
<1>并发总能改进性能
<2>编写并发程序无需修改设计
<3>在采用Web或EJB容器时候,理解并发问题并不重要。
对并发中肯的说法:
<1>并发会在性能和编写额外代码上增加一些开销
<2>正确的并发是复杂的,即便对于简单问题也是如此
<3>并发缺陷并非总能复现,所以常被看做偶发事件而忽略,未被当作真的缺陷看待
<4>并发常常需要对设计策略的根本性修改
3)并发防御原则
单一职责原则(SRP):方法、类、组件应当只有一个修改的理由。并发设计自身足够复杂到成为修改的理由。
限制数据作用域:应该谨记数据封装,严格限制对可能被共享的数据的访问。
线程应尽可能地独立:让每个线程在自己的世界中存在,不与其他线程共享数据。
4)了解执行模型
生产者—消费者模型:一个或多个生产者线程创建某些工作,并置于缓存或队列中。一个或多个消费者线程从队列中获取并完成这些工作。生产者和消费者之间的队列是一种限定资源。
读者——作者模型:当存在一个主要为读者线程提供信息源,但只偶尔被作者线程更新的共享资源,吞吐量就会是个问题。挑战之处在于平衡读者线程和作者线程的需求,实现正确操作,提供合理的吞吐量,避免线程饥饿。
宴席哲学家:一群哲学家坐在圆桌旁,每个哲学家的左手边放了一把叉子。桌面中央摆着一大碗面。哲学家们思索良久,直至肚子饿了。每个人都要拿起叉子吃饭。但除非手上有两把叉子,否则就无法进食。
5)警惕同步方法之间的依赖:建议使用一个共享对象的多个方法。
6)保持同步区域微小:锁是昂贵的,应该尽可能少涉及临界区,尽可能减少同步区域。
7)很难编写正确的关闭代码:尽早考虑关闭问题,尽早令其工作正常,平静关闭很难做到。
8)测试线程代码:将伪失败(偶发事件)看做可能的线程问题;先使非线程代码可工作;编写可插拔的线程代码;编写可调整的线程代码;运行多于处理器数量的线程;在不同平台上运行;调整代码并强迫错误发生。
1)草稿:要编写整洁代码,必须先写肮脏代码,然后再清理它。
2)详细例子见原书籍《代码整洁之道》。