工具类(或者说辅助类)是一种只有静态方法和封装,没有状态的结构。Apache Commons里的StringUtils、IOUtils、FileUtils,Guava里的Iterables和Iterators,以及JDK7里的Files都是工具类的完美例子。
这种设计思想在Java世界里非常普遍(C#、Ruby等亦是如此),因为工具类提供了任何地方都能使用的通用功能。
这里,我们想要遵从不要重复劳作的原则避免重复,所以,我们将公共代码块放到工具类里,必要时再次使用:
// This is a terrible design, don't reuse
public class NumberUtils {
public static int max(int a, int b) {
return a > b ? a : b;
}
}
事实上,这真是一个方便的手段吗?!
工具类是邪恶的
然而,在面向对象的世界里,工具类被认为是不好的(甚至可以说是“糟糕的”)习惯。
已经有很多关于这个话题的讨论了,举几个名字:Nick Malik 的 Are Helper Classes Evil, Simon Hart 的 Why helper, singletons and utility classes are mostly bad, Marshal Ward 的 Avoiding Utility Classes, Dhaval Dalal 的 Kill That Util Class!, Rob Bagby 的 Helper Classes Are A Code Smell。
此外, 在Stack Exchange有一些关于工具的问题:If a “Utilities” class is evil, where do I put my generic code?, Utility Classes are Evil。
所有他们的观点简单概括就是工具类不是严格意义上的对象,所以,他们不适合放到面向对象的世界里。它们继承了过程化编程,主要是因为那时我们习惯于功能分解的范式。
假如你同意这些观点并且想要停止使用工具类。我将通过例子展示这些工具这样被严格意义上的对象替换。
有关程序的例子
比如说,你想读取一个文本文件,按行分割,削减每行并且将结果保存到另一个文件里。这个可以用Apache Commons的FileUtils来做:
void transform(File in, File out) {
Collection<String> src = FileUtils.readLines(in, "UTF-8");
Collection<String> dest = new ArrayList<>(src.size());
for (String line : src) {
dest.add(line.trim());
}
FileUtils.writeLines(out, dest, "UTF-8");
}
上面的代码可能看起来很干净,然而,这是过程化编程而不是面向对象。我们操作数据(字节和位)时,每一行代码明确指示计算机从哪儿取数据然后放到哪里去。我们正在定义一个程序。
面向对象的选择
在一个面向对象的范式,我们应该实例化并组合对象,让它们管理的数据,而不是调用辅助静态函数,我们应该创建能够将我们需要的行为暴露出来的对象:
public class Max implements Number {
private final int a;
private final int b;
public Max(int x, int y) {
this.a = x;
this.b = y;
}
@Override
public int intValue() {
return this.a > this.b ? this.a : this.b;
}
}
这个过程调用:
int max = NumberUtils.max(10, 5);
将变成面向对象:
int max = new Max(10, 5).intValue();
对象代替数据结构
这是我用面向对象的方式设计和上面一样的文件转换功能:
void transform(File in, File out) {
Collection<String> src = new Trimmed(
new FileLines(new UnicodeFile(in))
);
Collection<String> dest = new FileLines(
new UnicodeFile(out)
);
dest.addAll(src);
}
FileLines实现Collection并且封装所有文件读写操作。FileLines实例完全充当了一个字符串集合,并且隐藏了所有I/O操作。当我遍历它的时候文件开始读取,当我调用它的addAll()方法时文件开始写入。
Trimmed同样实现了Collection并且封装了一个字符串集合(装饰者模式)。每次下一行被检索时,它得到已修剪过的。
参与片段的所有类都相当小:Trimmed、FileLines和UnicodeFile。它们每一个为自己单一的作用负责,因此完美的符合单一职责原则。
站在我们的角度,作为库的使用者,这可能没那么重要,但是作为他们开发人员来说这是必要的,开发、维护和单元测试类FileLines比用一个在拥有80+个方法3000行代码的工具类FileUtils中的readLines()方法容易很多。说真的,看看它的源代码。
一个面向对象处理是可以延迟执行的。in文件没有被读取直到它的数据被需要的时候。如果我们因为I/O错误导致没能打开out文件,第一个文件是无法感知的。只有当我们调用allAll()时完整的展示才开始。
第二段的所有行,除了最后一行,实例化和组合小对象到大对象里。对象组合是相当廉价的对CPU而言,因为它不会引起任何数据的转换。
此外,显然第二段运行在O(1)的空间,而第一段需要执行O(n)里。这是我们有第一个处理数据的结果。
在面向对象是的世界里没有数据,只有对象和它们的行为。
本文是一篇译文,点击《OOP Alternative to Utility Classes》查看原文,如有翻译不当的地方欢迎指出。如需转载,请标明原文和译文的出处,谢谢。
微信扫一扫 查看更多内容