使用instanceof是一种代码味道。 我认为我们可能对此表示同意。 每当我看到这样的构造时,我肯定会出现问题。 也许有人只是在进行更改时没有注意到问题? 也许有一个主意,但是它太复杂了,以至于需要太多的精力或时间才能让开发人员决定不做呢? 也许只是懒惰? 谁知道。 事实仍然是代码演变成这种状态,我们必须与之合作。
或者也许我们可以做些什么? 有什么可以打开我们的扩展代码吗?
今天,我想向您展示如何实现这一目标。 但是首先,让我解释一下为什么这个instanceof根本是个问题。
看一下代码
今天我们将讨论以下代码:
public class ChangeProcessingHandler {public CodeDelta triggerProcessingChangeOf(Code code, Change change) {verifyChangeOf(code, change);if (change instanceof Refactoring) {return processRefactoring(code, (Refactoring) change);} else if (change instanceof Improvement) {return processImprovement(code, (Improvement) change);} else if (change instanceof Growth) {return processGrowth(code, (Growth) change);} else {throw new UnsuportedChangeException();}}// some more code
}
我们将尝试对其进行改进。
我试图使这段代码具有描述性,但是让我简要地总结一下。 根据Change接口实现的特定类型,我们选择一种准确的处理方式。 如果找不到匹配的类型,我们将抛出一个异常。
现在,让我们看一下这段代码有什么问题。
接口及其实现?
当您查看方法的声明时,您能怎么说呢? 确实需要两个输入参数。 它给我们什么样的信息? 我们知道依赖关系,并基于它们的API,我们知道如何在方法主体中与传递的对象进行交互。
在给定的例子中是真的吗? 不幸的是没有。 我们正在传递一个Change实例,并且我们希望方法的主体将取决于其接口。 但是在内部,我们将实例转换为特定类型,这导致依赖性增加。
这本身不是一个好的设计决策,但更糟糕的是–我们在幕后增加了这个数字。 直到您不阅读方法的主体,您都不会知道。
缺乏知识远比依赖数量严重得多。
新类型不是那么容易添加
假设您必须添加Change接口的新实现。 会发生什么? 好吧,什么都没有。 您将添加类定义并对其进行测试。 您将运行所有测试。 如果至少有一个组件或系统测试能够通过新引入的Change接口实现达到所提供的代码并且失败,您将很幸运。
当没有这样的测试时,问题就开始了,您甚至都不知道应该改变某个地方以适应新功能。
一切都会编译,您将一直工作到……
为什么?
您在代码中注意到了这个不错的UnsupportedChangeException吗? 老实说,它在那里只是因为设计错误。
我们拥有它有两个原因:
- 没有它,代码将无法编译。 当然,如果方法为空,我们可以跳过它,但是在我们的示例中,我们必须返回或抛出一些东西。 我们可以用其他方法代替last if-else,但这不是我们想要做的。
- 它使我们无法添加新类型,而忘记在其中添加对新引入功能的支持。 假设至少有一种测试会在这种情况下失败。
为什么我称其为错误的设计? 好吧,使用异常来表示支持新功能的需求是对异常的滥用。 我还相信,如果我们的代码通过不编译来发出信号,那会更好。 这对我来说很有意义,而且肯定会提供更快的反馈。
来访者进行抢救!
访问者允许我们添加其他功能,其实现取决于对象的特定类型。 它允许使用接口的方法。 因此,我们可以避免自己检索有关特定接口的实现的信息。
首先,我们需要使检索有关对象类型的信息成为可能。 为此,我们必须在接口中添加一种方法,该方法将允许我们传递访问者:
public interface Change {void accept(Visitator visitator);
}
实现接口的每个对象的实现非常简单:
public class Refactoring implements Change {@Overridepublic void accept(Visitator visitator) {visitator.visit(this);}// some code
}
通过查看调用方法visit()的行可以观察到什么? 在这里可以检索有关类型的信息。 不需要instanceof,不需要强制转换。 这是我们在更好的设计支持下免费获得的东西。
这时,您可能知道Visitor的界面如下所示:
public interface Visitator {void visit(Refactoring refactoring);void visit(Improvement improvement);void visit(Growth growth);
}
不是那么复杂,不是吗?
之后,我们必须从ChangeProcessingHandler类中提取一些代码到实现Visitor接口的类中:
public class ChangeProcessor implements Visitator {private final Code code;public ChangeProcessor(Code code) {this.code = code;}@Overridepublic void visit(Refactoring refactoring) {// some code}@Overridepublic void visit(Improvement improvement) {// some code}@Overridepublic void visit(Growth growth) {// some code}
}
当然,我们必须在正确的地方使用它:
public class ChangeProcessingHandlerRefactored {public void triggerProcessingChangeOf(Code code, Change change) {verifyChangeOf(code, change);change.accept(new ChangeProcessor(code));}
}
是不是更好?
好的,所以我们更改了原始代码。 现在让我解释一下我们获得了什么。
- 我们刚刚摆脱了一个例外。 不再需要它了,因为对新引入的实现的必要支持将通过非编译代码发出信号。
- 快速反馈是使用接口的结果,该接口将告诉我们要完全支持所有功能还需要实现什么。
- 单一责任原则之所以起作用,是因为“访客”界面的每个特定实现仅负责一个功能。
- 设计是面向行为的(接口),而不是面向实现的(instanceof + cast)。 这样,我们隐藏了实现细节。
- 设计对扩展开放。 引入易于实现的新功能真的很容易,这些功能针对特定对象而有所不同。
它不是那么完美
每个设计都是一个权衡。 您得到了一些东西,但这是有代价的。
我在上段中列出了好处,那么成本呢?
- 这么多的对象
可能有人说这是使用任何设计模式的明显结果,我会说是的。 但是,这并不能改变以下事实:随着对象数量的增加,在对象之间导航变得更加困难。
将所有内容都放在一个对象中可能是一个问题,但名称不正确或杂乱无章的类可能会导致混乱。 - 复杂
所有这些对象都需要一个名称,如果这些对象与域相关,那就太好了。 在这种情况下,我们最终会对我们的应用程序有了更好的了解。 但这并非总是如此。
同样,我们在命名新引入的类时也必须非常小心。 所有这些都必须以不言自明的方式命名。 这并不像某些人想象的那么容易。 - 我的(受限)上下文在哪里?
访客可以帮助解决与示例中出现的问题类似的问题。 但是,如果有很多类似的地方,您必须意识到每个访问者都在某种程度上将对象的行为放入另一个对象。 得墨meter耳定律呢? 那告诉泰勒,不要问吗?
在使用访客解决instanceof问题之前,您应该问自己这个功能是否不是对象本身的一部分? 一些开发人员向我解释这是拥有小对象的一种方式。 好吧,对我而言,这种解释证明了我们应该考虑绑定上下文 。 对象仍然很小,它们的行为也不会泄漏给外部类。
就这样,伙计们
今天就这些。 我希望您发现重新设计的想法很有用,并且在阅读本文之后,您的代码中的异味肯定会受到威胁。 与往常一样,我鼓励您发表评论并分享您的观点和经验。 也许您更多地了解与此类变更相关的收益/问题。
翻译自: https://www.javacodegeeks.com/2016/10/really-need-instanceof.html