关于嵌套Java尝试/最后代码三明治的建议
我想就我碰到的技术提出一些建议。通过查看代码片段可以很容易理解,但我在下面的段落中进一步记录了它。关于嵌套Java尝试/最后代码三明治的建议
使用“代码三明治”成语在处理资源管理中是司空见惯的。习惯了C++的RAII习惯用法,我切换到了Java,发现我的异常安全的资源管理导致了深度嵌套的代码,在这个代码中,我很难掌握控制流程。
显然(java data access: is this good style of java data access code, or is it too much try finally?,Java io ugly try-finally block和更多)我并不孤单。
我尝试了不同的解决方案来应对这样的:
明确维护程序状态:
resource1aquired
,fileopened
...,和清理条件:if (resource1acquired) resource1.cleanup()
......但我在顺明确复制程序状态变量 - 运行时知道状态,我不想关心它。包裹在每一个功能嵌套块 - 结果更难遵循控制流程,并为真正尴尬的函数名称:
runResource1Acquired(r1)
,runFileOpened(r1, file)
,...
最后我来到了一个成语
取而代之的是:
也(概念)的一些 research paper on code sandwiches支持0使用助手构造,您可能会得到一个更线性的构造,其中补偿代码紧挨着始发者。
class Compensation {
public void compensate(){};
}
compensations = new Stack<Compensation>();
和嵌套代码变为线性:
try {
connection = DBusConnection.SessionBus(); // may throw, needs cleanup
compensations.push(new Compensation(){ public void compensate() {
connection.disconnect();
});
connection.export("/MyObject", myObject); // may throw, needs cleanup
compensations.push(new Compensation(){ public void compensate() {
connection.unExport("/MyObject");
});
// unfolded try{}finally{} code
} finally {
while(!compensations.empty())
compensations.pop().compensate();
}
我很高兴:无论多少异常路径,控制流程保持线性的,并且清理代码是目视下向发端的代码。最重要的是,它不需要人为限制的closeQuietly
方法,这使得它更加灵活(即不仅仅是Closeable
对象,还有Disconnectable
,Rollbackable
以及其他任何其他)。
但是...
我在其他地方没有发现这种技术。所以这里的问题是:
该技术是否有效?你看到了什么错误?
非常感谢。
很好。
无重大投诉,小事把我的头顶部:
- 一点点的性能负担
- 你需要做一些事情
final
的补偿看到他们。也许这可以防止一些使用案例 - 您应该在补偿期间捕获异常并继续运行补偿无论如何
- (位牵强),由于编程错误,您可能会在运行时意外清空补偿队列。无论如何,OTOH编程错误都可能发生。并与您的薪酬队列,你会得到“有条件的终极块”。
- 不要推这太多。在一个单一的方法中,它似乎没问题(但是你可能不需要太多的try/finally块),但是不要在补偿队列上下调用堆栈。
- 它延迟对“最外层最终”的补偿,这可能是需要尽早清理的问题
- 只有当您需要仅用于finally块的try块时才有意义。如果你有一个catch块,你可以直接在那里添加最后一个。
你知不知道你真的需要这个复杂性。当您尝试取消导出未导出的内容时会发生什么?
// one try finally to rule them all.
try {
connection = DBusConnection.SessionBus(); // may throw, needs cleanup
connection.export("/MyObject", myObject); // may throw, needs cleanup
// more things which could throw an exception
} finally {
// unwind more things which could have thrown an exception
try { connection.unExport("/MyObject"); } catch(Exception ignored) { }
if (connection != null) connection.disconnect();
}
使用的辅助方法,你可以做
unExport(connection, "/MyObject");
disconnect(connection);
我还以为断开意味着你不需要不导出连接正在使用的资源。
我喜欢这个方法,但是看到了一些限制。
第一个是在原始的情况下,在最初的finally块中抛出不会影响后面的块。在您的演示中,抛出未执行的动作将会停止断开连接补偿的发生。
其次,它的语言由于Java的匿名类的丑陋而变得复杂,包括需要引入一堆'final'变量,以便它们可以被补偿者看到。这不是你的错,但我想知道治疗是否比疾病更糟糕。
但总的来说,我喜欢这个方法,它很可爱。
我看到它的方式,你想要的是交易。您的补偿是交易,实施有点不同。我假设你没有使用JPA资源或支持事务和回滚的任何其他资源,因为只需使用JTA(Java事务API)就相当容易。另外,我认为你的资源不是由你开发的,因为你可以让他们从JTA实现正确的接口并使用它们进行交易。
所以,我喜欢你的方法,但是我会做的是隐藏从客户端弹出和补偿的复杂性。此外,您可以透明地传递事务。
因此(注意,提前难看代码):
public class Transaction {
private Stack<Compensation> compensations = new Stack<Compensation>();
public Transaction addCompensation(Compensation compensation) {
this.compensations.add(compensation);
}
public void rollback() {
while(!compensations.empty())
compensations.pop().compensate();
}
}
...丑陋的代码?我可以做更丑陋的:)这至少是一种很好的方式,可以将这种技术包装到可重用的类中。 – xtofl
我在这里错过的唯一的事情就是try-catch为'compensations.pop()。compensate()处理wrap',以处理在前面几个答案中指出的异常 – gnat
好吧,我也想念getter和setter,a适当的构造函数,空参数检查等 - 对我来说非常难看。 :)但是,是的,gnat,回滚代码应该更加详细,比如检查无效或无功能补偿等。如果赔偿失败,你想要做什么完全是另一回事:你是否继续?补偿是否相互依赖?抛出一个异常(我的最爱,因为你的应用程序状态在这种情况下是未定义的)?你的要求可能会告诉你该怎么做,所以把自己击倒! :) – LeChe
像的Java设备A的析构函数,这将在词法作用域的端部被调用,是一个有趣的话题;最好在语言层面解决,但语言czars并不觉得它非常有吸引力。
已经讨论过施工行为之后立即指定破坏行为(太阳下没有新东西)。一个例子是http://projectlombok.org/features/Cleanup.html
另一个例子,从私下讨论:
{
FileReader reader = new FileReader(source);
finally: reader.close(); // any statement
reader.read();
}
这是通过转化
{
A
finally:
F
B
}
到
{
A
try
{
B
}
finally
{
F
}
}
如果你的Java 8将关闭,我们可以在关闭中简洁地实现此功能:
auto_scope
#{
A;
on_exit #{ F; }
B;
}
然而,关闭,大多数资源库将提供自己的自动净化设备,客户并不需要自己来处理它
File.open(fileName) #{
read...
}; // auto close
lombok ...看起来像一些std :: boost for java :)谢谢你的提示! – xtofl
你似乎没有被处理的情况下一个可以从一个compensation()方法中抛出异常。这将阻止后续的补偿运行。 –
@Kevin:确实 - 这里有太多的C++习惯用法:析构函数不能抛出,我坚持这个习惯用法。 “补偿”实现可以处理异常。 – xtofl
@xtofl - 与你使用'try-finally'完全不一样 - 你可以在代码本身采用这个习惯用法,而不用为'finally'块打扰。此外,许多'Compensators'自然会抛出异常(例如,用于关闭数据库资源的'SQLException');每个实现**都有**来单独捕获和吞下所有异常,而不是你的'finally'块在一个地方一致地处理它。这对他们来说不是可选的(运行时异常会违反他们的规范,并且可能随时发生),所以您只需鼓励复制粘贴。 –