现在,每个人都知道使用GOTO运算符不仅不好,而且很糟糕。 关于它的使用的争论结束于20世纪80年代,大多数现代编程语言都将其排除在外。 但是,作为一种真正的邪恶,他成功地伪装成自己,并在21世纪以例外的名义复活。
一方面,在现代编程语言中,异常是一个相当简单的概念。 另一方面,它们经常被错误地使用。 有一个简单而众所周知的规则-例外仅用于处理损坏。 对“故障”概念的解释过于宽松,导致使用GOTO的所有问题。
理论实例
故障和负面业务场景之间的区别可以在登录窗口中以非常简单的用例清楚地看到:
- 用户输入登录名/密码。
- 用户单击“登录”按钮。
- 客户端应用程序将请求发送到服务器。
- 服务器成功检查了用户名/密码(将相应对的存在视为成功)。
- 服务器将信息发送给客户端,表明身份验证已成功,并发送到转换页面的链接。
- 客户端转到指定页面。
还有一个负面的延伸:
4.1。 服务器找不到相应的登录名/密码对,并向客户端发送有关此信息的通知。
考虑到场景4.1是一个“问题”,因此必须使用异常来实现它是一个相当常见的错误。 实际上并非如此。 登录名和密码不匹配是脚本的业务逻辑所提供的标准用户体验的一部分。 我们的商业客户期望这一发展。 因此,这不是故障,您不能在此处使用异常。
故障包括:客户端与北方之间的连接断开,DBMS无法访问,数据库中的方案不正确。 还有超过一百万个原因破坏了我们的应用程序,并且与用户的业务逻辑无关。
在我参与的开发项目中,有一个更复杂的登录逻辑。 通过连续3次输入错误的密码,该用户被暂时阻止了15分钟。 用户连续获得3次临时锁定后,便收到了永久锁定。 根据用户类型,还有其他规则。 异常的实现使得引入新规则变得极为困难。
考虑这个例子会很有趣,但是它太大了,而且不是很直观。 在异常情况下,如何将代码与业务逻辑混淆变得清晰明了,我将在另一个示例中向您展示。
示例加载属性
尝试看一下这段代码,清楚地了解它的作用。 具有相当简单的逻辑的过程并不大。 拥有良好的编程风格,对其本质的理解不应超过2-3分钟(我不记得我完全理解此代码所花的时间,但绝对超过15分钟)。
private WorkspaceProperties(){ Properties loadedProperties = readPropertiesFromFile(WORK_PROPERTIES_PATH, true);
因此,让我们揭示秘密-这里发生了什么。 从两个文件中WORK_PROPERTIES
的属性-必需的WORK_PROPERTIES
和附加的MY_WORK_PROPERTIES
,添加到共享属性存储中。 有一个细微差别-我们不知道特定属性文件的确切位置-它可以位于当前目录和祖先目录中(最多三个级别)。
至少有两件事令人困惑: throwIfNotExists
参数和catch FileNotFoundException
的大逻辑块。 所有这些不透明的暗示-异常用于实现业务逻辑(但是如何在一种情况下解释抛出异常是失败的,而在另一种情况下则不能解释)?
订立正确的合同
首先, throwIfNotExists
处理throwIfNotExists
。 在处理异常时,了解用例方面需要在何处进行处理非常重要。 在这种情况下,很明显readPropertiesFromFile
方法readPropertiesFromFile
无法确定缺少文件的时间是“不好”还是何时不存在。 这样的决定是在调用时做出的。 注释表明,我们决定该文件是否应存在。 但实际上,我们对文件本身而不是文件设置感兴趣。 不幸的是,这并非源于代码。
我们修复了以下两个缺点:
Properties loadedProperties = readPropertiesFromFile(WORK_PROPERTIES_PATH); if (loadedProperties.isEmpty()) { throw new RuntimeException("Can`t load workspace properties"); } loadedProperties = readPropertiesFromFile(MY_WORK_PROPERTIES_PATH); getProperties().putAll( loadedProperties );
现在已经清楚地显示了语义-
必须指定MY_WORK_PROPERTIES
,但不能指定MY_WORK_PROPERTIES
。 另外,在重构时,我注意到readPropertiesFromFile
永远不会返回null
,因此在读取MY_WORK_PROPERTIES
时会利用MY_WORK_PROPERTIES
。
我们检查不间断
先前的重构也影响了实现,但影响不大。 我刚刚删除了throwIfNotExists
处理throwIfNotExists
:
if (throwIfNotExists) throw new RuntimeException(…);
在仔细研究了实现之后,我们开始理解代码作者搜索文件的逻辑。 首先,检查文件是否在当前目录中,如果找不到,则在更高级别进行检查等。 即 显然,该算法提供了文件的缺失。 在这种情况下,检查是使用异常完成的。 即 违反了该原则-异常不是被视为“某些东西已损坏”,而是被视为业务逻辑的一部分。
有一个用于检查文件可用性以读取File.canRead()
。 使用它,您可以在catch
摆脱业务逻辑catch
try{ File file = new File(relativePath + filepath); is = new FileInputStream(file); isr = new InputStreamReader( is, "UTF-8"); loadedProperties.load(isr); loadingTryLeft = 0; } catch( FileNotFoundException e) { loadingTryLeft -= 1; if (loadingTryLeft > 0) relativePath += "../"; else throw e; } finally { if (is != null) is.close(); if (isr != null) isr.close(); } }
更改代码,我们得到以下信息:
private Properties readPropertiesFromFile(String filepath) { Properties loadedProperties = new Properties(); System.out.println("Try loading workspace properties" + filepath); try { int loadingTryLeft = 3; String relativePath = ""; while (loadingTryLeft > 0) { File file = new File(relativePath + filepath); if (file.canRead()) { InputStream is = null; InputStreamReader isr = null; try { is = new FileInputStream(file); isr = new InputStreamReader(is, "UTF-8"); loadedProperties.load(isr); loadingTryLeft = 0; } finally { if (is != null) is.close(); if (isr != null) isr.close(); } } else { loadingTryLeft -= 1; if (loadingTryLeft > 0) { relativePath += "../"; } else { throw new FileNotFoundException(); } } } System.out.println("Found file " + filepath); } catch (FileNotFoundException e) { System.out.println("File not found " + filepath); } catch (IOException e) { throw new RuntimeException("Can`t read " + filepath, e); } return loadedProperties; }
我还将变量( is
, isr
)的级别降低到了允许的最小值。
这种简单的重构极大地提高了代码的可读性。 代码直接显示算法(如果文件存在,那么我们将读取它,否则我们将减少尝试次数并查看上面的目录)。
显示GOTO
如果未找到文件,请详细考虑在某种情况下会发生什么情况:
} else { loadingTryLeft -= 1; if (loadingTryLeft > 0) { relativePath += "../"; } else { throw new FileNotFoundException(); } }
可以看出,这里的异常用于中断执行周期并实际执行GOTO功能。
对于怀疑者,我们将进行另一种更改。 与其以loadingTryLeft = 0
的形式使用小拐杖(拐杖,因为实际上成功的尝试不会loadingTryLeft = 0
剩余的尝试次数),我们明确指出读取文件会导致函数退出(不要忘记编写消息):
try { is = new FileInputStream(file); isr = new InputStreamReader(is, "UTF-8"); loadedProperties.load(isr); System.out.println("Found file " + filepath); return loadedProperties; } finally {
这使我们可以将while (loadingTryLeft > 0)
条件while (loadingTryLeft > 0)
替换为while(true)
:
try { int loadingTryLeft = 3; String relativePath = ""; while (true) { File file = new File(relativePath + filepath); if (file.canRead()) { InputStream is = null; InputStreamReader isr = null; try { is = new FileInputStream(file); isr = new InputStreamReader(is, "UTF-8"); loadedProperties.load(isr); System.out.println("Found file " + filepath); return loadedProperties; } finally { if (is != null) is.close(); if (isr != null) isr.close(); } } else { loadingTryLeft -= 1; if (loadingTryLeft > 0) { relativePath += "../"; } else { throw new FileNotFoundException();
为了摆脱明显的臭味, throw new FileNotFoundException
,您需要记住函数协定。 无论如何,该函数都会返回一组属性,如果他们无法读取文件,我们将其返回为空。 因此,没有理由引发异常并捕获它。 通常的while (loadingTryLeft > 0)
条件while (loadingTryLeft > 0)
足够了:
private Properties readPropertiesFromFile(String filepath) { Properties loadedProperties = new Properties(); System.out.println("Try loading workspace properties" + filepath); try { int loadingTryLeft = 3; String relativePath = ""; while (loadingTryLeft > 0) { File file = new File(relativePath + filepath); if (file.canRead()) { InputStream is = null; InputStreamReader isr = null; try { is = new FileInputStream(file); isr = new InputStreamReader(is, "UTF-8"); loadedProperties.load(isr); System.out.println("Found file " + filepath); return loadedProperties; } finally { if (is != null) is.close(); if (isr != null) isr.close(); } } else { loadingTryLeft -= 1; if (loadingTryLeft > 0) relativePath += "../"; } } System.out.println("file not found"); } catch (IOException e) { throw new RuntimeException("Can`t read " + filepath, e); } return loadedProperties; }
原则上,从正确的工作观点出发(例外情况),一切都在这里。 如果出现IOException问题,是否有必要引发RuntimeException值得怀疑,但为了兼容性,我们还是将其保留。
带来光泽
还剩下一些小事情,我们可以使代码更加灵活和易于理解:
- readPropertiesFromFile方法名称公开了其实现(顺便说一句,并抛出FileNotFoundException)。 最好称其为中性和简洁-loadProperties(...)
- 该方法同时搜索和读取。 对我来说,这是两种不同的职责,可以用不同的方法来划分。
- 该代码最初是在Java 6下编写的,但现在在Java 7中使用。这允许使用可关闭的资源。
- 从经验中我知道,当显示有关已找到或未找到文件的信息时,最好使用文件的完整路径,而不是相对路径。
if (loadingTryLeft > 0) relativePath += "../";
-如果您仔细查看代码,则可以看到-此检查是不必要的,因为 如果搜索限制已用尽,则无论如何都不会使用新值。 如果代码中有多余的东西,那么这是应该删除的垃圾。
最终版本的源代码:
private WorkspaceProperties() { super(new Properties()); if (defaultInstance != null) throw new IllegalStateException(); Properties loadedProperties = readPropertiesFromFile(WORK_PROPERTIES_PATH); if (loadedProperties.isEmpty()) { throw new RuntimeException("Can`t load workspace properties"); } getProperties().putAll(loadedProperties); loadedProperties = readPropertiesFromFile(MY_WORK_PROPERTIES_PATH); getProperties().putAll(loadedProperties); System.out.println("Loaded properties:" + getProperties()); } private Properties readPropertiesFromFile(String filepath) { System.out.println("Try loading workspace properties" + filepath); try { int loadingTryLeft = 3; String relativePath = ""; while (loadingTryLeft > 0) { File file = new File(relativePath + filepath); if (file.canRead()) { return read(file); } else { relativePath += "../"; loadingTryLeft -= 1; } } System.out.println("file not found"); } catch (IOException e) { throw new RuntimeException("Can`t read " + filepath, e); } return new Properties(); } private Properties read(File file) throws IOException { try (InputStream is = new FileInputStream(file); InputStreamReader isr = new InputStreamReader(is, "UTF-8")) { Properties loadedProperties = new Properties(); loadedProperties.load(isr); System.out.println("Found file " + file.getAbsolutePath()); return loadedProperties; } }
总结
解析的示例清楚地说明了源代码的粗心处理。 没有使用异常来处理故障,而是决定使用它来实现业务逻辑。 这立即导致其支持的复杂性,这反映在其为满足新要求而进行的进一步开发中,从而偏离了结构编程的原则。 使用简单的规则(仅针对故障的例外)将帮助您避免重返GOTO时代,并使代码保持整洁,可理解和可扩展。