IntelliJ IDEA 复杂的重构技巧(二)

本文作者:ice1000 原文链接:http://ice1000.org/2017/12/25/IDEARefactoring2/

上次我说了一些 “复杂的重构技巧” ,讲的是一些使用 IntelliJ 的简单功能实现复杂的重构需求的技巧。 看到大家的反响之后我就感觉那个可能不大亲民,因为很多人连 inline 这功能都不知道(那岂不是把 IntelliJ 用成了记事本), 于是我决定再写一篇讲讲 IntelliJ 已经提供好了的一些复杂的重构功能。

这就不再是需要自己进行奇奇怪怪的操作的教程了,就会亲民得多。

从方法中提取方法

这是用来快速复用一段代码的功能,名叫 “Extract Method” 。 比如,我现在有这么一段业务代码(顺带一提,这是在 Java 调用动态语言 API 时能使用的最健壮的处理数值类型的方法):

liceEnv.defineFunction("run-later", ((metaData, nodes) -> {
  Number time = (Number) nodes.get(0).eval();
  Consumer<Node> nodeConsumer = Node::eval;
  if (time != null) runLater(time.longValue(), () -> {
    for (int i = 1; i < nodes.size(); i++) {
      // 截图之前写的时候脑抽了,这个是后来改的
      nodeConsumer.accept(nodes.get(i));
    }
  });
  return new ValueNode(null, metaData);
}));
...

为了效率考虑,你决定不使用 subList(1,nodes.size()).forEach 而是使用 for 循环。

然后你突然发现,这个 “遍历一个集合除了第一个元素之外的元素” 操作在你的代码里面已经被调用了很多次了。 于是你决定贯彻 “非极端性 DRY 原则” ,把这坨代码复用起来。

我们仔细观察一下。 这坨代码中,直觉上,我们希望可以通过形如

nodes.forEachExceptFirst(someOperation::accept)

的代码来一行处理这个操作的(不懂方法引用的请退群),但是这个 forEachExceptFirst 是不存在的。

所以我们想自己造一个。 这时候我们就应该使用 IntelliJ IDEA 提供的 Extractmethod 功能了。

首先选中那一堆代码,然后按下 Ctrl+Alt+m,看到这么一个窗口。

然后我们在 “Name” 那一栏输入 forEachExceptFirst ,也就是我们想提取的函数的函数名;然后回车。

我们可以看到,代码变成了这样:

liceEnv.defineFunction("run-later", ((metaData, nodes) -> {
  Number time = (Number) nodes.get(0).eval();
  Consumer<Node> nodeConsumer = Node::eval;
  if (time != null) runLater(time.longValue(), () -> {
    forEachExceptFirst(nodes, nodeConsumer);
  });
  return new ValueNode(null, metaData);
}));
...

我们可以看看它生成的这个 forEachExceptFirst 方法:

private void forEachExceptFirst(
    List<? extends Node> nodes,
    Consumer<Node> nodeConsumer) {
  for (int i = 1; i < nodes.size(); i++) {
     nodeConsumer.accept(nodes.get(i));
  }
}

然后你就可以在其他地方使用这个方法了。

我们可以给它加上 JetBrains annotations:

private void forEachExceptFirst(
    @NotNull List<@NotNull ? extends @NotNull Node> nodes,
    @NotNull Consumer<@NotNull Node> nodeConsumer) {
  for (int i = 1; i < nodes.size(); i++) {
     nodeConsumer.accept(nodes.get(i));
  }
}

当然加这么多意义不大,对 Node 类型的 @NotNull 注解是可以去掉的。

撤回这个操作的话,请使用上一篇博客所大量使用的 inline 功能。

从类中提取接口

比如,我们有这么一个 Java 类(最近突然觉得,对类型的注解应该比可见性修饰符更靠近类型(比如在一个方法中, 我就可以用这种方法来区分对返回类型的注解(比如 @NotNull)和对方法本身的注解(比如 @Override)), 所以就有了这么个把注解写在可见性修饰符后面的奇怪的写法,希望读者不要介意这一点)。

public class Marisa {
  // blablabla

  public Marisa(@NotNull Touhou game) {
    // blablabla
  }

  public @NotNull ImageObject player() {
    return player;
  }

  public @NotNull List<@NotNull ImageObject> bullets() {
    return makeLeftRightBullets(player, mainBullet);
  }

  public void dealWithBullet(@NotNull ImageObject bullet) {
    // blablabla
  }
}

代码中省去了一些对文章不重要的细节。

然后我们可以在类名上右键,然后找到这个东西:

这样我们会看到一个窗口,里面的东西还挺复杂的:

首先我们在 “Interface name” 那里填我们想抽取的接口的名字,比如刚刚的那个类 Marisa ,就很适合 GensokyoManagement (毕竟魔理沙是幻想乡两位城管之一嘛,又因为城管的翻译是 Urbanmanagement) 这个名字的接口。

然后我们希望把这三个方法都抽取到接口里面去,于是就勾选下面的三个方法。请根据实际需求勾选需要抽取的方法。 最后回车。

这时候 IntelliJ IDEA 会询问你,是否 “尽可能在这个类被使用的地方,把这个类的类型改成接口的类型”。

这是一种很好的作法,比如我们会倾向于把

LinkedList<Marisa> gensokyoManagements = new LinkedList<Marisa>();

写成

List<GensokyoManagement> gensokyoManagements = new LinkedList<Marisa>();

,对不对吖。 这里这个提示就是问你要不要这么换一波的。这个就看需求了,另外建议取消勾选下面的 “Preview usages to be changed”。

最后我们就提取出来了这么个玩意(这里只有三个方法所以生成的代码很少,看起来不是很高大上, 如果你实现了一种操作比较多的数据结构(比如线段树啊,各种图啊树啊)再这么来一波,就能生成一大坨):

public interface GensokyoManagement {
  @NotNull ImageObject player();

  @NotNull List<@NotNull ImageObject> bullets();

  void dealWithBullet(@NotNull ImageObject bullet);
}

然后我们就可以再写其他类,比如:

public class Reimu implements GensokyoManagement {
}

然后让 IntelliJ IDEA 自动生成之前那些方法,然后我们就可以愉快地写实现啦。

接口与实现间的互相发送代码

我们还有很多可以做的事情,比如我们现在给 Marisa 类加了新方法作为新功能,然后我们想给 Reimu 也加上, 并把这个方法作为 GensokyoManagement 的一个抽象方法之一(接口的方法是默认抽象的,别因为省了 abstract 修饰符就以为不是了):

public @NotNull List<@NotNull ImageObject> spellCard() {
  return masterSpark();
}

我们可以这样,在新方法上右键,然后这么选:

这样我们会看到一个窗口,里面的东西不怎么复杂:

只需要勾选我们要送给接口(或者父类)的方法,然后回车就好了。 IntelliJ IDEA 会给你加上 @Override 修饰符,和生成新的抽象方法。

然后我们就可以跳到 Reimu 类,让 IntelliJ IDEA 生成一个空实现,然后接着写啦。

原文发布于微信公众号 - 程序猿DD(didispace)

原文发表时间:2017-12-29

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏程序员的诗和远方

异步多图加载这件小事儿(Promise与async)

日常开发过程中,时不时会遇到要同时预加载几张图片,并且等都加载完再干活的情况,结合 Promise 和 async/await 代码会优雅很多,但也容易遇到坑,...

3608
来自专栏JetpropelledSnake

Linux学习笔记之Redis中5种数据结构的使用场景介绍

原来看过 redisbook 这本书,对 redis 的基本功能都已经熟悉了,从上周开始看 redis 的源码。目前目标是吃透 redis 的数据结构。我们都知...

1241
来自专栏编程

改进异常处理的 6 条建议

来源:ImportNew - 唐尤华 , 合理地使用异常处理可以帮你节省数小时(甚至数天)调试时间。一个乘法异常会毁掉你的晚餐乃至周末计划。如果处置不及时,甚至...

2429
来自专栏Micro_awake web

一些关于python的小感想

python是一门优秀的语言,但随之而来的是大量的知识,各种模块,相信一个人的大脑是很难记住如此多的内容。这时后的我们就应该想办法避免去记忆这么多的内容。 1。...

2207
来自专栏为数不多的Android技巧

我为Dexposed续一秒——论ART上运行时 Method AOP实现

两年前阿里开源了Dexposed 项目,它能够在Dalvik上无侵入地实现运行时方法拦截,正如其介绍「enable ‘god’ mode for single ...

2922
来自专栏学习力

《Java从入门到放弃》框架入门篇:Struts2的常用验证方式

2118
来自专栏编程

java基础思维图解

Java是一门面向对象编程语言,不仅吸收了C++语言的各种优点,还摒弃了C++里难以理解的多继承、指针等概念,因此Java语言具有功能强大和简单易用两个特征。J...

2199
来自专栏佳爷的后花媛

h文件和c文件的区别include本身只是一个简单的文件包含预处理命令,即为把include的后面文件放到这条命令这里,除此之外,没有其它的用处(至少我也样认为).

其实在H文件里写函数也无所谓,只是不符合习惯而已。只要按照以上的格式写,一个H文件添加多少次都无所谓,

2412
来自专栏惨绿少年

Shell编程基础篇-上

1.1 前言 1.1.1 为什么学Shell Shell脚本语言是实现Linux/UNIX系统管理及自动化运维所必备的重要工具, Linux/UNIX系统的底层...

2180
来自专栏微信公众号:Java团长

Java异常进阶

在使用Java编写应用的时候,我们常常需要通过第三方类库来帮助我们完成所需要的功能。有时候这些类库所提供的很多API都通过throws声明了它们所可能抛出的异常...

1284

扫码关注云+社区

领取腾讯云代金券