提高代码可续性的小技巧,以connectTo方法为例。

源代码有两种不同的用户:程序员和计算机。一方面,计算机既能处理干净、结构良好的代码,也能处理混乱的代码。另一方面,程序员对代码的可读性很敏感。甚至是代码中的空白、正确使用缩进(这与计算机完全无关)也决定了代码容易理解或难以理解。

此外,代码的可读性也提高了可靠性,因为通常不容易隐藏一些bug。并且提高了可维护性,因为它更容易修改。

关于可读性的一些想法

编写可读的代码是一门被低估的技术,学校很少教授这种技术,但它却与软件的可靠性、维护和发展密切相关。程序员通常学习用机器容易理解的东西来实现所需功能的代码。这个编码过程需要添加一层又一层的抽象来将功能分解为更小的单元。

Java语言中,这些抽象是包、类和方法。如果整个系统足够大,就没有程序员可以单独控制整个代码库。有些开发者对某个特定的业务有一个纵深的认识。其他开发人员可能只负责一个抽象层并维护它的API。他们都需要经常阅读和理解别人编写的代码。提高可读性意味着将程序员理解一段代码所需的时间最小化。

如何编写可读的程序?用一句关于表现力的格言来总结就是,简单而直接地说出你的意思。事实上,可读性意味着清楚地表达代码意图。统一建模语言(UML)设计师之一格雷迪·布克(Grady Booch)给出了一个自然的类比:干净的代码读起来就像优美的散文。

写好散文不是简单地遵循一套固定的规则,而是需要练习和阅读著名作家的伟大文章,这一过程可能需要数年时间。幸运的是,与自然语言相比,计算机代码的表达能力非常有限,所以编写出干净的代码比写优美的散文更容易,或者至少更有条理。业界对重构和编写干净的代码越来越感兴趣。可读性已经成为敏捷开发中最重要的关注点之一。

试图使用一组简单的数字指标(如标记)来评估可读性。标识符的长度、表达式中出现的括号的数量,等等。这项工作仍在进行中,要达成一个稳定的共识还有很长的路要走,我们接下来将通过一个例子来说明和解释代码可读性的一些改进点。

整理connectTo方法

现在我们将注意力转向一个名叫connectTo的方法,该方法会将两个组里面的容器进行合并,并且容器里面的水会被均分。对其进行重构以提高可读性。首先查看初始版本的实现:

public void connectTo(Container other) {
   // 如果两个容器已经连接,则不做任何事情
   if (group==other.group) return;
   int size1 = group.size(),
   size2 = other.group.size();
   double tot1 = amount * size1,
   tot2 = other.amount * size2,
   newAmount = (tot1 + tot2) / (size1 + size2);
   // 合并两个组
   group.addAll(other.group);
   // 更新要连接的所有容器的组
   for (Container c: other.group) { c.group = group; }
   // 更新所有新连接的容器
   for (Container c: group) { c.amount = newAmount; }
}

这里有一个缺陷:它包含了大量的注释,试图解释每一行代码的含义。有些程序员关心他们的同事,想要他们更好的理解代码,自然会添加这样的注释。然而,这并不是实现容易理解这一目标的最有效的方法。更好的选择是使用提取方法的方式来进行重构。

可读性提示:“提取方法”重构规则——提取可以实现某一个小功能的代码块转到一个新方法并使用描述性名称。

我们可以在connectTo方法中应用这种技术。事实上,我们可以拆分5个新的方法,以及获得一个新的、可读性更强的connectTo方法:

/** Connects this container with another.
 *
 * @param other The container that will be connected to this one
 */
public void connectTo(Container other) {
   if (this.isConnectedTo(other)) return;
   double newAmount = (groupAmount() + other.groupAmount()) /      
   (groupSize() + other.groupSize());
   mergeGroupWith(other.group);
   setAllAmountsTo(newAmount);
}

这个方法更短,可读性更强。如果你试着把这个方法大声读出来,你会发现它几乎可以变成可以被理解的一个短文。为此,我们引入了五种适当的支持方法。事实上,很多业内的大佬都认为长方法是一种不好的代码味道,提取方法来消除这种坏味道是普遍被采纳的一种重构技术。

添加注释只能解释部分代码,而提取方法既解释代码又隐藏生成过程代码——将代码提取到单个方法中。在这个例子中,它会使原来的方法抽象级别保持在更高、更统一的高度,避免了旧版本代码中的高层API解释和底层实现错综复杂地交织在一起。

查询替换局部变量是另一种可用于connectTo方法的重构技术。

可读性提示:“用查询替换局部变量”重构规则——更改局部变量,通过调用一个计算其值的新方法来替换该量。你可以将此技术应用于局部变量newAmount,该变量只分配一次,然后用作setAllAmountsTo方法的参数。应用该技术可以直接删除变量newAmount,并将connectTo方法的最后两行替换为以下内容。

mergeGroupWith(other.group);
setAllAmountsTo(amountAfterMerge(other));

amountAfterMerge是一个计算合并后的每个容器水量的新方法。但是,稍加思考就会发现,amountAfterMerge方法需要克服很多困难才能完成任务,因为在调用方法时,两个group已经完成了合并。group已经包含了other的group。一个很好的折衷方案是将计算新水量的表达式封装到一个新方法中,同时保留局部变量,以便在合并组之前计算出新的量。

final double newAmount = amountAfterMerge(other);
mergeGroupWith(other.group);
setAllAmountsTo(newAmount);

总而言之,我不建议进行这种重构,如抽出5个方法版本中的代码所示newAmount表达式是可读的,不需要隐藏在单独的方法中。当它替换的表达式很复杂或在类中多次出现时,“用查询替换局部变量”规则通常更有用。

现在看看可读版本中connectTo方法的五个新支持方法。在这五个方法中,有两个最好声明为私有的,因为它们可能导致容器对象处于不一致的状态,不应该从类外部调用。他们是mergeGroupWith方法和setAllAmountsTo方法。

mergeGroupWith方法合并两组容器而不更新它们的水量。如果有人单独从外部调用它,很可能使一些或所有容器的水量发生错误。这个方法只有在使用它的上下文中才有意义:在connectTo方法的末尾,然后调用setAllAmountsTo方法。事实上,它是否真的应该独立成一个方法是有争议的。

一方面,让它独立可以通过给予它一个好名字来解释它的用途,而不是像开始的版本那样使用注释解释。另一方面,独立出来的方法可能在错误的上下文中被调用。因为我们是为了可读性而优化的,所以创建独立的方法会更好一点。类似的权衡setAllAmountsTo方法也适用。

private void mergeGroupWith(Set<Container> otherGroup) {
   group.addAll(otherGroup);
   for (Container x: otherGroup) {
     x.group = group;
   }
}

private void setAllAmountsTo(double amount) {
   for (Container x: group) {
     x.amount = amount;
   }
}

私有方法不值得用Javadoc注释。它们只在类内部使用,所以很少有人觉得有必要了解他们的细节。因此,添加注释不是太有必要的。注释的成本并不限于编写它们所需的时间。就像其他源代码一样,它需要维护,否则可能会过时。也就是说,随着版本的迭代,注释和它所描述的代码不同步了。

记住:过时的评论比没有评论更糟糕! 用描述性名称代替注释并不能避免这种风险。如果编写的代码功能和名称不符了,然后最终仍然可能产生一些过时的名称,这和过时的注释同样糟糕。

其他三种新的支持方法都是只读特性,不会带来任何不良影响。我们不应该轻易做出让他们公有化的决定。添加到类中任何公共成员的后续维护成本都要比添加相同的私有成员的成本大得多。公共方法的额外成本包括:

  1. 描述其功能的适当注释;
  2. 条件检查,以处理可能不正确的输入内容;
  3. 一套完整的测试,以确保其正确性。

connectTo方法的三个新的公有支持方法:

/** Checks whether this container is connected to another one.
 *
 * @param other the container whose connection with this will be
checked
 * @return <code>true</code> if this container is connected
 * to <code>other</code>
 */
public boolean isConnectedTo(Container other) {
 return group == other.group;
}

/** Returns the number of containers in the group of this
container.
 *
 * @return the size of the group
 */
public int groupSize() {
 return group.size();
}

/** Returns the total amount of water in the group of this
container.
 *
 * @return the amount of water in the group
 */
public double groupAmount() {
 return amount * group.size();
}

顺便说一下,isConnectedTo方法还改进了类的可测试性,因为它使以前在实现中需要推测的内容都变成了直接可测试的。实现connectTo的六个方法都非常短,其中connectTo是最长的方法本身只有6行。简洁是干净代码的主要原则之一

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

推荐阅读更多精彩内容