clean code,顾名思义就是整洁的代码,或者说清晰、漂亮的代码,相信大多数工程师都希望自己能写出这样的代码。

也许这是个千人千面的话题,每个工程师都有自己的理解。比如我,从一个天天被骂代码写得烂的人,逐渐学习成长,到现在也能写的出“人模人样”的代码来了。这期间算是积累了一点经验心得,想和大家分享,抛砖引玉。

本文主要针对面向对象编程的clean code来阐述,面向过程代码的思路会比较不同,不在本文的讨论范畴。

代码整洁的大前提

代码大部分时候是用来维护的,而不是用来实现功能的

这个原则适用于大部分的工程。我们的代码,一方面是编译好让机器执行,完成功能需求;另一方面,是写给身边的队友和自己看的,需要长期维护,而且大部分项目都不是朝生夕死的短命鬼。

大部分情况下,如果不能写出清晰好看的代码,可能自己一时爽快,后续维护付出的代价和成本将远高于你的想象。

对清晰好看代码的追求精神,比所有的技巧都要重要。

优秀的代码大部分是可以自描述的,好于文档和注释

当你翻看很多开源代码时,会发现注释甚至比我们自己写的项目都少,但是却能看的很舒服。当读完源码时,很多功能设计就都清晰明了了。通过仔细斟酌的方法命名、清晰的流程控制,代码本身就可以拿出来当作文档使用,而且它永远不会过期。

相反,注释不能让写的烂的代码变的更好。如果别人只能依靠注释读懂你的代码的时候,你一定要反思代码出现了什么问题(当然,这里不是说大家不要写注释了)。

说下比较适合写注释的两种场景:

  1. public interface,向别人明确发布你功能的语义,输入输出,且不需要关注实现。
  2. 功能容易有歧义的点,或者涉及比较深层专业知识的时候。比如,如果你写一个客户端,各种config参数的含义等。

设计模式只是手段,代码清晰才是目的

之前见过一些所谓“高手”的代码都比较抽象,各种工厂、各种继承。想找到一个实现总是要山路十八弯,一个工程里大部分的类是抽象类或者接口,找不到一两句实现的代码,整个读起代码来很不顺畅。我跟他聊起来的时候,他的主要立场是:保留合适的扩展点,克服掉所有的硬编码。

其实在我看来,也许他的代码被“过度设计”了。首先必须要承认的是,在同一个公司工作的同事,水平是参差不齐的。无论你用了如何高大上的设计,如果大多数人都不能理解你的代码或者读起来很费劲的话,其实这是一个失败的设计。

当你的系统内大部分抽象只有一个实现的时候,要好好思考一下,是不是设计有点过度了,清晰永远是第一准则。

代码整洁的常见手段

记住原则后,我们开始进入实践环节,先来看下有哪些促成clean code的常见手段。

code review

很多大公司会用git的pull request机制来做code review。我们重点应该review什么?是代码的格式、业务逻辑还是代码风格?我想说的是,凡是能通过机器检查出来的事情,无需通过人。比如换行、注释、方法长度、代码重复等。除了基本功能需求的逻辑合理没有bug外,我们更应该关注代码的设计与风格。比如,一段功能是不是应该属于一个类、是不是有很多相似的功能可以抽取出来复用、代码太过冗长难懂等等。

我个人非常推崇集体code review,因为很多时候,组里相对高级的工程师能够一眼发现代码存在较大设计缺陷,提出改进意见或者重构方式。我们可以在整个小组内形成一个好的文化传承和风格统一,并且很大程度上培养了大家对clean code的热情。

勤于重构

好的代码,一般都不是一撮而就的。即使一开始设计的代码非常优秀,随着业务的快速迭代,也可能被改的面目全非。

为了避免重构带来的负面影响(delay需求或者带来bug),我们需要做好以下的功课:
① 掌握一些常见的“无痛”重构技巧,这在下文会有具体讲解。
② 小步快跑,不要企图一口吃成个胖子。改一点,测试一点,一方面减少代码merge的痛苦,另一方面减少上线的风险。
③ 建立自动化测试机制,要做到即使代码改坏了,也能保证系统最小核心功能的可用,并且保证自己修改的部分被测试覆盖到。
④ 熟练掌握IDE的自动重构功能。这些会很大程度上减少我们的体力劳动,避免犯错。

静态检查

现在市面上有很多代码静态检查的工具,也是发现bug和风格不好的比较容易的方式。可以与发布系统做集成,强制把主要问题修复掉才可以上线。目前美团点评技术团队内部的研发流程中已经普遍接入了Sonar质量管理平台。

多读开源代码和身边优秀同学的代码

感谢开源社区,为我们提供了这么好的学习机会。无论是JDK的源码,还是经典的Netty、Spring、Jetty,还是一些小工具如Guava等,都是clean code的典范。多多学习,多多反思和总结,必有收益。

代码整洁的常见技巧

前面的内容都属于热身,让大家有个整体宏观的认识。下面终于进入干货环节了,我会分几个角度讲解编写整洁代码的常见技巧和误区。

通用技巧

单一职责

这是整洁代码的最重要也是最基本的原则了。简单来讲,大到一个module、一个package,小到一个class、一个method乃至一个属性,都应该承载一个明确的职责。要定义的东西,如果不能用一句话描述清楚职责,就把它拆掉。

我们平时写代码时,最容易犯的错误是:一个方法干了好几件事或者一个类承载了许多功能。

先来聊聊方法的问题。个人非常主张把方法拆细,这是复用的基础。如果方法干了两件事情,很有可能其中一个功能的其他业务有差别就不好重用了。另外语义也是不明确的。经常看到一个get()方法里面竟然修改了数据,这让使用你方法的人情何以堪?如果不点进去看看实现,可能就让程序陷入bug,让测试陷入麻烦。

再来聊聊类的问题。我们经常会看到“又臭又长”的service/biz层的代码,里面有几十个方法,干什么的都有:既有增删改查,又有业务逻辑的聚合。每次找到一个方法都费劲。不属于一个领域或者一个层次的功能,就不要放到一起。

我们team在code review中,最常被批评的问题,就是一个方法应该归属于哪个类。

优先定义整体框架

我写代码的时候,比较喜欢先去定义整体的框架,就是写很多空实现,来把整体的业务流程穿起来。良好的方法签名,用入参和出参来控制流程。这样能够避免陷入业务细节无法自拔。在脑海中先定义清楚流程的几个阶段,并为每个阶段找到合适的方法/类归属。

这样做的好处是,阅读你代码的人,无论读到什么深度,都可以清晰地了解每一层的职能,如果不care下一层的实现,完全可以跳过不看,并且方法的粒度也会恰到好处。

简而言之,我比较推崇写代码的时候“广度优先”而不是“深度优先”,这和我读代码的方式是一致的。当然,这件事情跟个人的思维习惯有一定的关系,可能对抽象思维能力要求会更高一些。如果开始写代码的时候这些不够清晰,起码要通过不断地重构,使代码达到这样的成色。

清晰的命名

老生常谈的话题,这里不展开讲了,但是必须要mark一下。有的时候,我思考一个方法命名的时间,比写一段代码的时间还长。原因还是那个逻辑:每当你写出一个类似于"temp"、"a"、"b"这样变量的时候,后面每一个维护代码的人,都需要用几倍的精力才能理顺。

并且这也是代码自描述最重要的基础。

避免过长参数

如果一个方法的参数长度超过4个,就需要警惕了。一方面,没有人能够记得清楚这些函数的语义;另一方面,代码的可读性会很差;最后,如果参数非常多,意味着一定有很多参数,在很多场景下,是没有用的,我们只能构造默认值的方式来传递。

解决这个问题的方法很简单,一般情况下我们会构造paramObject。用一个struct或者一个class来承载数据,一般这种对象是value object,不可变对象。这样,能极大程度提高代码的可复用性和可读性。在必要的时候,提供合适的build方法,来简化上层代码的开发成本。

避免过长方法和类

一个类或者方法过长的时候,读者总是很崩溃的。简单地把方法、类和职责拆细,往往会有立竿见影的成效。以类为例,拆分的维度有很多,常见的是横向/纵向。例如,如果一个service,处理的是跟一个库表对象相关的所有逻辑,横向拆分就是根据业务,把建立/更新/修改/通知等逻辑拆到不同的类里去;而纵向拆分,指的是
把数据库操作/MQ操作/Cache操作/对象校验等,拆到不同的对象里去,让主流程尽量简单可控,让同一个类,表达尽量同一个维度的东西。

让相同长度的代码段表示相同粒度的逻辑

这里想表达的是,尽量多地去抽取private方法,让代码具有自描述的能力。举个简单的例子

public void doSomeThing(Map params1,Map params2){
Do1 do1 = getDo1(params1);
Do2 do2 = new Do2();
do2.setA(params2.get("a"));
do2.setB(params2.get("b"));
do2.setC(params2.get("c"));
mergeDO(do1,do2);
}
private void getDo1(Map params1);
private void mergeDo(do1,do2){...};

类似这种代码,在业务代码中随处可见。获取do1是一个方法,merge是一个方法,但获取do2的代码却在主流程里写了。这种代码,流程越长,读起来越累。很多人读代码的逻辑,是“广度优先”的。先读懂主流程,再去看细节。类似这种代码,如果能够把构造do2的代码,提取一个private 方法,就会舒服很多。

面向对象设计技巧

贫血与领域驱动

不得不承认,Spring已经成为企业级Java开发的事实标准。而大部分公司采用的三层/四层贫血模型,已经让我们的编码习惯,变成了面向DAO而不是面向对象。

缺少了必要的模型抽象和设计环节,使得代码冗长,复用程度比较差。每次撸代码的时候,从mapper撸起,好像已经成为不成文的规范。

好处是上手简单,学习成本低。但是每次都不能重用,然后面对两三千行的类看着眼花的时候,我的心是很痛的。关于领域驱动的设计模式,本文不会展开去讲。回归面向对象,还是跟大家share一些比较好的code技巧,能够在一个通用的框架下,尽量好的写出漂亮可重用的code。

个人认为,一个好的系统,一定离不开一套好的模型定义。梳理清楚系统中的核心模型,清楚的定义每个方法的类归属,无论对于代码的可读性、可交流性,还是和产品的沟通,都是有莫大好处的。

为每个方法找到合适的类归属,数据和行为尽量要在一起

如果一个类的所有方法,都是在操作另一个类的对象。这时候就要仔细想一想类的设计是否合理了。理论上讲,面向对象的设计,主张数据和行为在一起。这样,对象之间的结构才是清晰的,也能减少很多不必要的参数传递。

不过这里面有一个要讨论的方法:service对象。如果操作一个对象数据的所有方法都建立在对象内部,可能使对象承载了很多并不属于它本身职能的方法。

例如,我定义一个类,叫做person,。这个类有很多行为,比如:吃饭、睡觉、上厕所、生孩子;也有很多字段,比如:姓名、年龄、性格。

很明显,字段从更大程度上来讲,是定义和描述我这个人的,但很多行为和我的字段并不相关。上厕所的时候是不会关心我是几岁的。如果把所有关于人的行为全部在person内部承载,这个类一定会膨胀的不行。

这时候就体现了service方法的价值,如果一个行为,无法明确属于哪个领域对象,牵强地融入领域对象里,会显得很不自然。这时候,无状态的service可以发挥出它的作用。但一定要把握好这个度,回归本质,我们要把属于每个模型的行为合理的去划定归属。

警惕static

static方法,本质上来讲是面向过程的,无法清晰地反馈对象之间的关系。虽然有一些代码实例(自己实现单例或者Spring托管等)的无状态方法可以用static来表示,但这种抽象是浅层次的。说白了,如果我们所有调用static的地方,都写上import static,那么所有的功能就由类自己在承载了。

让我画一个类图?尴尬了……画不出来。

而单例的膨胀,很大程度上也是贫血模型带来的副作用。如果对象本身有血有肉,就不需要这么多无状态方法。

static真正适用的场景:工具方法,而不是业务方法。

巧用method object

method object是大型重构的常用技巧。当一段逻辑特别复杂的代码,充斥着各种参数传递和是非因果判断的时候,我首先想到的重构手段是提取method object。所谓method object,是一个有数据有行为的对象。依赖的数据会成为这个对象的变量,所有的行为会成为这个对象的内部方法。利用成员变量代替参数传递,会让代码简洁清爽很多。并且,把一段过程式的代码转换成对象代码,为很多面向对象编程才可以使用的继承/封装/多态等提供了基础。

举个例子,上文引用的代码如果用method object表示大概会变成这样

class DoMerger{
map params1;
map params2;
Do1 do1;
Do2 do2;
public DoMerger(Map params1,Map params2){
this.params1 = params1;
this.params2 = parmas2;
}
public void invoke(){
do1 = getDo1();
do2 = getDo2();
mergeDO(do1,do2);
}
private Do1 getDo1();
private Do2 getDo2();
private void mergeDo(){
print(do1+do2);
}
}

面向接口编程

面向接口编程是很多年来大家形成的共识和最佳实践。最早的理论是便于实现的替换,但现在更显而易见的好处是避免public方法的膨胀。一个对外publish的接口,一定有明确的职责。要判断每一个public方法是否应该属于同一个interface,是很容易的。

整个代码基于接口去组织,会很自然地变得非常清晰易读。关注实现的人才去看实现,不是嘛?

正确使用继承和组合

这也是个在业界被讨论过很久的问题,也有很多论调。最新的观点是组合的使用一般情况下比继承更为灵活,尤其是单继承的体系里,所以倾向于使用组合
,否则会让子类承载很多不属于自己的职能。

个人对此观点持保留意见,在我经历过的代码中,有一个小规律,我分析一下。

protected abstract 这种是最值得使用继承的,父类保留扩展点,子类扩展,没什么好说的。

protected final 这种方法,子类是只能使用不能修改实现的。一般有两种情况:
① 抽象出主流程不能被修改的,然而一般情况下,public final更适合这个职能。如果只是流程的一部分,需要思考这个流程的类归属,大部分变成public组合到其他类里是更合适的。
② 父类是抽象类无法直接对外提供服务,又不希望子类修改它的行为,这种大多数情况下属于工具方法,比较适合用另一个领域对象来承载并用组合的方式来使用。

protected 这种是有争议的,是父类有默认实现但子类可以扩展的。凡是有扩展可能的,使用继承更理想一些。否则,定义成final并考虑成组合。

综上所述,个人认为继承更多的是为扩展提供便利,为复用而存在的方法最好使用组合的方式。当然,更为大的原则是明确每个方法的领域划分。

代码复用技巧

模板方法

这是我用得最多的设计模式了。每当有两个行为类似但又不完全相同的代码段时,我总是会想到模板方法。提取公共流程和可复用的方法到父类,保留不同的地方作为abstract方法,由不同的子类去实现。

并在合适的时机,pull method up(复用)或者 pull method down(特殊逻辑)。

最后,把不属于流程的、但可复用的方法,判断是不是属于基类的领域职责,再使用继承或者组合的方法,为这些方法找到合适的安家之处。

extract method

很多复用的级别没有这么大,也许只是几行相同的逻辑被copy了好几次,何不尝试提取方法(private)。又能明确方法行为,又能做到代码复用,何乐不为?

责任链

经常看到这样的代码,一连串类似的行为,只是数据或者行为不一样。如一堆校验器,如果成功怎么样、失败怎么样;或者一堆对象构建器,各去构造一部分数据。碰到这种场景,我总是喜欢定义一个通用接口,入参是完整的要校验/构造的参数,
出参是成功/失败的标示或者是void。然后有很多实现器分别实现这个接口,再用一个集合把这堆行为串起来。最后,遍历这个集合,串行或者并行的执行每一部分的逻辑。

这样做的好处是:
① 很多通用的代码可以在责任链原子对象的基类里实现;
② 代码清晰,开闭原则,每当有新的行为产生的时候,只需要定义行的实现类并添加到集合里即可;
③ 为并行提供了基础。

为集合显式定义它的行为

集合是个有意思的东西,本质上它是个容器,但由于泛型的存在,它变成了可以承载所有对象的容器。很多非集合的类,我们可以定义清楚他们的边界和行为划分,但是装进集合里,它们却都变成了一个样子。不停地有代码,各种循环集合,做一些相似的操作。

其实很多时候,可以把对集合的操作显示地封装起来,让它变得更有血有肉。

例如一个Map,它可能表示一个配制、一个缓存等等。如果所有的操作都是直接操作Map,那么它的行为就没有任何语义。第一,读起来就必须要深入细节;第二,如果想从获取配置读取缓存的地方加个通用的逻辑,例如打个log什么的,你可以想象是多么的崩溃。

个人提倡的做法是,对于有明确语义的集合的一些操作,尤其是全局的集合或者被经常使用的集合,做一些封装和抽象,如把Map封装成一个Cache类或者一个config类,再提供GetFromCache这样的方法。

总结

本文从clean code的几个大前提出发,然后提出了实践clean code的一些手段,重点放在促成clean code的一些常用编码和重构技巧。
当然,这些只代表笔者本人的一点点感悟。好的代码,最最需要的,还是大家不断追求卓越的精神。欢迎大家一起探索交流这个领域,为clean code提供更多好的思路与方法。

参考:https://tech.meituan.com/clean-code.html

聊聊clean code的更多相关文章

  1. Writing Clean Code 读后感

    最近花了一些时间看了这本书,书名是 <Writing Clean Code ── Microsoft Techniques for Developing Bug-free C Programs& ...

  2. 说说怎么写clean code

    前两天参加了公司组织的一个培训,主题是“如何写出好的代码” ,刚看到这个主题,第一反应是又不知道是哪个培训机构来忽悠钱的!老大安排了,就去听听呗. 说实在的,课程内容没有什么新鲜的东西,就是讲讲如何发 ...

  3. 小课堂week13 Clean Code Part2

    Clean Code Part2 对象与数据结构 首先让我们进行一个严肃的思考,对象与数据结构的区别在哪里? 如下两段代码分别用数据结构和对象的方法来描述了一个Point. public class ...

  4. 小课堂Week12 Clean Code Part1

    小课堂Week12 Clean Code Part1 今天的主题是函数,让我们看一个函数,找一找其中的"不整洁". 我们也根据这段代码,讨论下对于整洁代码的两个重要原则. publ ...

  5. “Clean Code” 读书笔记序

    最近开始研读 Robert C.Martin 的 “Clean Code”,为了巩固学习,会把每一章的笔记整理到博客中.而这篇博文作为一个索引和总结,会陆续加入各章的笔记链接,以及全部读完后的心得体会 ...

  6. Clean Code–Chapter 7 Error Handling

    Error handling is important, but if it obscures logic, it's wrong. Use Exceptions Rather Than Return ...

  7. Clean Code – Chapter 4: Comments

    “Don’t comment bad code—rewrite it.”——Brian W.Kernighan and P.J.Plaugher The proper use of comments ...

  8. Clean Code – Chapter 3: Functions

    Small Blocks and Indenting The blocks within if statements, else statements, while statements, and s ...

  9. Writing clean code is what you must do in order to call yourself a professional.

    Clean Code  A Handbook of Agile Software Craftsmanship

随机推荐

  1. dubbo的详细介绍

    1.背景 随着互联网的发展,网站应用的规模不断扩大,常规的垂直应用架构已无法应对,分布式服务架构以及流动计算架构势在必行,亟需一个治理系统确保架构有条不紊的演进. 2.架构 单一应用架构 当网站流量很 ...

  2. 成为IT经理必备的十大能力 不只是技术!

    对于一个IT从业者,让你谋得工作的也许是技术能力,但有助于提升职业生涯的却是软技能.步步高升的人都是那些发表文章.在会议上积极发言以及关注客户的员工(程序员).与此同时,通常情况下,企业CIO或多或少 ...

  3. chromedriver与chrome版本对应表,firefox、geckodriver

     一. chromedriver与chrome对应表(记得就会更新): chromedriver版本 支持的Chrome版本 v2.36 v64-66 v2.35 v62-64 v2.34 v61-6 ...

  4. hdu-5023线段树刷题

    title: hdu-5023线段树刷题 date: 2018-10-18 13:32:13 tags: acm 刷题 categories: ACM-线段树 概述 这道题和上次做的那道染色问题一样, ...

  5. JAVAEE——SSH项目实战03:新增客户、数据字典、文件上传和修改客户

    作者: kent鹏 转载请注明出处: http://www.cnblogs.com/xieyupeng/p/7145599.html 一.新增客户 1.数据字典  用于枚举项目中有限个数的字典项 (1 ...

  6. Initializing the FallBack certificate failed . TDSSNIClient initialization failed

    安装SQL后服务不能启动,报错: 2014-03-24 14:33:10.06 spid13s     Error: 17190, Severity: 16, State: 1.2014-03-24 ...

  7. [leetcode tree]101. Symmetric Tree

    Given a binary tree, check whether it is a mirror of itself (ie, symmetric around its center). For e ...

  8. python orm字段解析

    null # 是否可以为空 default # 默认值 primary_key # 主键 db_column # 列名 db_index # 索引(db_index=True) unique # 唯一 ...

  9. Navicat Premium 激活码 序列号。破解版。

    韩梦飞沙  韩亚飞  313134555@qq.com  yue31313  han_meng_fei_sha Navicat Premium 激活码  序列号.破解版.

  10. 20162327WJH使用队列:模拟票务站台代码分析

    20162327WJH使用队列:模拟票务站台代码分析 用链队实现队列的情况 1.用链表实现队列的代码 关键方法代码及补全代(LinkedOueue类) public void enqueue(T el ...