写软件的人有一大爱好,就是喜欢聚在一起,相互调侃自家的代码,“我公司祖传代码,五代单传,无注释,如无字天书……”,”我公司代码相关浪,想重构把,又害怕出线上问题”,诸如此类。
近些年比较火的是微服务,但是微服务需要更强的业务建模能力和技术管理能力,否则实现和维护微服务只能是难上加难,有时候,我不禁会想,屎山般的代码是不是每个系统发展中不可避免的命运?如果不是,那应该靠什么去避免代码的腐化呢?
也许一个很好的架构是解决问题的办法,因为当你在写代码前,如果没有业务压力,可以悠哉游哉地想好几天,等动手写时,一切的分治联合都了然于胸,洋洋洒洒,如有神助。但是如果业务繁忙,需要多个团队相互协作,就很难保证架构在演化过程中保持清晰,健壮,也很难保证自动自觉地写出干净的代码,而不会为了完成任务去堆砌。
上文摘自张建飞老师《代码精进之路,从码农到工匠》一书,本文也主要围绕着这本书的内容展开,写一些我自身的体会及后续编程之路的思考。
好的命名可以极大地提升代码可读性和可理解性,但常常被认为是编程中的细节问题,其重要性被大大低估,在日常工作中,大部分时间都在阅读和理解代码,好的命名能够让代码的概念清晰,增加代码的表达能力,词不达意的命名会破坏我们的思考的连贯性,分散有限的注意力。
取一个有意义的名字
在编码的过程中,给一个类或者方法命名是很纠结的,因为需要体现出这块代码的实际作用,还需要简洁明了,并和项目中其他规范保持一致。这个过程是一个抽象和思考的过程,每当我纠结于一个名称的时候,我一般会去重新思考整体的业务逻辑,如果你无法为这个模块赋予合适的名字,就说明你对这里的理解还不够深刻,需要重新去挖掘问题的本质。
变量名
变量名应该是名词,能够正确地描述业务,有表达力,举个例子:
// request请求的参数名String name = “xxxx”;
上面的变量只能通过注释去得知具体指的是什么,如果没有注释,阅读代码的人为了知道name的含义,就需要通读代码的上下文以获取线索,如果按照下面的方式去命名,那么阅读代码的人就很容易直到这个变量代表什么意思。
String attributeNameFromRequest = “xxxx”;
这样做还有一个好处,就是便于搜索,假设你想找request的相关代码,就可以直接搜,很快就可以定位到位置。但如果你搜索name,就会出现一堆与你初衷相悖的代码。
方法名
方法命名需要具体,空泛的命名没有意义。
例如processData()就不是一个好的命名,因为所有的方法都是对数据的处理,这样的命名并没有表明要做的事情,相比之下,buildRequestParams()或者validateUserInfo()就要好许多。
方法的命名要体现出做什么,而不是怎么做。假如我们将用户信息存在一个栈中,现在要从栈中取最近的用户信息,那么getLastestUserInfo()就比popInfo()要好,因为栈数据结构是底层实现细节,命名应该提升抽象层次,体现业务语义。合理的命名可以让你省掉记住出栈的步骤,你只需要简单地说,取最近用户的信息即可。
类名
类是面向对象中最重要的概念之一,是一组数据和操作的封装。
实体类承载了核心业务和核心业务逻辑,其命名要充分体现业务语义,并在团队内达到共识,如Customer,Bank和User等,辅助类是辅助实体类一起完成业务逻辑的,其命名要能够通过后缀来体现功能。例如,用来Customer做控制路由的控制类CustomerCotroller,提供Customer服务的服务类CustomerService,获取数据存储的仓储类CustomerMapper。对于辅助类,尽量不要用Helper,Util等后缀,因为其含义太过笼统,容易破坏SRP(单一职责原则)。
比如处理CSV,可以这样写:
CSVHelper.parse();CSVHelper.create();
但是更建议将CSVHelper拆开:
CSVParse.parse();CSVBuilder.create();
包名
包代表一组有关系的类的集合,起到分类组合和命名空间的作用。
包名应该能够反映一组类再更高抽象层次上的联系,例如,有一组类Apple,Pear,Orange,我们可以将它们放在一个包中,命名为fruit。
命名的一致性
保持命名的一致性,可以提高代码的可读性,从而简化复杂度。因此,我们要小心选择命名,一旦选中,就要持续遵循,保证名称始终一致。
一个概念一个词
查询这个概念,可以用fetch,retrieve,get和query来表示,如果不加约定地给多个类中的同种查询方法命名,你该如何记得哪个类中的哪个方法呢?同样,在一段代码中,同时存在manager,controller和handler,会令人感到困惑。下面是一个常用的约定组合:
CRUD操作 | 方法名约定 |
新增 | create |
添加 | add |
删除 | remove |
修改 | update |
查询(多个结果) | list |
查询(单个结果) | get |
分页查询 | page |
统计 | count |
多使用对仗词
遵守对仗词的命名规则有助于保持一致性,从而提高代码的可读性。例如first/last这样的对仗词就很容易理解,而像fileOpen和fClose这样的组合就不对称,让人迷惑。下面列举一下常见的对仗词组合:
Add/remove
Open/close
Begin/end
Insert/delete
Show/hide
Create/destroy
Lock/unlock
…
后置限定词
在编码过程中会有一些表示计算结果的变量,例如总额,平均值,最大值等,如果用类似Tatal,Sum,Average,Max,Min这样的限定词来修改某个命名,那么需要把限定词放在最后,并在项目中贯彻执行,保持命名风格的一致性。
变量中最重要的部分,即为这一变量赋予主要含义的部分应位于最前面,这样可以突出显示,并会被首先阅读,如果贯彻限定词后置的原则,我们就能收获到一组非常优雅,具有对称性的命名:revenueTotal,expenseTotal和revenueAvgerage等。
需要注意的是有些词的含义,例如num,放在变量名的结束位置表示一个下标,customerNum表示的是当前客户的序号,为了避免num带来的歧义,建议用Count或Total来表示总数,用id来表示序号。
统一业务语言和技术语言
确保团队在内部的所有交流,模型,代码和文档中都要使用同一种编程语言。
而有些技术语言是通用的,业内人士都能理解,我们应该尽量使用这些术语进行命名,例如DO,DAO,DTO等。
自明的代码
好的代码是最好的文档。
中间变量
通过添加中间变量让代码变得自明,即将计算过程打算成多个步骤,并用有意义的变量名来命名中间变量,从而把隐藏的计算过程以显性化的方式表达出来。
例如,我们要通过Regex来获得字符串中的值,并放到map中,
Matcher matcher = headerPattern.matcher(line);
if(matcher.find()){headers.put(matcher.group(1),matcher.group(2));}
用中间变量,可以写成如下形式:
Matcher matcher = headerPattern.matcher(line);
if(matcher.find()){String key = matcher.group(1);String value = matcher.group(2);headers.put(key,value);}
设计模式语言
使用设计模式语言也是代码自明的重要手段之一,在技术人员之间共享和使用设计模式语言,可以极大地提升沟通效率。当然,前提是大家都要理解和熟悉这些设计模式,否则就会变成“鸡同鸭讲”。因此,我们有必要在命名上就将设计模式显性化的表示出来,让阅读代码的人能体会到设计者的意图。
举个例子,spring里的ApplicationListener就充分体现了它的设计和用处。通过这个命名,我们直到它是用观察者模式,每一个被注册的ApplicationListener在Application状态发生变化时,都会接收到一个notify,这样我们就可以在容器初始化完成后进行一些业务操作,比如数据加载,初始化缓存等。
当进行邮件营销时要根据一些规则过滤掉一些客户,例如没有邮箱地址的客户,没有订阅关系不能发送邮件的客户,3天内不能重复发送邮件的客户等。这类需求使用责任链方式就可以很好的解决:
FilterChain filterChain = FilterChainFactory.buildFilterChain(NoEmailAddressFilter.class,EmailUnsubscribeFilter.class,EmailThreeDayNotRepearFilter.class);public class NoEmailAddressFilter implements Filter{@Overridepublic void doFilter(Object context,FilterInvoker nextFilter){Map<String,Object> contextMap = (Map<String,Object>) context;String email = ConvertUtils.convertParamType(contextMap.get("email"),String.class);if(StringUtils.isBlank(email)){return;}nextFilter.invoke(context);}}
FilterChain这个名字非常恰当地表达了作者的意图,Filter表示用来过滤,Chain表示用的是责任链模式。
小心注释
如果注释是为了阐述代码背后的意图,那么这个注释是有用的,如果注释只是为了复述代码功能,那么就显得没必要。