前言
俗话说好看的皮囊千篇一律,当然高质量的代码我们也是有目共睹,对于看过spring、springboot、tomcat等底层代码的同学来说,由衷的感叹:这代码写的真TMD好。那对于我们程序员而言,那如何才能写出高质量的代码呢?让别人看到我们代码的时候也由衷的说一句:这代码写的真TMD好。更进一步我们如何做才能写出高质量的代码?本文主要是分享下作者在探索高质量代码之路上的一些个人看法,供同样奋斗在代码之路的同学参考。
最重要的是可读性
正如Martin Fowler(重构那本书的作者,软件界的泰斗)所说:
任何一个傻瓜都能写出计算机可以理解的代码。唯有写出人类容易理解的代码,才是优秀的程序员。
代码是需要维护的,而维护的前提首先是看懂代码,这就要求代码具有可读性。可读性高的代码能够降低后续的维护成本,提升后续开发的效率。
项目代码是需要维护的,除非你写的是一次性代码
大部分情况下是“别人”维护代码,而不是代码的第一作者
互联网迭代节奏快,人员更新换代频繁,需求日新月异,导致代码维护频率高
构建项目需要大量的代码,项目的规模动辄百万行代码量,甚至千万行代码
代码可读性我们一般是指源代码(source code)具有可读性,源代码的目标用户有两类,一类是编译器,如JDK编译器,一类是后续维护的人,对于第一类用户来说,代码可读性的要求没有那么高,只要程序能编译通过,可以正常运行就行。对于第二类用户来说,代码追求可读性的目的是降低他人阅读你的代码的难度,可读性高的代码就像一篇字体工整、段落清晰、逻辑明确的文章,让看到文章的人愿意继续读你的文章,经历过高考的人可以想象一下阅卷老师看到高考作文的感受。
综上,高质量代码最重要的是可读性。什么高内聚低耦合、可扩展性是高质量代码具有的特点,但可读性才是高质量代码最根本的要求。
高质量和低质量代码的特点
好代码的特性
(可读性高)编码标准统一,风格一致,别人愿意维护代码!
先把阿里巴巴编码手册考了吧,Java体系下的行业标准。整体标准的统一,风格一致,可以避免很多不必要的坑,也在代码阅读上形成基本的共识。
(扩展性强)高内聚低耦合
"低耦合, 高内聚"是由于人脑自身的限制所决定的
- 人的记忆是短暂而非永久的
- 在同一时间, 人能考虑到的因素是有限的
由于以上2个限制,所以人必须把复杂的东西分割成不同的组件,其中组件内的概念尽量贴近以至于思考一个因素很容易能联想或者推断另外一个,或者说这些东西在一起很容易记忆、分析、推理... 等, 这就是高聚合。而在这个思考过程中,如果可以尽量把“不相干”的东西排出在外,那么人脑的负担就会小很多,这就是低耦合。
好的接口应当满足设计模式六大原则,很多设计模式、框架都是基于高内聚低耦合这个出发点的:
- 开闭原则:对扩展开发,修改关闭
- 迪米特原则:最少知道原则,间接朋友不要依赖
- 里氏替换原则:依赖父类的地方可替换成子类,子类不要重写父类的方法
- 职责单一原则:类的职责单一
- 接口隔离:接口决定可使用的范围
- 依赖倒置原则:细节依赖抽象,面向接口编程
代码坏味道
你愿意维护代码吗?
Martin Fowler的重构 - 改善既有代码的设计(Refactoring: Improving the Design of Existing Code)和Robert C.Martin的代码整洁之道(Clean Code)关于代码坏味道都有比较详细的介绍,尤其是重构这本书,指出二十多种有“坏味道”的代码,从定性的角度出发给出了界定代码坏味道的方法,当然Martin Fowler说的都是真真切切的道理,真理性毋庸置疑。
本文的作者在实践过程中发现,主观的角度更能说明代码是有坏味道的,这也是依据高质量代码最重要是可读性,那何不问问将要接手你代码人的感受?这也是从真正的用户的反馈出发(代码是别人维护的,那我们让别人来评价我们的代码坏味道。
如果别人问你:“这个代码交给你,你愿意维护吗?”如果你的回答是肯定不愿意,那这个代码坏味道就比较明显。
ELSE IF语句多层嵌套
多层嵌套的IF ELSE语句,尤其是哪些嵌套7~8层的IF ELSE语句,对于维护的人来说简直是地狱。如果你体感不明显,请看下下图,对于多层嵌套的IF ELSE语句,维护代码的人很难找到正确的路径最终实现正确修改代码。
图片来自《What Is Bad Code, Why It Is Dangerous, and How to Write Good Code》
代码风格各异
代码风格本身无好坏之分,就像你是DDD架构风格,还是经典的MVC三层架构风格,本身架构风格没有好坏之分,只是大家对于同一个事物从不同的侧面认知不同,只要团队内的风格一致就好。我觉得这特别像“靠右行驶通行制”,这也是道路交通规则中最基本的原则。如果人、车在道路上随意行动,必然会导致交通混乱、毫无秩序,甚至碰撞、车祸不断。
代码风格各异,就像一群人到一起,每个人都说自己国家的语言,然后大家彼此都没法理解。
Martin Fowler曾经在一篇文章中曾经引用过Phil Karlton的话:There are only two hard things in Computer Science: cache invalidation and naming things.
在 CS 领域中,有两件事是非常难的,一个是缓存失效,一个是命名。可见命名规范统一是多么的重要。
如何写出高质量代码
关于如何写出高质量代码,本文从战略上给出一些自己的思考和建议,并不是具体的方法,希望给前进路上的你一些启发。
代码是脸面
在实际的工作中,我一直坚持“代码是脸面”的理念,当然也在团队中经常强调。代码是脸面是说,在写代码的时候尽量别依靠自己的思维惯性去写代码,思维惯性的下意识是:如果怎么怎么样,我就怎么怎么样,如果再怎么怎么样,我就再怎么怎么样,这样写出来的代码无形中就是各种IF ELSE的嵌套。
代码是脸面是说:写代码的时候时刻怀着敬畏之心写代码,时刻想着我的代码代表着我的脸面,代表着一个人的口碑,对待代码我们要从心态上战略摆正。这样才能认真的对待自己写的每一行代码,这样才会精雕细琢自己的每一行代码,防止无意识的代码坏味道。常言道:君子有三畏:常怀敬畏之心,方能行有所止。
别把事情复杂化,即控制复杂性
别把事情复杂化主要是指:异常处理尽量别复杂化、方案设计尽量别复杂化。
异常和错误处理是造成软件复杂的罪魁祸首之一。如果所有的异常都考虑到,那程序则无法继续下去,这会导致过度防御性的编程,造成代码的可读性很差,而且也会把很多错误掩盖。作者比较推荐在实际的项目中先把主体功能逻辑实现,异常先简化处理,后边再逐步的优化异常处理,不能从一开始就考虑过多的异常,导致程序无法继续写下去。
方案设计尽量别复杂化,别想着从一开始就给出完美的设计方案,这样只会让代码变更复杂,有点“杀鸡焉用牛刀”的味道,无形中造成非必要的复杂性。
鼓励抄代码
团队内鼓励抄代码的几点考虑:
- 优秀的代码可以很快得到普及和传播,这样是最快提高团队代码质量的方法
- 可以拉齐团队内的代码风格、规范和设计,抄代码让团队内的代码风格趋于一致
代码是迭代出来的
互联网的快速发展,很难一开始就做出完美的设计,写出高质量的代码,只能持续的投入精力优化和重构代码,才能在追求高质量代码的路上更进一步。好的代码是日拱一卒的结果,在日常工作中要重视代码和设计细节的改进。
唯手熟尔
通往高质量代码的捷径只有一条:唯手熟尔,即经常写代码。之前作者一直有个误区,认为多看优秀的代码(Tomcat、spring等)就能提高自己的代码水平,后来发现纸上得来终觉浅,绝知此事要躬行,慢慢认知到自己的错误之后,后来实现同一个功能实现,会采用各种方式写一遍,设计模式也是变换了多种,最终终于对设计模式有了自己的理解,代码水平才慢慢的有了明显的提高。
复杂性转移,站在别人的肩膀上开发代码
软件工程发展到今天,各种开源框架层出不穷,各种针对特定领域的软件也各种各样,我们在解决问题的时候,可以考虑利用现有已有的成果和技术,如:作者最近有个项目,项目中涉及到拓扑结构图展示,开始我们是采用普通的MYSQL数据存储拓扑数据,造成查询拓扑图的时候代码很复杂,后来通过引入阿里云上的图数据库GDB,让原来复杂的逻辑处理变成只有几行代码实现。再举个例子:现在咱们会直接写原生的servlet程序吗?当然大家的回答是不会,甚至很多同学听不没听过servlet程序,现在大家都基于springboot开发服务端程序,简单写个controller加几个注解,就能很容易的开发服务端程序。
Java技术发展到今天,基本在各个领域都有专业的技术人员在投入建设,我们应该站在他们的肩膀上开发代码,遇到事情要做充分的技术调研,再开发着手干,这样让很多本来需要我们处理的复杂性代码转移出去,另外也让程序看起来更简洁,可读性更高,作者在最后推荐几个经常用的开源框架。
忌造不圆的*
忌造不圆的*是说在解决问题的时候,尽量别自己独自蛮干,多上google查现有哪些*已经有了,哪些*解决了哪些问题,选择合适的*解决自己的问题。在现有的*基础上开发,别重复造一个同样的*,且我们造的*还不圆。
让IDE帮你写代码
用IDEA写代码,写的过程中IDE会有各种提示,是什么问题,应该怎么解决,其实IDE已经帮我做了很多事,我们应该充分利用IDE的能力,写出高质量的代码。
IDE就像一个聪明的老师,一直在看我们写的代码,还把怎么优化代码都告诉我们了。
写在最后
写高质量代码的方法论其实有很多种,其中问题的关键还是多实践、多动手、多琢磨、多看、再多实践,通过不断的循环,代码写多了,通过学习和实践自然慢慢的代码的质量就提升了。
开源框架和规范
spring全家桶(springboot、springframework)
工具类guava和apache common
Java时间处理神器jodd
简化bean代码神器lombok
bean对象转化利器mapstruct
并发框架disruptor
Java服务端最佳实践参考
异常的统一处理
public class GlobalExceptionHandler { /** * 默认的其他错误信息封装 * * @param req req * @param ex exception * @return response */ value = {BaseException.class}) ( public ResponseResult customHandler(HttpServletRequest req, BaseException ex) { // 这里可以对异常统一进行收集处理 log.error(ExceptionUtil.buildErrorMessage(ex)); return ResponseResult.createResult(ex.getErrorCode(), ex.getErrorMsg()); } /** * 其他未处理的异常 * * @param e exception * @return response */ Exception.class) ( public ResponseResult noHandlerFoundException(Exception e) { log.error(ExceptionUtil.buildErrorMessage(e)); return new ResponseResult(ResultStatus.ERROR_SERVER_INTERNAL); } /** * 请求参数缺少异常捕获 * * @param e exception * @return response */ MissingServletRequestParameterException.class) ( public ResponseResult missRequestParamException(MissingServletRequestParameterException e) { log.error(ExceptionUtil.buildErrorMessage(e)); return new ResponseResult(ResultStatus.ERROR_PARAM); } /** * 请求参数缺少异常捕获 * * @param e exception * @return response */ ValidationException.class) ( public ResponseResult missRequestParamException(ValidationException e) { log.error(ExceptionUtil.buildErrorMessage(e)); return new ResponseResult(ResultStatus.ERROR_PARAM, e.getMessage()); } UnknownUserException.class) ( public ResponseResult unknownUserException(UnknownUserException e) { e.printStackTrace(); log.error(ExceptionUtil.buildErrorMessage(e)); return new ResponseResult(ResultStatus.ERROR_USER_QUERY_FAIL, e.getMessage()); } }
响应结果和错误码
public class ResponseResult<T> { private T data; private int errorCode; private String errorMsg; public ResponseResult(ResultStatus status, T content) { this.data = content; this.errorCode = status.getValue(); this.errorMsg = status.getMessage(); } public ResponseResult(ResultStatus status) { this.errorCode = status.getValue(); this.errorMsg = status.getMessage(); } public ResponseResult(T content, int errCode, String errMsg) { this.data = content; this.errorCode = errCode; this.errorMsg = errMsg; } public static <T> ResponseResult<T> createResult() { return new ResponseResult(null, 0, "success"); } public static <T> ResponseResult<T> createResult(T content) { return new ResponseResult(content, 0, "success"); } public static <T> ResponseResult<T> createResult(int errCode, String errMsg) { return new ResponseResult(null, errCode, errMsg); } public static <T> ResponseResult<T> createResult(ResultStatus status, T data) { return new ResponseResult(status, data); } public String toString() { StringBuilder sb = new StringBuilder(); sb.append("errCode=").append(errorCode); sb.append("errMsg=").append(errorMsg); sb.append("data=").append(data); return sb.toString(); } }
public enum ResultStatus { /** * 请求成功 */ SUCCESS(0, "success"), /** * 请求失败 */ FAILURE(-1, " failure"), ERROR_LOGGED_INVALID(-1, "登录失效"), ERROR_PARAM(4001, "参数错误"), ERROR_SERVER_INTERNAL(5000, "服务器内部错误"); private final int value; private final String message; ResultStatus(int value, String message) { this.value = value; this.message = message; } public static ResultStatus parse(int value) { ResultStatus retValue = ResultStatus.SUCCESS; for (ResultStatus item : ResultStatus.values()) { if (value == item.getValue()) { retValue = item; break; } } return retValue; } public int getValue() { return this.value; } public String getMessage() { return this.message; } }
JSON格式数据统一转换
public class FastJsonConvert { public HttpMessageConverters fastJsonHttpMessageConverters() { FastJsonHttpMessageConverter fastConverter = new FastJsonHttpMessageConverter(); FastJsonConfig fastJsonConfig = new FastJsonConfig(); fastJsonConfig.setSerializerFeatures(SerializerFeature.PrettyFormat, SerializerFeature.WriteNullStringAsEmpty, SerializerFeature.WriteNullNumberAsZero, SerializerFeature.WriteNullListAsEmpty, SerializerFeature.WriteMapNullValue); SerializeConfig config = new SerializeConfig(); config.propertyNamingStrategy = PropertyNamingStrategy.SnakeCase; fastJsonConfig.setSerializeConfig(config); fastConverter.setFastJsonConfig(fastJsonConfig); HttpMessageConverter<?> converter = fastConverter; return new HttpMessageConverters(converter); } }
bean的mapper映射,简化bean的相互转化
componentModel = "spring", nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE) (public interface AoneMapper { AoneMapper INSTANCE = Mappers.getMapper(AoneMapper.class); AoneDeployDetailDTO entityToDto(AlibabaAoneEnvGetdeploybyidResponse.DeployObj object); source = "objectDO.processId", target = "processId") ( expression = "java(objectDO.getReleases().get(0).getReleaseRevision())", target = "releaseReversion") ( expression = "java(objectDO.getReleases().get(0).getReleaseUrl())", target = "releaseUrl") ( AoneBranchInfoDTO entityToDto(ObjectDO objectDO); }
Lombok注解类,简化bean定义实现
public class AppInfo { private int itrackAppId; private String aoneAppName; private boolean deployByAone; private int aoneAppId; private Map<String, List<String>> appGroupNameAndIpListMap; }