代码整洁之道(一)-Clean Code

1 有意义的命名

1.1 名副其实

变量、函数或类的名称应该已经答复了所有的大问题,她该告诉你,为什么存在,做什么事,应该怎么用。如果名称需要注释来补充,那就不算是名副其实

int d; // 消失的时间

名称d什么也没说明。但是下面的名称是否会好一点呢

int elapsedTimeIndays;
int daysSinceCreation;
int daysSinceModification;
int fileAgeIndays;

1.2 做有意义的区分

作为反面教材,没有意义的区分通常分为以下两类:

  1. 以数字系列命名,例如在下面的例子中,如果将参数改为source和destination,这个函数就会像样很多。
public static void copyChars(char a1[], char a2[]) {
  for (int i = 0; i < a1.length; i++){
    a2[i] = a1[i];
  }
}
  1. 废话是另一种没有意义的区分,例如有一个Product类,还有一个名为ProductData或ProductInfo的类,那她们的名称虽然相同,意义却无区别。variable一词永远不应当出现在变量名中,table一词永远不应当出现在表名中。还有就是像下面的三个函数,你应该调用哪一个呢?
getActiveAccount();
getActiveAccounts();
getActiveAccountInfo();

1.3 使用可搜索的名称

对于单字母名称和数字常量,有一个问题,就是很难在一大篇文字中找出来。找MAX_CLASSES_PER_STUDENT很容易,但是找m就麻烦了。名称的长短应与其作用域大小相对应,若变量或常量可能在代码中多处使用,则应赋予其便于搜索的名称。再比较

for (int i = 0; i < 34; i++){
  s += (t[j] * 4) / 5;
}

int realDaysPerIdealDay = 4;
int WORK_DAYS_PER_WEEK;
int sum = 0;
for (int i = 0; i < NUMBER_OF_TASKS; i++) {
  int realTaskDays = taskEstimate[i] * realDaysPerIdealDay;
  int realTaskWeeks = (realTaskDays / WORK_DAYS_PER_WEEK);
  sum += realTaskWeeks;
}

采用能表达意图的名称,貌似拉长了函数代码,但要想想看,WORK_DAYS_PER_WEEK要比5好找得多,另外,当你需要在一个工程中,快速定位代码位置,起一个易于搜索的名称,也会大大提高效率。

1.4 类名与方法名

类名和对象名应该是名词或名词短语,例如Customer、WikiPage、Account或AddressParser。
方法名应当是动词或动词短语,如postPayment、deletePage或save。

1.5 添加有意义的语境

假设你有名为firstName、lastName、street、houseNumber、city、state和zipcode的变量,把她们放一块儿的时候,很明显的构成了一个地址,但是假设只是在某个方法中看见孤零零的一个state变量呢?你会看出来她是某个地址的一部分呢。可以添加前缀,例如:addressFirstName等。当然更好的做法是创建Address类。

1.6 不要添加没用的语境

假设有一个名为“加油站豪华版”(Gas Station Deluxe)的应用,在其中每个类添加GSD前缀就不是什么好点子了,说白了,你是在和自己的idea过不去,输入G想要idea提示可用类的列表,列表恨不得有一英里那么长。为什么要搞得idea没有办法帮助你。

只要短名称足够清楚,就比长名称好。别给名称添加不必要的语境。

2 函数

2.1 短小

函数的第一条规则是要短小,第二条规则是还要更短小。

代码块和缩进
if、else、while语句等,其中的代码块应该只占一行,该行大抵应该是一个函数调用语句。这样不但能保持函数短小,而且,因为块内调用的函数拥有较具说明性的名称,所以增加了文档上的价值。
这也意味着函数不应该容纳嵌套结构。所以,函数的缩进层级,不应该多于一层或两层。当然,这样的函数易于阅读和理解。

2.2 只做一件事

函数应该做一件事。做好这件事。只做一件事。
如果函数只是做了该函数名下同一抽象层上的步骤,则函数还是只做了一件事。编写函数毕竟是为了把较大的概念,拆分为另一抽象层上的一系列步骤。

public class ProductServiceImpl{

  @Autowired
  private ProductProertyRepository propertyRepository;

  @Autowired
  private ProductPriceReposiotry priceRepository;

  public ProductBO getProduct(String skuId) {
    if (StringUtils.isBlank(skuId){
      return new ProductBO();
    }
    ProductPropertyBO property = propertyRepository.getProperties(skuId);
    ProductPriceBO price = priceRepository.getPrice(skuId);
    return enrichProductBO(property, price);
  }
}

例如上面这段代码,其实做了四件事:

  1. 判断skuId是否为空,如果为空,返回空对象。
  2. 调用属性Repository,查询商品属性。
  3. 调用价格Repository,查询商品价格。
  4. 封装数据。

其实可以说,这四件事是在同一抽象层上。而具体的查询商品属性逻辑,不应该是和其他三步操作,在同一层级才对。

2.3 每个函数一个抽象层级

要确保函数只做一件事,函数中的语句就要在同一层级上。函数中混杂不同的抽象层级,往往会让人迷惑,读者可能无法判断某个表达式是基础概念还是细节。更恶劣的是,就像破窗效应,一旦细节与基础概念混杂,更多的细节就会在函数中纠结起来。

比如上面的getProduct方法中,属性和价格就是基础概念,具体的细节分别在propertyRepository和priceRepository中实现。

2.4 Switch语句

写出短小的Switch语句很难(当然,也包括if/else语句),即便是只有两种条件的Switch语句,也比我想要的单个代码块或函数大得多。
写出只做一件事的Switch语句也很难,Switch天生要做N件事。不得不写Switch语句的时候,要确保每个Switch都埋藏在较低的抽象层级,而且永远不重复。例如下面这段代码:

public Money calculatePay(Employee e) throws InvalidEmployeeType {
  switch (e.type) {
    case COMMISSIONED:
      return cauculateCommissionedPay(e);
    case SALARIED:
      return cauculateSalariedPay(e);
    case HOURLY:
      return cauculateHourlyPay(e);
    default:
      throw new InvalidEmployeeType(e.type);
  }
}

该函数有以下几个问题:

  1. 太长,当出现新的员工类型时,还会变得更长。
  2. 违反了单一职责原则(Single Responsibility Principle),因为有好几个修改她的理由。
  3. 违反了开闭原则(Open Closed Principle),每当添加新类型时,都必须修改该函数。

不过该函数最麻烦的是,可能到处都有类似结构的函数。例如可能会有判断是否发薪日,发放工资等函数。

isPayday(Employee e, Date date);
deliverPay(Employee e, Money pay);

该问题的解决方案是利用多态(或者工厂+策略模式),抽象出来一个员工顶层类Employee,然后分别定义股东员工CommissionedEmployee、普通员工SalariedEmployee、钟点工HourlyEmployee,如下所示:

public abstract class Employee{
  public abstract Money calculatePay(Employee e);
  public abstract isPayday(Employee e, Date date);
  public abstract deliverPay(Employee e, Money pay);
}

public class CommissionedEmployee extends Employee{...}

public class SalariedEmployee extends Employee{...}

public class HourlyEmployee extends Employee{...}

2.5 使用具有描述性的名称

Ward原则:如果每个程序都让你感到符合预期,那就是整洁代码。

You know you are working on clean code when each routine turns out to be pretty much what you excepted

函数越小,功能越集中,就越便于起个好名字。长而具有描述性的名称,要比短而令人费解的名称好。

给函数起个好名字,能较好地解释函数的意图,对于单参数函数,函数和参数应当形成一种非常良好的动词/名词对形式,例如write(content)就表示我要调用write函数写入content。

2.6 函数参数

最理想的参数数量是0,其次是1,再次是2,有足够特殊的理由,才能用3个以上参数。
从测试的角度看,参数更叫人为难,如果你要为一个具有10个参数的函数,编写单元测试代码,想想都很费劲吧。

向函数传入布尔值,简直是骇人听闻。这样做相当于大声宣布,本函数职责不够单一,如果参数为true会这样做,如果为false会那样做。

如果函数看起来需要多个参数,就说明其中一些参数应该封装为类了。例如下面的差别:

Circle makeCircle(double x, double y, double radius);
Circle makeCircle(Point center, double radius);

把多个参数创建为一个对象,看似是在作弊,其实不然,当一组参数被共同传递,往往就是这些参数共同组成了一个概念。就像上面的x和y一样,她们一起组成了坐标点Point的概念。

2.7 无副作用

函数承诺只做一件事,但还是会做其他被隐藏的事,例如下面这段代码,看似无伤大雅,就是查出来用户信息,然后对输入的密码进行加密,然后对比用户真实密码,以此校验密码。

public class UserValidator{
  public boolean checkPassword(String userName, String password) {
    User user = userRepository.getByName(userName);
    if (user == null) {
      return false;
    }
    String userPassword = user.getPassword();
    String cryptedPassword = ctyptographer.crypted(password);
    if (userPassword.equals(cryptedPassword)) {
      Session.initialize();
      return true;
    }
    return false;
  }
}

显然,副作用就在于对Session.initialize()的调用,checkPassword函数,顾名思义,就是用来检查密码的,并未表明她会初始化session。

而且这一副作用造成了时序性的耦合,checkPassword只能在特定时刻调用。如果在不合适的时候调用,session数据就会莫名其妙的丢失。

2.8 分隔写操作与读操作

函数要么做点什么事情,要么回答什么事情,但是不要两件事情都做。函数应该修改某对象的状态,或是返回该对象的有关信息。如果两样都干,常会导致混乱。例如

public boolean set(String attribute, String value);

该函数设置某个属性值,如果成功就返回true,如果属性不存在,就返回false。这样就会导致下面的调用:

if (set("name", "Bryan"))...

当读者看到这个if语句的时候,就会有歧义:

  • name属性是否之前已经设置为Bryan了;
  • name属性是否成功设置为Bryan。

而解决方案就是把读操作和写操作分隔开来,防止混淆的产生。

if (attributeExsists("name")) {
  setAttribute("name", "Bryan");
}

2.9 不要重复自己

它的英文描述为:Don’t Repeat Yourself。中文直译为:不要重复自己。
代码复用性:代码的可复用性表示一段代码可被复用的特性或能力,我们在编写代码的时候,让代码尽量可复用。
如何提高复用性:

  1. 少代码耦合,高度耦合的代码会影响到代码的复用性,我们要尽量减少代码耦合。
  2. 满足单一职责原则,越细粒度的代码,代码的通用性会越好,越容易被复用。
  3. 模块化,这里的“模块”,不单单指一组类构成的模块,还可以理解为单个类、函数。我们要善于将功能独立的代码,封装成模块。独立的模块就像一块一块的积木,更加容易复用,可以直接拿来搭建更加复杂的系统。
  4. 业务与非业务逻辑分离,越是跟业务无关的代码越是容易复用,越是针对特定业务的代码越难复用。
  5. 通用代码下沉,从分层的角度来看,越底层的代码越通用、会被越多的模块调用,越应该设计得足够可复用。
  6. 继承、多态、抽象、封装,利用继承,可以将公共的代码抽取到父类,子类复用父类的属性和方法。利用多态,我们可以动态地替换一段代码的部分逻辑,让这段代码可复用。除此之外,抽象和封装,从更加广义的层面、而非狭义的面向对象特性的层面来理解的话,越抽象、越不依赖具体的实现,越容易复用。代码封装成模块,隐藏可变的细节、暴露不变的接口,就越容易复用。
  7. 应用模板等设计模式,一些设计模式,也能提高代码的复用性。比如,模板模式利用了多态来实现,可以灵活地替换其中的部分代码,整个流程模板代码可复用。

函数小结

写代码和写文章很像,初稿也许粗陋无序,你可以对其斟酌推敲,直至达成你心目中的样子。大师级程序员把系统当做故事来讲,而不是当做程序来写。真正的目标在于讲述系统的故事,而你编写的函数,必须干净利落地拼装到一起,形成一种精确而清晰的语言,帮助你讲故事。

3 注释

“别给糟糕的代码加注释——重新写吧”,如果你发现自己需要写注释,就再想想看,是否有办法用代码来表达。

注释存在的时间越久,离其所描述的代码就越远,就越来越变得全然错误。原因很简单:程序员不能坚持维护注释。

带有少量注释的整洁而有表达力的代码,要比带有大量注释的零碎而又复杂的代码,像样的多。与其花时间为糟糕的代码编写注释,不如花时间清理那堆糟糕的代码。
你愿意看到这个:

// 校验员工是否符合福利的条件
if (employee.flags & HOURLY_FLAG ** employee.age > 65)

还是下面这个:

if (employee.isEligibleForBenefits())

最好的代码是没有注释,代码已经清晰明了的表达了作者的意图;其次是代码没有那么的清晰,但是有一定的注释可以帮你了解;最差的就是代码看不明白,注释也混乱不清。

4 格式

你今天编写的功能代码,极有可能在下一版本中被修改,但代码的可读性却会对以后可能发生的修改行为产生深远影响。

4.1 向报纸学习

想想写的好的报纸文章,你从上往下阅读,在顶部,你期望有个头条,告诉你故事主题,好让你决定是否读下去。第一段是整个故事的大纲,给出粗线条概述,但隐藏了故事细节。接着读下去,细节渐次增加,直至你了解所有的日期、名字、说法及其他细节。

源文件也要像报纸文章那样。名称应当简单且一目了然。名称本身应该足以告诉我们是否在正确的模块中。源文件最顶部应该给出高层次的概念和算法。细节应该往下渐次展开,直至找到源文件中最底层的函数和细节。

4.2 垂直格式

你是否曾经在某个类中探索,从一个函数跳到另一个函数,上下求索,想要弄清楚这些函数如何操作、如何互相相关,最后却被搞糊涂了。

关系密切的概念应该互相靠近,应避免迫使读者在源文件中跳来跳去。

变量声明。变量声明应尽可能靠近其使用位置。

实体变量。实体变量应该在类的顶部声明。这不会增加变量的垂直距离,因为在设计良好的类中,她们应该会被大多数方法使用。

相关函数。若某个函数调用了另外一个,就应该把她们放到一起,而且调用者尽可能放在被调用者上面。这样程序就会有自然的顺序。

概念相关。概念相关的代码应该放到一起。代码的相关性越强,彼此之前的距离就应该越短。

4.3 横向格式

程序员们都喜欢短代码行,应该尽力保持代码行短小。最好是无需拖动滚动条,就可以看到代码最右边。
横向的分隔可以参考《阿里巴巴Java开发手册》。

public static void main(String[] args) { 
  // 缩进 4 个空格 
  String say = "hello"; 
  // 运算符的左右必须有一个空格 
  int flag = 0; 
  // 关键词 if 与括号之间必须有一个空格,括号内的 f 与左括号,0 与右括号不需要空格 
  if (flag == 0) { 
    System.out.println(say); 
  } 
 
  // 左大括号前加空格且不换行;左大括号后换行 
  if (flag == 1) { 
    System.out.println("world"); 
    // 右大括号前换行,右大括号后有 else,不用换行 
  } else { 
    System.out.println("ok"); 
    // 在右大括号后直接结束,则必须换行 
  } 
}
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,271评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,275评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,151评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,550评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,553评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,559评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,924评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,580评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,826评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,578评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,661评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,363评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,940评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,926评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,156评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,872评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,391评论 2 342

推荐阅读更多精彩内容