关于嵌套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和更多)我并不孤单。

我尝试了不同的解决方案来应对这样的:

  1. 明确维护程序状态:resource1aquiredfileopened ...,和清理条件:if (resource1acquired) resource1.cleanup() ......但我在顺明确复制程序状态变量 - 运行时知道状态,我不想关心它。

  2. 包裹在每一个功能嵌套块 - 结果更难遵循控制流程,并为真正尴尬的函数名称: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以及其他任何其他)。

但是...

我在其他地方没有发现这种技术。所以这里的问题是:


该技术是否有效?你看到了什么错误?

非常感谢。

+1

你似乎没有被处理的情况下一个可以从一个compensation()方法中抛出异常。这将阻止后续的补偿运行。 –

+0

@Kevin:确实 - 这里有太多的C++习惯用法:析构函数不能抛出,我坚持这个习惯用法。 “补偿”实现可以处理异常。 – xtofl

+1

@xtofl - 与你使用'try-finally'完全不一样 - 你可以在代码本身采用这个习惯用法,而不用为'finally'块打扰。此外,许多'Compensators'自然会抛出异常(例如,用于关闭数据库资源的'SQLException');每个实现**都有**来单独捕获和吞下所有异常,而不是你的'finally'块在一个地方一致地处理它。这对他们来说不是可选的(运行时异常会违反他们的规范,并且可能随时发生),所以您只需鼓励复制粘贴。 –

很好。

无重大投诉,小事把我的头顶部:

  • 一点点的性能负担
  • 你需要做一些事情final的补偿看到他们。也许这可以防止一些使用案例
  • 您应该在补偿期间捕获异常并继续运行补偿无论如何
  • (位牵强),由于编程错误,您可能会在运行时意外清空补偿队列。无论如何,OTOH编程错误都可能发生。并与您的薪酬队列,你会得到“有条件的终极块”。
  • 不要推这太多。在一个单一的方法中,它似乎没问题(但是你可能不需要太多的try/finally块),但是不要在补偿队列上下调用堆栈。
  • 它延迟对“最外层最终”的补偿,这可能是需要尽早清理的问题
  • 只有当您需要仅用于finally块的try块时才有意义。如果你有一个catch块,你可以直接在那里添加最后一个。
+0

谢谢。你是什​​么意思“有条件的最终块”? – xtofl

+0

那么,你可以说'如果(某事)componensation.push(extraCompensation)'。 – Thilo

你知不知道你真的需要这个复杂性。当您尝试取消导出未导出的内容时会发生什么?

// 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); 

我还以为断开意味着你不需要不导出连接正在使用的资源。

+1

然后他可能会运行unExport,即使导出没有运行。不在上面的简单例子中,但你明白了。 (可以由许多布尔值处理)。 – Thilo

+0

事实上:这个解决方案只有在增加的复杂性真正增加了代码可读性的情况下才有用。也许这个例子不够特别:)。 – xtofl

+0

@Thilo,“然后他可能会运行unExport,即使导出没有运行。”你知道这甚至是一个问题吗? –

我喜欢这个方法,但是看到了一些限制。

第一个是在原始的情况下,在最初的finally块中抛出不会影响后面的块。在您的演示中,抛出未执行的动作将会停止断开连接补偿的发生。

其次,它的语言由于Java的匿名类的丑陋而变得复杂,包括需要引入一堆'final'变量,以便它们可以被补偿者看到。这不是你的错,但我想知道治疗是否比疾病更糟糕。

但总的来说,我喜欢这个方法,它很可爱。

您应该研究一下首次在Java 7中引入的试用资源。这应该会减少必要的嵌套。

+1

我做到了。他们将资源限制为“可关闭”的实现者,这是非常严格的imho。 (而且我还没有使用Java 7,但这是一个小问题:) – xtofl

+0

你可能会在DBus的bug跟踪器中提交一个bug。 Imho没有理由说这个类没有实现Closeable。 – soc

我看到它的方式,你想要的是交易。您的补偿是交易,实施有点不同。我假设你没有使用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(); 
    } 
} 
+0

...丑陋的代码?我可以做更丑陋的:)这至少是一种很好的方式,可以将这种技术包装到可重用的类中。 – xtofl

+1

我在这里错过的唯一的事情就是try-catch为'compensations.pop()。compensate()处理wrap',以处理在前面几个答案中指出的异常 – gnat

+1

好吧,我也想念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 
+0

lombok ...看起来像一些std :: boost for java :)谢谢你的提示! – xtofl