导图社区 代码整洁之道
代码的质量和其整洁度成正比,干净的代码,既在质量上较为可靠,也为后期的维护,升级奠基了良好的基础。优美整洁的前提是对逻辑的充分理解,是对代码的精确封装,合理重构,是对每一行代码的深思熟虑,是为了给出一个正确的解决方案,而不是给出一堆可以运行的代码。坚守让营地比你来时更干净的原则,遵守整洁代码的规则,坚信细节之中自有天地,整洁成就卓越代码。
编辑于2020-12-04 10:17:47代码整洁之道
代码部分
有意义的命名
名副其实
如果名称需要注释来补充,就不算名副其实
明确才是王道
长名称胜于短名称,搜索得到的名称胜于自造编码代称
函数的目的不应该需要结合上下文才能表达
自己能够表达自己
取个好名字更好的解释函数的意图以及参数的顺序和意图
命名规则保持一致
定值要定义成常量表示,通过常量名来表达意思,不要突兀直接使用定值
避免误导
集合+类型命名的误导
变量命名细微差别的误导
变量和其他专有名词一致造成的误导
变量识别区不到导致的误导 【1和 l :一和L 0和 o:零 和 O】
不用双关语
命名必须表达出开发者的真实意图
不要以数字系列或是废话【多个不同的变量表达的是同一个意思】进行区分
使用读得出来的名称来命名
不要乱造词
使用可搜索的名称
MAX_MENU_ITEM 比 15 好搜索
避免使用编码
不要在变量上加上变量类型,避免类型改变造成的误导
不用俚语,俗语命名
一个概念一个词
同一个抽象概念在不同的类中用一个词来表示
使用解决方案领域名称
使用计算机科学术语、算法名、模式名、数学术语命名
添加有意义的语境
例如添加前缀来明确变量的含义
不要加毫无意义的语境(例如给所有的类都加上项目前缀)
命名用词
方法名:动词或是动词短语
类名和对象名:名称或是名称短语
函数
第一原则:短小
最多10行
函数的缩进层不应该多于一层或是两层
函数只做一件事
函数只做了改函数名下同一抽象层次上的步骤,则该函数还是只做了一件事
判断是否只做了一件事:看函数是否还能在拆分出一个函数来
不要产生副作用,做本职工作之外的事
每个函数一个抽象层级
向下规则:让每个函数后面都跟着位于下一抽象层次的函数
函数要么做什么事,要么回答什么事【要么修改什么,要么返回什么】,不要二者兼得
if 语句、else语句、while语句等,其中的代码应该只有一行
switch
为做N件而生,但是也是造成函数庞大的原因之一
原则:能抽象就抽象,能多态处理就多态处理,不要图省事为可以抽象的类写不同的函数在switch中进行调用
函数参数
尽量避免三个参数
一元函数
普遍形式
处理传入的参数
将传入的参数进行转换的到其他的返回值
事件
标识参数:尽量不要向函数中传入Bool类型的值
尽量使用一元函数,尽量避免三元函数
参数对象
三个或是三个以上的参数就需要进行封装了
尽量不要使用输出参数,用返回值代替
使用异常代替返回错误码
抽离try~catch代码块
把try和catch代码块的主题部分抽离出来,另外形成函数
错误处理就是一件事
别重复自己
先写在优化
类
组织顺序
变量列表->公共函数
变量顺序:公共静态常量->私有静态变量->私有实体变量
公共函数调用的私有函数紧随其后
类应该短小
如果无法为类命名以精确的名称,这个类就太长了
通过权责来衡量类的大小
系统应该由许多短小的类而不是少量巨大的类组成,每个小类封装一个权责,只有一个修改理由,并与少数其他类一起协同达到期望的系统行为
单一权责原则(SRP)
类或是模块应该有且只有一条加以修改的理由
鉴别权责帮助我们在代码中认识到并且创建出更好的抽象
让软件能工作和让软件保持整洁是两种截然不同的工作
内聚
类应该是有少量实体变量,且类中每个函数都应该操作一个或是多个这种变量
类的每个变量都被每个函数所使用,该类就具有最大的内聚性
内聚性高意味着类中的方法和变量相互依赖、相互结合成一个逻辑整体
当类丧失了内聚性就拆分它
为了修改而组组
类的组织应该满足开闭原则:对扩张展开放,对修改关闭
理想状态是通扩展系统而非修改现有代码来添加新功能
隔离修改
具体类包含细节,抽象类只呈现概念
依赖具体类的客户类,细节发生改变时,就会有风险,可以借助接口和抽象类俩隔离这些细节带来的影响
类设计原则:依赖倒置
类应该依赖抽象而不是具体细节
面向抽象编程
错误处理
错误处理不应该搞乱代码的逻辑,否者就是错误的做法
错误处理和主要逻辑应该各自独立
整洁技巧
先写try……catch……finally语句
try代码块就像是事务,catch代码块将程序维持在一种持续状态
有助于定义代码用户应该期待什么
有助于维护好该范围的事务特征
使用异常而非返回码
会搞乱调用者代码
使用不可控异常
因为可控异常打破了封装,高层函数调用底层函数必须知道底层函数的异常细节
给出异常发生的环境说明
方便判断错误的来源和处所
应该创建信息充分的错误信息[包含失败操作和失败类型],并且和异常一起传递出去,有日志系统,就传递足够的信息到Catch中,并记录下来
依调用者需要定义异常类
对错误的分类
按来源
组件
其他
按类型
设备
网络
编程
对于定义异常类时,最重要的考虑应该是它们如何被捕获
将第三方API打包是个良好的实践手段。当你打包一个第三方API,你就降低了对它的依赖:未来你可以不太痛苦地改用其他代码库
使用特例模式
创建一个类或是配置一个对象,用来处理特例,你来处理特例,客户端就不需要在应付异常行为了,异常行为被封装到了特例对象中去了
空对象模式:
一种处理null值的设计模式
抽象出来一个接口,实体类和空实体类都继承于该接口,在实体被调用在可能出现null的位置用空实体对象去代替。
别返回null
会增加工作量,也会给调用方增加麻烦
如果是调用第三方的API中可能返回null值的函数,可以考虑用心方法打包这个函数,新方法中抛出异常或是返回特例对象
别传递null
除非特殊需要,否则不要传递null值
格式
目的
为了更好的沟通以及后续程序的扩展,提高了代码的可读性
保持良好的代码格式
选用一套管理代码格式的简单规则
沟通才是专业开发者的头等大事
垂直格式
单个文件行数:200行~500行
不同概念之间预留空行进行概念区分。例如:函数之间
紧密相关联的代码相互靠近
变量相互靠近,变量和函数之间空格间隔
变量申明应该尽可能靠近其使用位置
本地变量(局部变量)应该在函数的顶部出现
实体变量应该在类的顶部申明
相关函数应该放在一起,调用者尽可能放在被调用者上面
概念相关的代码应该放在一起
横向格式
每行8-~120个字符
在赋值操作符周围加上空格
缩进
遵循团队规则
注释
尽量少用注释,在代码上多下功夫让代码本身能够表达自己的意图【注释不会被坚持维护,会造成注释和函数的错位】
注释是为了解释糟糕的代码:在需要加注释的时候,先想办法优化代码,无能为力了才进行注释 说明
唯一真正好的注释是你想办法不去写注释
好注释
法律信息
提供信息的注释
例如抽象方法的返回值
对意图的解释
阐释
警告
todo
要做的工作列表
放大某种看起来不合理之物的重要性
API的说明文档
坏注释
无关痛痒的注释
误导性注释
函数头注释
日志式注释
标记位置:使用后记得删除
括号后面为了区分模块结束位置的注释
归属和签名
无用的信息
注释掉的代码
系统部分
单元测试
测试驱动开发(TDD)
自动化单元测试
TDD三定律
在编写不可通过的单元测试前,不可编写生产代码
只可编写刚好无法通过的单元测试,不能编译也算不通过
只可编写刚好足以通过当前失败测试的生产代码
保持测试整洁
测试代码和生产代码一样重要,测试代码需要被思考、被设计、被照料,需要像生产代码一样保持整洁
测试的好处
单元测试让代码可扩展、可维护、可复用。
测试能让开发没有后患的进行代码修改
整洁的测试
可读性是整洁测试的唯一要素
测试语言技巧
构造-操作-检验
双重标准
5个原则
快速
运行速度要快
独立
测试间相互独立,可以单独运行每个测试,以及任何顺序
可重复
可在任何环境中重复通过
自足验证
应该有bool值输出,无论是通过还是不通过
及时
测试需要及时编写
单个测试中的断言数量应该最小化
每个测试一个概念
对象和数据结构
数据抽象
隐藏实现并非只在变量之间放上一个函数层那么简单,隐藏关乎抽象,类并不是简单地用取值器和赋值器将其变量推向外间,而是暴露抽象接口,方便用户无需了解数据的实现就能操作数据本体
使用抽象形态表述数据,以最好的方式呈现某个对象包含的数据
数据、对象的反对称性
对象和数据结构之间的二分原理
过程式代码(使用数据结构的代码)便于在不改动既有数据结构的前提下添加新函数、面向对象代码便于在不改动既有函数的前提下添加新类
过程式代码难以添加新数据结构,因为必须修改所有的函数,面向对象代码难以添加新函数,因为必须修改所有类
复杂系统中
需要添加新数据类型
面向对象编程和对象更适合
需要添加新函数
过程式编码和带护具结构更适合
得墨忒耳律
模块不应该了解它所操作对象的内部情况
对象隐藏数据,暴露操作
对象不应该通过存取器暴露其内部结构
类C中的函数F只应该调用4类对象的函数
类C内部的函数
由F创建的对象
作为参数传递给F的对象
由C的实体变量持有的对象
不要和陌生人谈话
链式调用
不要链式调用函数
可以链式使用数据结构
避免出现对象和数据结构的混合结构
数据传送对象
最为精炼的数据结构:是只有一个公共变量、没有函数的类
被称为数据传送对象(DTD)
在数据库通信或是解析套接字传递信息之类的场景中十分有用
系统
目的:在系统城层面上保持整洁
将系统的构建和使用分开
对象的创建和使用分离开来
分解main
main函数或是称之为main的模块中负责创建所需对象,然后传递到应用程序中,应用程序只管使用
工厂
使用工厂模式来让应用程序控制何时创建对象,具体的创建由工厂实现,应用程序只是在适当的时候获取到对象实体而已
依赖注入
扩容
面向切面编程(AOP)是一种扩容的普适手段
测试驱动系统架构
优化决策
明智使用添加了可论证价值的标准
系统需要领域特定语言[DSL]
迭进
通过跌进设计达到整洁目的
软件系统设计四条原则
运行所有测试
只要系统可测试
就会导向保持类短小且目的单一的设计方案
耦合度高的代码难以写测试,可测试意味着耦合度的降低
不可重复
重复
代码相同或是相似
实现上的重复
一个类中有一个size 函数一个isEmpty() 函数 ,可以在isEmpty中 是直接使用来size 与0 的关系来进行判断是否为空
想要大规模复用,就得理解如何实现小规模复用
模板方法是一种移除高程级重复的通用技巧
表达力
理解系统是做什么的,才能在修改的时候降低出现缺陷的可能性
代码应该清楚的表达作者的意图
尽可能少的类和方法
重构
内容:提升内聚性,降低耦合度,切分关注面,模块化系统性关注面,缩小函数和类的尺寸,选择更好的名称
在重构的过程中:消除代码重复,保证表达力,尽可能减少类和函数的数量
对接部分
边界
使用第三方代码
不需要每个需要的地方都直接与第三方接口进行直接的借出,而是在第三方接口外面封装一层,系统直接用封装后的类进行第三方接口的使用,需要隐藏的接口或是需要进行处理的细节都在改封装内中进行处理,系统的调用方不再关心细节的处理,直接使用即可
使用适配器模式进行处理
浏览和测试边界
为要使用的第三方代码编写测试
通过编写测试来遍览和理解第三方代码
学习性测试
确保第三方程序按照我们想要的方式进行工作
方便及时发现更新内容
使用尚不存在的代码
如果需要与第三方进行对接(第三方接口还没出来),可以先定义与第三方进行封装的接口,然后本地根据改接口可以直接先进行对接后的后续工作处理,等第三方开发好之后,在来处理封装类的细节
整洁边界
在使用第三方接口时:依赖可控制的东西好过于你控制不了的东西
使用适配器模式来处理三方接口
将三方接口进行封装,系统通过封装接口间接使用三方接口
整体总结
味道和启发
注释
注释应该是谈及代码自身没有提到的东西
坏味道
不恰当的注释
注释只应该描述有关代码和设计的技术性信息
废弃注释
不要编写,及时更新或是删掉
冗余注释
描述 能够充分自我描述的的东西的注释
糟糕的注释
需要写注释就把它写好
注释掉的代码
发现及时删掉
环境
坏味道
需要多步才能实现的构造
构建系统应该是单步的小操作,单个指令迁出系统,单个指令构建系统
需要多步才能做到的测试
单个指令能够执行全部单元测试
函数
坏味道
参数过多
参数列表的参数不应该超过3个
输出参数
函数要么修改什么,要么返回什么,只做一件事
标识参数
不要传入bool类型的参数
死函数
永不调用的函数,立即删掉
一般性问题
坏味道
一个源文件中存在多种语言
尽量减少源文件中额外语言的数量和范围
明显的行为未被实现
函数应该实现其他开发人员有理由期待的行为
不正确的边界行为
不能相信直觉
在写出能工作的函数的前提下,需要努力去证明代码在所有的角落和边界情况下真能工作
每种边界条件
每种极端情况
忽略安全
关注编译器的警告
及时处理失败测试
重复
每次看到重复的代码-都代表遗漏了抽象
重复最明显的形态是你看到一样的代码
进行抽象
重复较隐蔽的形态是在不同的模块中不断出现、检测同一组条件的switch/case 和 if/else链
用多态来代替
重复更隐蔽的形态是类似的算法但具体代码行不同的模块
可用模板方法模式-策略模式来修正
在错误的抽象层级上的代码
较低层级概念和较高层级概念不应该混杂在一起
只需细节实现有关的常量、变量和函数不应该在基类里出现
基类依赖于派生类
信息过多
限制类或是模块中暴露的接口的数量
类拥有的实体变量越少越好
类中函数越少越好
函数知道的变量越少越好
死代码
不执行的代码
从不发生的条件的if语句中找
从不抛出异常的try……catch语句中找
不被调用的小函数中找
从switch/case语句中找
垂直分割
变量和函数应该在靠近被使用的地方定义,本地变量应该正好在其首次被使用的位置上面申明,垂直距离要短
私有函数应该在首次被调用的位置下面定义
前后不一致
用同一个变量来存相同的对象
特定函数中用某个变量来存现指定对象,当在其他函数中用到该对象时,应该用同一个变量来进行存储
混淆视听
没有实现的默认构造函数
没有用到的变量、函数
没有信息量的注释
及时删掉
人为耦合
不相互依赖的东西不耦合
人为耦合:指两个没有直接目的之间的模块之间的耦合
例如没有直接父子关系类直接的继承关系
特性依赖
类中的函数只应该对其所属类中的变量和函数感兴趣,不该垂青其他类中的变量和汉函数
当方法通过某个其他对象的访问器和修改器来操作该对象内部数据,那么该函数就依赖改对象所属的类的范畴
有时候这种依赖又是必要的,按需抉择
选择算子参数
选择算子
任何一种是用于选择函数行为的参数
枚举
整数
bool
……
选择算子参数只是一种避免把大函数拆分为多个小函数的偷懒做法
晦涩的意图
代码要尽可能具有表现力
位置错误的权责
代码应该放在读者自然而然其他它所在的地方
放在合适的位置而不是自己觉得方便的位置
不恰当的静态函数
不要编写不该是静态的静态函数
通常用非静态函数,有疑问就用非静态函数
非要用静态函数,保证该函数没机会有多态行为
掩蔽时序耦合
某系操作需要按照一定的顺序来进行,那么就不要掩蔽他们之间的顺序关系
可以通过创建顺序队列来暴露时序耦合
var a= XX; var b = XX(a);var c = XX(b)
好味道
使用解释性变量
多用解释性变量
函数名称应该表达其行为
好的名称是可以通过名称看到函数行为的
理解算法
写之前需要了解它是怎么工作的,而不是一味的只想让它可以工作
最好途径:重构
你得知道你的解决方案是正确的,而不是可以用
把逻辑依赖改为物理依赖
一个模块依赖于另一个模块,该依赖应该是物理上的而不是逻辑上的
例如:一个类A负责把数据收集为某种格式的形式,传递到类B中,在打印处理,这里涉及到页面大小的问题,页面大小就不应该在A中直接指定,而是通过一个函数来物理化这种依赖
用多态来替代if/else或是 switch/case
对于给定的选择类型,不应有多于一个switch语句。在那个switch语句汇总的多个case,必须创建多态对象,取代系统中其他类似switch语句
遵循标准约定
每个团队都应该遵循基于通用行业规范的一套编码标准
用命名常量替代魔术数
魔术数:指不能自我描述的符号
准确
代码中的含糊和不准确要么是意见不同,要么是源于懒惰
在代码做决定时,确认自己足够准确,明确自己为何要这么做,遇到异常要怎么处理
结构甚于约定
命名约定次于强制性结构
良好命名的枚举的switch/case 要次于 拥有抽象函数的基类
封装条件
应该把解释了条件意图的函数抽离出来
if(shouldBeDelete(timer)) 要优于 if( timer.hasExired() && !timer.isRecurrent())
避免否定性条件
if(buff.shouldCompact()) 要优于 if( !buff.shouldNotCompact() )
函数只做一件事
别随意
构建代码需要理由,且理由和代码结构相契合
封装边界条件
把处理边界条件的代码集中在一处,不要散落于代码中
函数应该只在一个抽象层级上
函数中的语句应该在同一抽象层级上,该层级应该是函数名所操作的下一层
在较高层级防止可配置的数据
在较高层放置默认常量或是配置,可以最为参数传递给较低层。
较高层的配置更容易修改
避免传递浏览
不要出现类似于:a.GetB().GetC().DoSomthing() 这样的代码
不要继承常量
用枚举代替选择常量
使用通配符避免过长的导入清单
名称
好味道
采用描述性名称
取名需要谨慎,确认名称是具有描述性的
名称应该和抽象级相符
名称也要就有抽象性,能够代表更广的含义,而不是特定死的命名,例如用connectionLocator 代替 phoneNumber
尽可能用标准命名法
无歧义的名称
为较大作用范围选用较长的名称
名称长度和作用范围广泛度相关
例如循环变量 ,i ,j
避免编码
不要在名称中包含有类型或是作用范围信息
名称应该说明副作用
名称应该说明函数、变量、类的一切信息,不能掩蔽副作用,不要用简单的动词来描述不止做了一个简单动作的函数
测试
被忽略的测试就是对不确定事物的疑问
坏味道
测试不足
不要看起来够了就够了,只要还有没有被测试探测过的条件,或是还有没有被验证过的计算,测试就不够
好味道
使用测试率工具
别略过小测试
测试边界条件
代码重点是:要处理的就是那些小概率的事件(失败),而不是大概率事件(正确)
全面测试相近的缺陷
缺陷倾向于扎堆,某个函数有缺陷,最好全名测试下这个函数,缺陷可能不止一个
测试失败的模式有启发性
测试覆盖率的模式有启发性
通过测试用例来发现问题
测试应该快速
电子书
提取码:7khm
让营地比你来时更干净