《代码整洁之道》 --- 写出优雅的代码

变量名

原则一:名副其实

  • 变量名和函数名或类名,一定要让人知道它是干什么用的。如果一个变量,需要注释来补充,则不是一个好的变量名。
1
2
// 如果代码中到处出现d,则会让人十分困惑
int d ; // count time in days
  • 减少代码的模糊度
1
2
3
4
5
6
7
8
9
10
11
12
13
public List<int[]> getThem(){

List<int[]> list1 = new ArrayList<>();
for(int[] x : theList){

if(x[0] == 4){
list1.add(x);
}

}

return list1;
}

​ 上面这段代码存在几个问题,有很多东西,如果不结合上下文(其他代码)来看的话,我们很难快速知道该段代码到底想表达什么东西:

(1)theList 是什么东西?类型是什么

(2)4 表达什么意思

(3)X[0],有什么特殊的吗?

(4)List1,又是什么东西

(5)getThem又是什么意思

​ 假设代码的场景,正在开发一款扫雷游戏,theList是所有单元格存储集合,[0]表示该单元格的状态,4表示已经标记。显然,list1是存储已经标记的所有单元格。因此,上述代码可以改为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public List<int[]> getAllTagCell(){

List<int[]> tagCellList = new ArrayList<>();
for(int[] cell : gameboard){

if(cell[CELL_STATUS] == TAG_THE_CELL){
tagCellList.add(x);
}

}

return tagCellList;
}

// 甚至可以进一步封装
public List<Cell> getAllTagCell(){

List<Cell> tagCellList = new ArrayList<>();
for(Cell cell : gameboard){

if(Cell.isTagged()){
tagCellList.add(x);
}

}

return tagCellList;
}

原则二:做有意义的区分

(1)变量名后加数字

​ 因为同一个作用域里(通常指某个function内),不能够出现相同名字的变量。因此很常见的办法就是,在变量后面加数字。

1
2
3
4
5
public arrsTransfer(int[] n1,int[] n2){
for(int i=0;i<n1.length;i++){
n1[i]=n2[i];
}
}

​ 很显然,这是把数组n1拷贝到n2。这里有个问题,如果不写注释的情况下,很难知道是 n1 —> n2,还是 n2 —> n1。

​ 因此,我们把n1和n2,改为destination和source。代码的可读性就好很多了。

(2)没意义的区别

​ 比如说,有一个Person类,你很难说出Person,PersonInfo,People,这几个变量有什么区别。如果不做特殊约定的话

​ 同样,a和the也很难区分。

原则三:尽量避免缩写,除非这个缩写是大家的共识

1
2
3
4
5
6
7
8
9
class Record{
private long genymdhms;
private long modymdhms; //years month day hour minutes second
}

class Record{
private long generationTimestamp;
private long modifyTimestamp;
}

​ 如果不是大家的共识,没有人知道ymdhms表示什么东西。

原则四:常量尽量起别名

​ 不直接使用常量,而用别名有两个好处。

  • 代码可读性更强
  • 便于搜索
1
2
3
4
5
6
7
8
9
10
11
12
13
public List<int[]> getThem(){

List<int[]> list1 = new ArrayList<>();
for(int[] x : theList){

if(x[0] == 4){
list1.add(x);
}

}

return list1;
}

​ 以这段代码为例子,如果直接使用0和4,我们不明白这代表什么意思。可读性很差。

​ 并且在修改代码的时候,我们直接搜索4,必然会出现一大堆无关的东西,因此,我们改用别名来操作。

1
2
3
4
const (
CELL_STATUTE = 0
CELL_BE_TAGGED = 4
)

原则五:每个概念一个词

  • 同一个类中的方法,有getXXX,fetchXXX ;findXXX,selectXXX,这种会让人很困惑。我们统一使用get,find这样会更好
  • 例如,manager,controller,driver也会让人困惑,统一manager好了

原则六:有意义的前缀

​ 设想你有名为firstName,lastName,street,houseNumber,city。把他们放一块,很显然形成一个地址,但是单独来看就不知道是什么了。

​ 可以在变量前加个前缀,就很显而易见了,例如addressFirstName

函数

先看一段很糟糕的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
 public static  String testableHtml(PageData pageData, boolean includeSuiteSetup) throws Exception{

WikiPage wikiPage = pageData.getWikiPage();
StringBuffer buffer = new StringBuffer();

if(pageData.hasAttribute("test")){
if(includeSuiteSetup){
WikiPage suiteSetup = PageCrawlerImpl.getInheritedPage();
if(suiteSetup != null){
WikiPagePath pagePath = suiteSetup.getFullPath();
}
}
}
buffer.append(pageData.getContent());

if(pageData.hasAttribute("test")){
if(includeSuiteSetup){

}
}
}



return "";
}

​ 这段代码不光长,而且很复杂(if判断很多,而且还是嵌套判断),有大量的字符串。导致整段代码理解起来非常费解。有太多不同层级的抽象,奇怪的字符串,以及if嵌套。

​ 但是,我们可以做一点抽象和重构,就能在几行代码内就解决问题。我们看一下重构后的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

private static final string TEST_ATTRIBUTE_TAG = "Test";
public static String renderPageWithSetupsAndTeardowns(PageData pageData,boolean isSetUp) throw Exception(){

boolean isTestPage = pageData.hasAttribute(TEST_ATTRIBUTE_TAG);
if(isTestPage){
WikiPage testPage = pageData.getWikiPage();
String newPageContent = new StringBuffer();
includeSetupPage(XXX);
newPageContent.append(pageData.getContent());
includeTeardownPage(XXX);
pageData.setContent(newPageContent.toString());
}

return pageData.getHtml();
}

​ 我们可以很清晰的看到这段代码到底干了什么事情,总的来说是两件事。处理setUpPage和TearDownPage,并且把内容塞入一个testPage中。

原则一:函数应该尽可能短

​ 《clean code》提倡代码最好压缩到20行左右。我个人觉得这个标准过于严苛了,做到以下几点我个人觉得就可以了

  • 如果某段中,对某个对象的几个(>=2)字段修改,最好把他进行封装
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
class People{
String name;
int age;
int sex;
}
// 重构前
public void testFunciton(RpcReq req,RpcRsp rsp){

// ......

People people = new People();
people.name = req.name;
people.age = req.age;
people.sex = req.sex;

// ......
}

// 重构后
public People getPeopleItemByReq(RpcReq req){
People people = new People();
people.name = req.name;
people.age = req.age;
people.sex = req.sex;

return people;
}

public void testFunciton(RpcReq req,RpcRsp rsp){

// ......

People people = getPeopleItemByReq(req);

// ......
}


  • 处理逻辑进行分装,保证每段处理逻辑只做一件事
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private static final string TEST_ATTRIBUTE_TAG = "Test";
public static String renderPageWithSetupsAndTeardowns(PageData pageData,boolean isSetUp) throw Exception(){

boolean isTestPage = pageData.hasAttribute(TEST_ATTRIBUTE_TAG);
if(isTestPage){
WikiPage testPage = pageData.getWikiPage();
String newPageContent = new StringBuffer();
includeSetupPage(XXX);
newPageContent.append(pageData.getContent());
includeTeardownPage(XXX);
pageData.setContent(newPageContent.toString());
}

return pageData.getHtml();
}

​ 像这段代码,把setupPage和includeTeardown的处理逻辑封装了,然后封装之后因为他们是同层级的功能,又能封装一次,就让函数非常简洁

  • 一个函数内,尽量少if。if内的条件,以及后续的处理逻辑,也可以进行封装
    • 建议if里面不要再嵌套if,除非实在没办法

原则二:只做一件事情

​ 一开始的那段代码,做了很多事情。(1)创建传冲去 (2)获取页面信息 (3)渲染路径 (4)添加字符串…但是重构之后,就变得简单了很多,制作了一件事,把setup和tearDown放入到测试页面中。其他事情,放到如何getSetUpPage和tearDownPage中。

如何判断该函数是否符合只做一件事的标准?我个人觉得符合下面条件就可以了

  • 函数做的事情,跟函数名描述的一样
  • 函数是否再继续拆分
  • 但是,没必要过于追求函数只做一件事,个人觉得,函数里面所做的事情,都是同一层级的,那问题也不大,我们把该函数封装起来就好了,因为追求所有函数都只做一件事是很难的。应该追求的是,大部分的函数都只做一件事

原则三:每个函数一个抽象层级

自顶向下规则

  • 该函数,要做设置和拆分
    • 设置
      • 如果这是套件,则套件设置步骤
        • 套件设置步骤:XXXX
      • 如果不是套件,则普通步骤
        • 普通步骤:XXXX
    • 拆分

原则四:switch 和 if else

​ 写出短小的switch语句很难,因为switch这个语法就是定义为做同多件事情。我们看一下下面这段代码

1
2
3
4
5
6
7
8
9

public Money caculatePay(Employee e){
switch(e.type){
case COMMISSIONED:
caculateCommissionPay(e);break;
case HOURLY:
caculateHourlyPay(e);break;
}
}

这段代码有两个比较重大的问题:

  • 如果Employee添加新的type,那么必需修改该函数 (我们期望的当然是改动越小越好)
  • 显然类似结构的代码会在很多地方,如果新加类型,改动就会非常大了

​ 这种问题的根本原因是几个不同type的Employee耦合在了一起,那么解决方案就是把switch隐藏在工厂方法的底层。那么只要创建Employee的时候,指定type就好了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

public abstract class Employee{
public caculatedPay();
public payDate();
}

public class SalaryEmployee extend Employee{
//...
}

public calss HourlyEmployee extend Employee{

}
// 工厂
public class EmployFactory(){
Employee makeEmployee(int emlpoyeeType){
switch(employeeType){
case SALARY:
return new SalaryEmployee();
case HOURLY:
return new HourlyEmployee();
}
}
}

​ 把type隐藏在工厂方法里面,上述的问题大部分都能够解决。并且代码的可拓展性会变得更好。

原则五:起一个好的函数名

  • 不要害怕长名称,长名称总比短而令人费解的名称好
  • 命名方式保持一致

目前遇到起名很烦躁的情况是Dao层,给自己定下这些规范

(1)主键 / 唯一键查询

Select + XXX + By + PrimaryKey

(2)批量查询

Find + XXX + By + field1_field2…

原则六:尽量减少函数参数

 对于函数的参数 0个最好,1个可以接受,2个还行,3个底线,超过3个不能接受。如果传递真的超过三个,考虑以下两个方面:
  • 能不能做函数拆分?
  • 把参数进行封装

本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!