调试是“以交互方式运行程序/方法,在每个语句后中断执行流程并显示……的过程。”简而言之,它是一种非常有用的技术……对于一个糟糕的程序员而言。 或仍然在用C编写过程代码的老程序员。面向对象的程序员从不调试其代码-他们编写单元测试。 我的意思是,单元测试是一种完全替代调试的技术。 如果需要调试,则设计很糟糕 。
The Revenant(2015),作者:Alejandro G.Iñárritu
假设我是一个糟糕的命令式程序程序员,这是我的Java代码:
class FileUtils {
public static Iterable<String> readWords(File f) {
String text = new String(
Files.readAllBytes(Paths.get(f)),
"UTF-8"
);
Set<String> words = new HashSet<>();
for (String word : text.split(" ")) {
words.add(word);
}
return words;
}
}
此静态实用程序方法读取文件内容,然后在其中找到所有唯一的单词。 很简单 但是,如果它不起作用,我们该怎么办? 假设这是文件:
We know what we are,
but know not what we may be.
从中,我们得到以下单词列表:
"We"
"know"
"what"
"we"
"are,\n"
"but"
"not"
"may"
"be\n"
现在,这对我而言似乎不正确……那么下一步是什么? 文件读取无法正常工作或拆分中断。 让我们调试吧? 让我们通过输入为它提供文件,并逐步进行操作,跟踪并观察变量。 我们将找到该错误并进行修复。 但是,当出现类似问题时,我们将不得不再次调试! 这就是单元测试应该避免的 。
我们应该一次创建一个单元测试,以重现该问题。 然后,我们解决问题并确保测试通过。 这就是我们节省解决问题投资的方式。 我们不会再修复它,因为它不会再发生。 我们的测试将阻止它的发生。
如果您认为调试变得更快,更轻松,请考虑一下代码的质量
但是,只有在创建单元测试很容易的情况下,所有这些方法才有效。 如果困难的话,我会懒得做。 我将调试并解决问题。 在此特定示例中,创建测试是相当昂贵的过程。 我的意思是单元测试的复杂度会很高。 我们必须创建一个临时文件,用数据填充它,运行该方法,然后检查结果。 为了弄清楚到底发生了什么,以及漏洞在哪里,我必须创建一些测试。 为了避免代码重复,我还必须创建一些补充实用程序来帮助我创建该临时文件并用数据填充它。 这是很多工作。 好吧,也许不是“很多”,而是经过了数分钟的调试。
因此,如果您认为调试更快,更轻松,请考虑一下代码的质量。 我敢打赌,它有很多重构的机会,就像上面示例中的代码一样。 这是我将如何修改它。 首先,我将其转换为一个类,因为实用程序静态方法是一种不好的做法 :
class Words implements Iterable<String> {
private final File file;
Words(File src) {
this.file = src;
}
@Override
public Iterator<String> iterator() {
String text = new String(
Files.readAllBytes(Paths.get(this.file)),
"UTF-8"
);
Set<String> words = new HashSet<>();
for (String word : text.split(" ")) {
words.add(word);
}
return words.iterator();
}
}
看起来已经更好了,但是复杂性仍然存在。 接下来,我将其分解为较小的类:
class Text {
private final File file;
Text(File src) {
this.file = src;
}
@Override
public String toString() {
return new String(
Files.readAllBytes(Paths.get(this.file)),
"UTF-8"
);
}
}
class Words implements Iterable<String> {
private final String text;
Words(String txt) {
this.text = txt;
}
@Override
public Iterator<String> iterator() {
Set<String> words = new HashSet<>();
for (String word : this.text.split(" ")) {
words.add(word);
}
return words.iterator();
}
}
您现在怎么看? 为Words类编写测试是一项非常简单的任务:
import org.junit.Test;
import static org.hamcrest.MatcherAssert.*;
import static org.hamcrest.Matchers.*;
public class WordsTest {
@Test
public void parsesSimpleText() {
assertThat(
new Words("How are you?"),
hasItems("How", "are", "you")
);
}
}
那花了多少时间? 少于一分钟。 我们不需要创建一个临时文件并向其中加载数据,因为Words类对文件没有任何作用。 它只是解析输入的字符串并在其中找到唯一的单词。 现在,由于测试很小,我们可以轻松创建更多测试,因此很容易修复。 例如:
import org.junit.Test;
import static org.hamcrest.MatcherAssert.*;
import static org.hamcrest.Matchers.*;
public class WordsTest {
@Test
public void parsesSimpleText() {
assertThat(
new Words("How are you?"),
hasItems("How", "are", "you")
);
}
@Test
public void parsesMultipleLines() {
assertThat(
new Words("first line\nsecond line\n"),
hasItems("first", "second", "line")
);
}
}
我的观点是,当编写单元测试的时间远远大于单击那些“ Trace-In / Trace-Out”按钮所花费的时间时,必须进行调试。 这是合乎逻辑的。 我们都很懒惰,想要快速简便的解决方案。 但是调试会浪费时间并浪费能量。 它可以帮助我们发现问题,但并不能阻止它们再次出现。
当我们的代码是需要调试的程序和算法,当代码是所有的目标应该如何实现的,而不是我们的目标是什么 。 再次参见上面的示例。 第一个静态方法是关于我们如何读取文件,解析文件以及查找单词的所有方法。 它甚至被命名为readWords() (一个动词 )。 相反,第二个例子是关于将要实现的。 它可以是文件的Text ,也可以是Text的Words (都是名词 )。
我相信在干净的面向对象编程中没有调试的地方。 只有单元测试!
翻译自: https://www.javacodegeeks.com/2016/11/are-you-still-debugging.html