- 一、整洁代码
- 混乱的代价
- 有些团队在项目初期进展迅速,但是有那么一两年的时间却慢如蜗行。每次对代码的修改都影响到其他两三处的代码
- 花时间保持代码整洁不仅有关效率,还有关生存。
- 程序员遵从不了解混乱风险经理的意愿,也是不专业的做法(多听听需求,测试,用户的声音,表达自己的想法和实情)
- 制造混乱无助于赶上期限。混乱只会拖慢你,让你错过期限。
- 什么是整洁的代码?
- Bjarne Stroustrup,C++发明者:我喜欢优雅和高效的代码。代码逻辑应该直接了当,叫缺陷难以隐藏;尽量减少依赖关系,使之便于维护;依据某种分层战略完善错误处理代码;性能调至最优,省得引诱别人做没规矩的优化,搞出一堆混乱来。整洁的代码只做好一件事。
- Grady Booch,《面向分析与设计》:整洁的代码简单直接。整洁的代码如同优美的散文。整洁的代码从不隐藏设计者的意图,充满了干净利落的抽象和直接了当的控制语句。
- Dave Thomas,OTI公司创始人:整洁的代码应可由作者之外的开发者阅读和增补。它应有单元测试和验收测试。它使用有意义的命名。它只提供一种而非多种做一件事的途径。它只有尽量少的依赖关系,而且要明确地定义和提供清晰、尽量少的API。代码应通过其字面表达含义,因为不同的语言导致并非所有必须信息均可通过代码自身清晰表达。
- Michael Feathers,《修改代码的艺术》:我可以列出我留意到的整洁代码的所有特点,但其中有一条是根本性的。整洁的代码总是看起来像是某位特别在意它的人写的。几乎没有改进的余地。代码作者什么都想到了,如果你企图改进它,总会回到原点,赞叹某人留给你的代码——全心投入的某人留下的代码。
- Ron Jeffries,《极限编程实施》:简单代码,依其重要顺序:能通过所有测试;没有重复代码;体现系统中的全部设计理念;包括尽量少的实体,比如类、方法、函数等
- Ward Cunningham,Wiki发明者:如果每个例程都让你感到深合已意,那就是整洁代码。如果代码让编程语言看起来像是专为解决那个问题而存在,就可以称之为漂亮的代码。
- 有趣的现象
- 读与写时间的比例超过10:1.写新代码的时候,我们一直在阅读旧代码。
- 童子军军规
- “让营地比你来时更干净”
- 如果每次签入时,代码都比签出时干净,那么代码就不会腐坏
- 二、有意义的命名
- 名副其实
- 一旦发现有更好的名称,就换掉旧的
- 变量、函数或类名称应该已经答复了所有的大问题
- 代码的模糊度:即上下文在代码中未被明确体现的程度
- 避免误导
- 程序员必须避免留下掩盖代码本意的错误线索。应当避免使用与本意相悖的词
- 以同样的方式拼写出同样的概念才是信息,拼写前后不一致就是误导
- 要注意使用小写字母i和大写字母O作为变量名,看起来像“壹”和“零”
- 做有意义的区分
- 不要为了满足编译器和解释器的需求而写代码(如 a,b;虽然满足编译器和解释器,但是不方便阅读)
- 同一作用范围内两样不同的东西不能重名,如果名称必须相异,那其意思也应该不同才对
- 废话是另一种没意义的区分。假设你有一个Product类,如果还有一个ProductInfo或ProductData类,那它们的名称虽然不同,意思却无区别
- 只要体现出有意义的区分,使用a和the这样的前缀就没错
- 废话都是冗余。Variable一词记录不应当出现在变量名中,Table一词永远不应当出现在表名中
- 使用读的出来的名称(名称要是可阅读和描述的)
- 使用可搜索的名称
- 单字母名称和数字常量有个问题,就是很难在一大篇文字中找出来(能够全局搜索出来)
- 名称的长短应与其作用域大小相对应
- 避免使用编码
- 把类型或作用域编进名称里面,徒然增加了解码的负担
- 避免思维映射
- 不应当让读者在脑中把你的名称翻译为他们熟知的名称
- 专业程序员了解,明确是王道
- 类名
- 类名对象名应该是名词或者名词短语
- 方法名
- 方法名应当是动词或动词短语。修改器和断言应该根据其值命名,并依Javabean标准加上get、set和is前缀
- 可以考虑将相应构造器设置为private,强制使用这种命名手段
- 别扮可爱(不用什么幽默词语,也就是只有自己能懂的词)
- 言到意到,意到言到
- 每个概念对应一个词
- 别用双关语
- 避免将一个词用于不同的目的
- 应尽力写出易于理解的代码,把代码写得让别人能一目尽览而不必殚精竭虑地研究
- 使用解决方案领域名称
- 尽管用那些计算机科学术语、算法名、模式名、数学术语(因为阅读代码的人,一定与你有相同的技术背景)
- 使用源自所涉问题领域的名称
- 如果不能用程序员熟悉的术语来给手头的工作命名,就采用从所涉问题领域而来的名称
- 优秀的程序员和设计师,其工作之一就是分离解决方案领域和问题领域的概念
- 添加有意义的语境
- 需要用有良好命名的类、函数或名称空间来放置名称,给读者提供语境
- 如果没这么做,给名称添加前缀就是最后一招了
- 不要添加无用的语境
- 只要短名称足够清楚,就要比长名称好
- 最后的话
- 取好名字最难的地方在于需要良好的描述技巧和共有文化背景
- 三、函数
- 短小
- 函数的第一规则是要短小。第二条规则是还要更短小
- if语句、else语句、while语句等,其中的代码块应该只有一行,该行大抵是一个函数调用语句
- 函数不应该大到足以容纳嵌套结构,所以,函数的缩进层级不该多于一层或两层
- 只做一件事
- 函数应该做一件事,做好这件事。只做这件事
- 要判断函数是否不止做了一件事,就是看看是否能再拆出一个函数,该函数不仅只是单纯地重新诠释其实现
- 只做一件事的函数无法被合理地切分为多个区段
- 每个函数一个抽象层级
- 要确保函数只做一件事,函数中的语句都要在同一抽象层级上
- 自顶向下读代码:向下规则,让代码拥有自顶向下的阅读顺序,让每个函数后面都跟着下一抽象层级的函数
- switch语句
- 写出短小的switch语句很维,写出只做一件事的switch语句也很难,Switch天生要做N件事
- 将switch语句埋到抽象工厂底下,不让任何人看到
- 如果只出现一次,用于创建多态对象,而且隐藏在某个继承关系中,在系统其他部分看不到,就还能容忍
- 使用描述性的名称
- 沃德原则:“如果每个例程都让你感到深合已意,那就是整洁代码”
- 函数越短小,功能越集中,就越便于取个好名字
- 别害怕长名称。长而具有描述性的名称,比短而令人费解的名称好。
- 命名方式要保持一致。使用与模块名一脉相承的短语、名词和动词给函数命名
- 函数参数
- 最理想的参数数量是零,有足够的理由才能用三个以上参数
- 从测试角度看,参数甚至更叫人为难(需要测试用例,需要兼容所有参数可能性,如3参数,每个有4种可能;就需要4*4*4=64种测试用例)
- 事件:在这种形式中,有输入参数而无输出参数,程序将函数看作一个事件,使用该参数修改系统状态
- 转换:如果函数要对输入参数进行转换操作,转换结果就该体现为返回值
- 标识参数:向函数传入布尔值会使方法签名立刻变得复杂起来,大声宣布函数不止做一件事
- 如果函数看来需要两个、三个或三个以上参数,就说明其中一些参数应该封装为类了
- 有可变参数的函数可能是一元、二元甚至三元,超过这个数量就可能要犯错了
- 对于一元函数,函数和参数应当形成一种非常良好的动词/名词对形式
- 无副作用
- 函数承诺只做一件事,但还是会做其他被藏起来的事,会导致古怪的时序性耦合及顺序依赖
- 参数多数会被自然而希地看作是函数的输入
- 分隔指令与询问
- 函数要么做什么事,要么回答什么事,但二者不可得兼
- 使用异常替代返回错误码
- 从指令式函数返回错误码轻微违反了指令与询问分隔的规则。它鼓励了在if语句判断中把指令当作表达式使用
- try/catch代码块把错误处理与正常流程混为一谈,最好把try和catch代码块的主体部分抽离出来,另外形成函数(throw exception也一样)
- 错误处理就是一件事,处理错误的函数不该做其他事
- 依赖磁铁(dependency magnet):其他许多类都得导入和使用它
- 别重复自己
- 重复是软件中一切邪恶的根源。许多原则与实践规则都是为控制与消除重复而创建
- 结构化编程
- 每个函数、函数中的每个代码块都应该有一个入口、一个出口。遵循这些规则,意味着在每个函数中只该有一个return语句,循环中不能有break或者continue语句,而且永永远远不能有任何的goto语句
- 有在大函数中这些规则才会有明显好处,因为,只要函数保持短小,偶尔出现的return、break或continue语句没有坏处,goto语句尽量避免(条件1,一定要满足条件2)
- 如何写出这样的函数
- 打磨代码,分解函数、修改名称、消除重复
- 缩短和重新安置方法、拆散类、保持测试通过
- 四、注解
- 注解的缺点
- 若编程语言足够有表达力,就不需要注释
- 注释的恰当用法是弥补我们在用代码表达意图时遭遇的失败。注释总是一种失败
- 程序员应当负责将注释保持在可维护、有关联、精确的高度,更应该把力气用在写清楚代码上,直接保证无须编写注释
- 不准确的注释要比没注释坏得多
- 注释不能美化糟糕的代码
- 带有少量注释的整洁而有表达力的代码,要比带有大量注释的零碎而复杂的代码像样得多
- 与其花时间编写解释你搞出的糟糕的代码的注释,不如花时间清洁那堆糟糕的代码
- 用代码来阐释
- 用代码解释你大部分的意图,很多时候,简单到只需要创建一个描述与注释所言同一事物的函数即可
- 好注释
- 法律信息
- 提供信息的注解
- 对意图的解释:提供某个决定背后的意图
- 阐释:注释把某些晦涩难懂的参数或返回值的意义翻译为某种可读形式
- 警示:有时,用于警告其他程序员会出现某种后果的注释也是有用的
- TODO注释:注意要清理
- 放大:放大某种看来不合理之物的重要性
- 共API中的Javadoc
- 坏注释
- 喃喃自语
- 多余的注释
- 误导性注释
- 循规式注释
- 日志式注释
- 废话注释
- 可怕的废话
- 能用函数和变量时就别用注释
- 位置标记
- 括号后面的注释
- 归属与署名
- 注释掉的代码
- HTML注解
- 非本地信息
- 信息过多
- 不明显的联系
- 函数头
- 非公共代码的javadoc
- 范例
- 五、格式
- 格式的目的
- 格式关乎沟通,而沟通是专业开发者的头等大事
- 垂直格式
- 短文件比长文件易于理解
- 源文件也要像报纸文章那样。名称应当简单且一目了然。名称本身应该足够告诉我们否在正确的模块中。源文件自顶部应该给出高层次概念和算法。细节应该往下渐次展开,直至找到源文件中最底层的函数和细节。
- 几乎所有的代码都是从上往下读,从左往右读。每行展现一个表达式或一个子句,每组代码行展示一条完整的思路。
- 紧密相关的代码应该互相靠近
- 除非有很好的理由,否则就不要把关系密切的概念放到不同的文件中,实际上,这也是避免使用protected变量的理由之一,应避免迫使读者在源文件和类中跳来跳去
- 变量声明应尽可能靠近其使用位置,在函数顶部出现,循环的控制变量总是在循环语句中声明
- 实体变量在类的顶部声明
- 相关函数。若某个函数调用了另一个,就应该把他们放在一起,而且调用者应该尽可能放在被调用者上面
- 概念相关。概念相关的代码应该放在一起。相关性越强,彼此之间的距离就应该越短
- 我们想自上向下展示函数调用依赖顺序,被调用的函数应该放在执行调用的函数下面,这就建立了一种自顶向下贯穿源代码模块的良好信息流(能一开始就读懂整个程序的大脉络,不用沉溺于细节中)
- 横向格式
- 尽力保持代码行短小,遵守无需拖动滚动条到右边的原则,最好不要超过120个字符
- 使用空格字符将彼此紧密相关的事物连接到一起,也用空格字符把相关性较弱的事物分开
- 对齐,像是在强调不重要的东西,把目光从真正的意义上拉开
- 如果有较长的列表需要做对齐处理,那问题就是在列表的长度上而不是对齐上
- 程序员相当依赖缩进模式
- 有时,while或for语句的语句体为空,如果无法避免,就确保空范围体的缩进,用括号包围起来
- 团队原则
- 每个程序员都有自己喜欢的格式规则,但如果在一个团队中工作,就是团队说了算。
- 好的软件系统是由一系列读起来不错的代码文件组成的。他们需要拥有一致和顺畅的风格。
- 六、对象和数据结构
- 数据抽象
- 隐藏实现关乎抽象,类并不简单地用取值器和赋值器将其变量推向外部,而是曝露抽象接口,以便用户无需了解数据的实现就能操作数据本体
- 数据对象的反对称性
- 对象吧数据隐藏于抽象之后,暴露操作数据的函数。数据结构暴露其数据,没有提供有意义的函数。
- 对象与数据结构之间的二分原理:
- 过程式代码(使用数据结构的代码)便于在不改动既有数据结构的前提下添加新函数。面向对象代码便于在不改动既有函数的前提下添加新类
- 过程式代码难以添加新数据结构,因为必须修改所有函数。面向对象代码难以添加新函数,因为必须修改所有类
- 得墨忒耳律(The Law of Demeter 也称迪米特法则)
- 模块不应了解它所操作对象的内部情形
- 方法不应调用任何函数返回的对象的方法
- link:https://blog.csdn.net/vking_wang/article/details/8455636
- 混合结构,一半是对象,一半是数据结构,应避免这种结构
- 数据传送对象
- 最精炼的数据结构,是只有一个公共变量,没有函数的类。这种数据结构有时候被称为数据传送对象,或DTO(Data Transfer Objects)。在与数据库通信、或解析套接字传递的消息之类场景中充当翻译角色
- Active Record:一种特殊的DTO形式。他们是拥有公共(或可豆式访问的)变量的数据结构,但是通常也会拥有类似save和find这样的可浏览方法。
- 七、错误的处理
- 原则
- 错误处理很重要,但如果它搞乱了代码逻辑,就是错误的做法
- 使用异常而非返回码
- 遇到错误时,最好抛出一个异常。调用代码很整洁,其逻辑不会被错误处理搞乱
- 先写Try-Catch-Finally
- 异常的妙处之一是,它们在程序中定义了一个范围。执行try-catch-finally语句中try部分的代码时,你是在表明可随时取消执行,并在catch语句中接续
- 在某种意义上,try代码块就像是事务,catch代码块将程序维持在一种持续状态
- 在编写可能抛出异常的代码时,最好先写try-catch-finally语句,能帮你定义代码的用户应该期待什么,无论try代码块中执行的代码出什么错都一样
- 使用不可控异常
- 可控异常的代价就是违反开放、闭合原则,得在catch语句和抛出异常处之间的每个方法签名中声明该异常
- 可控异常意味着对软件中较低层级的修改,都将波及较高层级的签名
- 给出异常发生的环境说明
- 抛出的每个异常,都应当提供足够的环境说明,以便判断错误的来源和处所
- 应创建信息充分的错误消息,并和异常一起传递出去
- 依调用者需要定义异常类
- 最重要的考虑是它们如何被捕获
- 将第三方API打包是个良好的实践手段,降低了对每个第三方的依赖,也有助于模拟第三方调用
- 定义常规流程
- 特例模式(SPECIAL CASE PATTERN,[Fowler]),创建一个类或配置一个对象,用来处理特例,异常行为被封装到特例对象中
- 别返回null值
- 返回null值,基本是在给自己增加工作量,也是在给调用者添乱,只要有一处没检查null值,应用程序就会失控
- 别传递null值
- 将null值传递给其他方法更糟糕,除非API要求你向它传递null值,否则就要尽可能避免传递null值
- 八、边界
- 使用第三方代码
- 第三方程序包和框架提供者追求普适性,这样就能在多个环境中工作,吸引广泛的用户
- 我们建议不要将Map(或在边界上的其他接口)在系统中传递,把它保留在类或近亲类中,避免从API中返回边界接口,或将接口作为参数传递给公共API
- 浏览和学习边界(学习新的第三方代码,需要些学习性测试用例去熟悉它)
- 学习性测试的好处不只是免费
- 学习性测试毫无成本,编写测试是获得这些知识(要使用的API)的容易而不会影响其他工作的途径
- 学习性测试确保第三方程序包按照我们想要的方式工作
- 使用尚不存在的代码
- 编写我们想得到的接口,好处之一是它在我们控制之下,有助于保持客户代码更可读,且集中于它该完成的工作(根据需求,编写我方使用的接口,新接入的代码按照这个标准实现功能)
- 整洁的边界
- 边界上的改动,有良好的软件设计,无需巨大投入和重写即可进行修改
- 边界上的代码需要清晰的分割和定义了期望的测试。应该避免我们的代码过多地了解第三方代码中特定信息。依靠你能控制的东西,好过依靠你控制不了的东西,免得日后受它控制
- 使用ADAPTER模式将我们的接口转换为第三方提供的接口
- 九、单元测试
- TDD三定律
- 定律一 : 在编写不能通过的单元测试之前,不可编写生产代码
- 定律二:只可编写刚好无法通过的单元测试用例,不能编译也不算通过
- 定律三:只可编写刚好足以通过当前失败测试的生产代码
- 保持测试整洁
- 脏测试等同于没测试,测试必须随生产代码的演进而修改,测试越脏,就越难修改
- 测试代码和生产代码一样重要,它需要被思考、被设计和被照料,它该像生产代码一般保持整洁
- 如果测试不能保持整洁,你就会失去它们,没有了测试,你就会失去保证生产代码可扩展的一切要素
- 整洁的测试
- 三要素:可读性,可读性和可读性,明确、简洁还有足够的表达力
- 构造-操作-检验(BUILD-OPERATE-CHECK)模式:第一个环节构造测试数据,第二个环节操作测试数据,第三个部分检验操作是否得到期望的结果
- 守规矩的开发者也将他们的测试代码重构为更加简洁和具有表达力的形式
- 每个测试一个断言
- JUnit中每个测试函数都应该有且只有一个断言语句
- 每个测试一个概念
- 尽可能减少每个概念的断言数量,每个测试函数只测试一个概念
- F.I.R.S.T
- 快速(Fast)测试应该快
- 独立(Independent)测试应该相互独立
- 可重复(Repeatable)测试应当可在任何环境中重复通过
- 自足验证(Self-Validating)测试应该有布尔值
- 及时(Timely)测试应该及时编写
- 十、类
- 类的组织
- 类应该从一级变量列表开始,如果有公共静态变量,应该先出现,然后是私有静态变量,以及实体变量,很少会有公共变量
- 公共函数应该跟在变量列表之后
- 保持变量和工具函数的私有性,但并不执着于此
- 类应该短小
- 第一规则是类应该短小,第二规则是还要更短小
- 衡量方法,计算权责(responsibility)
- 类的名称应当描述其权责,如果无法为某个类命以精确的名称,这个类大概就太长了,类名越含混,该类越有可能拥有过多权责
- 单一责权原则(SRP):类和模块应有且只有一条加以修改的理由。
- 系统应该由许多短小的类而不是少量巨大的类组成,每个小类封装一个权责,只有一个修改的原因,并与少数其他类一起协同达成期望的系统行为
- 方法操作的变量越多,就越黏聚到类上,如果一个类的每个变量都被每个方法所使用,则该类具有最大的内聚性
- 保持函数和参数列表短小的策略,有时会导致为一组子集方法所用的实体变量数量增加。出现这种情况时,往往意味着至少有一个类要从大类中挣扎出来。你应当尝试将这些变量和方法分拆到两个或多个类中,让新的类更为内聚
- 将大函数拆为许多小函数,往往也是将类拆分为多个小类的时机
- 为了修改而组织
- 在整洁的系统中,我们对类加以组织,以降低修改的风险
- 开放-闭合原则(OCP):类应当对扩展开放,对修改封闭
- 在理想系统中,我们通过扩展系统而非修改现有代码来添加新特性
- 部件之间的解耦代表着系统中的元素相互隔离得很好
- 依赖倒置原则(Dependency Inversion Principle,DIP),类应该依赖于抽象而不是依赖于具体细节
浙公网安备 33010602011771号