任何优雅对象的类必须是抽象的或final的,我相信,这条规则背后的意图是消除继承。继承的缺点和子类型的缺点是相当清楚的,所以我不会在这里强调,然而,在我的实践中,我很快意识到这条规则出了问题。
比如下面案例来自 Cactoos框架 的TeeInput类,注意下面代码final class:
public final class TeeInput implements Input {
/**
* The source.
*/
private final Input source;
/**
* The destination.
*/
private final Output target;
///
///
///
/// 72!!! constructors calling each other!
/// 72个构造函数彼此调用
///
///
@Override
public InputStream stream() throws IOException {
return new TeeInputStream(
this.source.stream(), this.target.stream()
);
}
}
这个代码class是加了final的,同时内部有七十二个构建函数!大约有一千行代码,其中90%是构造函数,它们 什么都不做!它有什么样的可维护性?!
抛开情绪
让我们仔细看看这些构造函数。设置类成员的主构造函数是这样的:
public TeeInput(final Input input, final Output output) {
this.source = input;
this.target = output;
}
Input并且Output是来自Cactoos的接口 - 第一个产生InputStream,第二个OutputStream。 TeeInput这些流的行为类似于teeUnix命令:管道输入,将其写入提供的输出。
如果我们现在检查辅助构造函数,我们会注意到它们都是使用文件,URL,输入/输出流等的类的典型用法的快捷方式。这些辅助的构造函数是为了客户端调用者可以直接TeeInput构造函数,避免对象组合深入嵌套过于冗长。
一切顺利,但导致两个问题:
1. 构造函数的数量快速增长
2. 对于TeeInput具有一个Input属性和一个Output属性的类,辅助构造函数的可能详尽计数将是:
Input类型数量N * 个Output类型数量N
其中N是类实现的数量。这个数字一直会增长:
1. 任何新的类属性:将在上面的等式中添加新的N. 像“新属性”这样的东西不太可能发生在具有高度凝聚力和单一反应性的类中,TeeInput但仍然是可能的。
2. 现有属性类型的任何新子类型 - 另一方面,这是高度隔离的接口(如Input和)的典型情况Output。
这两个因素导致构造函数的数量非常快,因此72个构造函数的数量不是终点。
有人可能会反驳说,没有必要定义详尽的构造函数集,这是事实。但是当需要一个构造函数,但是这个构造函数未定义时,它们将面临另一个问题。
这个类Class是无法继承
好的,我们有TeeInput几十个构造函数。现在想象一下,一些开发人员采用Cactoos框架并在他自己的框架上定义了一个新的子类型。让我们假设InputFromUniverse。它在里面实际上做什么并不重要。
class InputFromUniverse {
private final UniverseCoordinates coords;
public InputFromUniverse(UniverseCoordinates coords) {
this.coords = coords;
}
@Override
public InputStream stream() throws IOException {
// some implementation
}
}
开发人员开始用Cactoos不同类组合,并发现当使用InputFromUniverse作为TeeInput的构造入参时,会出现很多时候重复:
...
new TeeInput(
new InputFromUniverse(
new UniverseCoordinates(...)
),
new OutputToFile(
file
)
)
...
在这种情况下,他会有很大的诱惑力来消除重复 - 他所需要的只是增加一个新的辅助构造者TeeInput,将UniverseCoordinates和File作为输入函数......
但是这个Class是final的。它是密封的,如果不修改源代码就不能扩展它。而源代码TeeInput是在Cactoos中,因此对于可怜的开发人员来说唯一的选择就是在他的代码中反复重复这个组合,这不利于可维护性。
那么什么继承是邪恶的呢?
是的, 来自Java的关键字extends总是意味着麻烦吗?
让我们假设TeeInput该类不是final,并且没有任何辅助构造函数:
public class TeeInput implements Input {
private final Input source;
private final Output target;
public TeeInput(final Input input, final Output output) {
this.source = input;
this.target = output;
}
@Override
public final InputStream stream() throws IOException {
return new TeeInputStream(
this.source.stream(), this.target.stream()
);
}
}
这对客户来说不再不方便,但它很短且自给自足。那么如果有人想要扩展这个类?他们可以通过extends来自由地做到这一点:
public class TeeInputFromUriToFile extends TeeInput {
public TeeInputFromUriToFile(final URI uri, final File file) {
super(new InputFromUri(uri), new OutputToFile(file));
}
}
public class TeeInputFromFileToFile extends TeeInput {
public TeeInputFromFileToFile(final File input, final File output) {
super(new InputFromFile(input), new OutputToFile(output));
}
}
public class TeeInputFromUniverseToFile extends TeeInput {
public TeeInputFromUniverseToFile(final UniverseCoordinates universeCoordinates, final File file) {
super(new InputFromUniverse(universeCoordinates), new OutputToFile(file));
}
}
它是实现继承吗?也许。但更重要的问题是:我们可以调用TeeInput继承子类型!
仔细看看。所有这些继承者都有一个有趣的能力。对于使用此类继承者操作的任何组合,用其构造函数的内容替换继承者永远不会改变组合的行为。例如,这两个对象是等价的:
new TeeInputFromUrlToFile(
"http://example.com/somefile",
new File("/tmp/tempFile")
)
new TeeInput(
new InputFromUri(
"http://example.com/somefile"
),
new OutputFromFile(
new File("/tmp/tempFile")
)
)
对于使用它们的任何客户端程序,它们都保证以相同的方式运行。听起来不熟悉吗?该TeeInputFromUrlToFile实际上是子类型TeeInput。
但是,如果有人想覆盖了TeeInput中的stream方法,那么可能很容易被破坏TeeInput。为了防止这种情况,有意地使该方法成为final:
public final InputStream stream() throws IOException {
return new TeeInputStream(
this.source.stream(), this.target.stream()
);
}
。这将强制继承者作为一个整体继承一个类,而不仅仅是它们的一部分。
以这种方式重用类的概念可能看起来类似于装饰模式。我完全赞成这个想法 - 它是安全的面向对象代码重用的非常好用的方法。
那么,为什么“任何优雅对象的类必须是final的”?这条规则不成立。任何类都可以这种方式进行子类型化。设置类为final只是设置无用的人为障碍,使代码难以扩展而不进行修改,违反了OCP。
总结一下:永远不要让“优雅”的class变成final- 这是有害的,没有任何好处。相反 - 不如让方法变成final。