亲宝软件园·资讯

展开

Java connectTo

扑腾的蛾子 人气:0

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

此外,代码的可读性也提高了可靠性,因为通常不容易隐藏一些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注释。它们只在类内部使用,所以很少有人觉得有必要了解他们的细节。因此,添加注释不是太有必要的。注释的成本并不限于编写它们所需的时间。就像其他源代码一样,它需要维护,否则可能会过时。也就是说,随着版本的迭代,注释和它所描述的代码不同步了。

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

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

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行。简洁是干净代码的主要原则之一。

加载全部内容

相关教程
猜你喜欢
用户评论