3.4 重构遗留代码

为了进一步阐释如何重构遗留代码,本节将举例说明如何将一段使用循环进行集合操作的代码,重构成基于Stream的操作。重构过程中的每一步都能确保代码通过单元测试,当然你也可以自行实际操作一遍,体验并验证。

假定选定一组专辑,找出其中所有长度大于1分钟的曲目名称。例3-19是遗留代码,首先初始化一个Set对象,用来保存找到的曲目名称。然后使用for循环遍历所有专辑,每次循环中再使用一个for循环遍历每张专辑上的每首曲目,检查其长度是否大于60秒,如果是,则将该曲目名称加入Set对象。

例3-19 遗留代码:找出长度大于1分钟的曲目

  1. public Set<String> findLongTracks(List<Album> albums) {
  2. Set<String> trackNames = new HashSet<>();
  3. for(Album album : albums) {
  4. for (Track track : album.getTrackList()) {
  5. if (track.getLength() > 60) {
  6. String name = track.getName();
  7. trackNames.add(name);
  8. }
  9. }
  10. }
  11. return trackNames;
  12. }

如果仔细阅读上面的这段代码,就会发现几组嵌套的循环。仅通过阅读这段代码很难看出它的编写目的,那就来重构一下(使用流来重构该段代码的方式很多,下面介绍的只是其中一种。事实上,对Stream API越熟悉,就越不需要细分步骤。之所以在示例中一步一步地重构,完全是出于帮助大家学习的目的,在工作中无需这样做)。

第一步要修改的是for循环。首先使用StreamforEach方法替换掉for循环,但还是暂时保留原来循环体中的代码,这是在重构时非常方便的一个技巧。调用stream方法从专辑列表中生成第一个Stream,同时不要忘了在上一节已介绍过,getTracks方法本身就返回一个Stream对象。经过第一步重构后,代码如例3-20所示。

例3-20 重构的第一步:找出长度大于1分钟的曲目

  1. public Set<String> findLongTracks(List<Album> albums) {
  2. Set<String> trackNames = new HashSet<>();
  3. albums.stream()
  4. .forEach(album -> {
  5. album.getTracks()
  6. .forEach(track -> {
  7. if (track.getLength() > 60) {
  8. String name = track.getName();
  9. trackNames.add(name);
  10. }
  11. });
  12. });
  13. return trackNames;
  14. }

在重构的第一步中,虽然使用了流,但是并没有充分发挥它的作用。事实上,重构后的代码还不如原来的代码好——天哪!因此,是时候引入一些更符合流风格的代码了,最内层的forEach方法正是主要突破口。

最内层的forEach方法有三个功用:找出长度大于1分钟的曲目,得到符合条件的曲目名称,将曲目名称加入集合Set。这就意味着需要三项Stream操作:找出满足某种条件的曲目是filter的功能,得到曲目名称则可用map达成,终结操作可使用forEach方法将曲目名称加入一个集合。用以上三项Stream操作将内部的forEach方法拆分后,代码如例3-21所示。

例3-21 重构的第二步:找出长度大于1分钟的曲目

  1. public Set<String> findLongTracks(List<Album> albums) {
  2. Set<String> trackNames = new HashSet<>();
  3. albums.stream()
  4. .forEach(album -> {
  5. album.getTracks()
  6. .filter(track -> track.getLength() > 60)
  7. .map(track -> track.getName())
  8. .forEach(name -> trackNames.add(name));
  9. });
  10. return trackNames;
  11. }

现在用更符合流风格的操作替换了内层的循环,但代码看起来还是冗长繁琐。将各种流嵌套起来并不理想,最好还是用干净整洁的顺序调用一些方法。

理想的操作莫过于找到一种方法,将专辑转化成一个曲目的Stream。众所周知,任何时候想转化替代代码,都该使用map操作。这里将使用比map更复杂的flatMap操作,把多个Stream合并成一个Stream并返回。将forEach方法替换成flatMap后,代码如例3-22所示。

例3-22 重构的第三步:找出长度大于1分钟的曲目

  1. public Set<String> findLongTracks(List<Album> albums) {
  2. Set<String> trackNames = new HashSet<>();
  3. albums.stream()
  4. .flatMap(album -> album.getTracks())
  5. .filter(track -> track.getLength() > 60)
  6. .map(track -> track.getName())
  7. .forEach(name -> trackNames.add(name));
  8. return trackNames;
  9. }

上面的代码中使用一组简洁的方法调用替换掉两个嵌套的for循环,看起来清晰很多。然而至此并未结束,仍需手动创建一个Set对象并将元素加入其中,但我们希望看到的是整个计算任务由一连串的Stream操作完成。

到目前为止,虽然还未展示转换的方法,但已有类似的操作。就像使用collect(Collectors.toList())可以将Stream中的值转换成一个列表,使用collect(Collectors.toSet())可以将Stream中的值转换成一个集合。因此,将最后的forEach方法替换为collect,并删掉变量trackNames,代码如例3-23所示。

例3-23 重构的第四步:找出长度大于1分钟的曲目

  1. public Set<String> findLongTracks(List<Album> albums) {
  2. return albums.stream()
  3. .flatMap(album -> album.getTracks())
  4. .filter(track -> track.getLength() > 60)
  5. .map(track -> track.getName())
  6. .collect(toSet());
  7. }

简而言之,选取一段遗留代码进行重构,转换成使用流风格的代码。最初只是简单地使用流,但没有引入任何有用的流操作。随后通过一系列重构,最终使代码更符合使用流的风格。在上述步骤中我们没有提到一个重点,即编写示例代码的每一步都要进行单元测试,保证代码能够正常工作。重构遗留代码时,这样做很有帮助。