关于嵌套Java尝试/最终代码三明治的建议

2022-09-02 01:59:36

我想要一些关于我碰到的技术的建议。通过查看代码片段可以很容易地理解它,但我在下面的段落中记录了它。


使用“代码三明治”成语来处理资源管理是司空见惯的。习惯了C++的 RAII 习语,我切换到 Java,发现我的异常安全资源管理导致了深度嵌套的代码,在这种代码中,我很难掌握常规的控制流。

显然(java数据访问:这是Java数据访问代码的好风格,还是最后尝试太多了?Java io丑陋的尝试-最终阻止等等)我并不孤单。

我尝试了不同的解决方案来解决这个问题:

  1. 显式维护程序状态:、...,并有条件地清理:...但是我避免在显式变量中复制程序状态 - 运行时知道状态,我不想关心它。resource1aquiredfileopenedif (resource1acquired) resource1.cleanup()

  2. 将每个嵌套块包装在函数中 - 导致更难遵循控制流,并使函数名称非常尴尬:,,...runResource1Acquired( r1 )runFileOpened( r1, file )

最后,我得出了一个成语,这个成语也(概念上)得到了一些关于代码三明治的研究论文的支持


取而代之的是:

// (pseudocode)
try {
   connection = DBusConnection.SessionBus(); // may throw, needs cleanup
   try {
        exported = false;
        connection.export("/MyObject", myObject ); // may throw, needs cleanup
        exported = true;
            //... more try{}finally{} nested blocks
    } finally {
        if( exported ) connection.unExport( "/MyObject" );
    }   
} finally {
   if (connection != null ) connection.disconnect();
}

使用帮助器构造,您可以得到一个更加线性的构造,其中补偿代码紧挨着发起方。

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();
}

我很高兴:无论有多少例外路径,控制流都保持线性,清理代码在视觉上紧挨着原始代码。最重要的是,它不需要人为限制的方法,这使得它更加灵活(即不仅是对象,而且是,以及其他任何方法)。closeQuietlyCloseableDisconnectableRollbackable

但。。。

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


此技术是否有效?你在其中看到了什么错误?

多谢。


答案 1

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

首先,在原版中,在早期最终块中的投掷不会影响后面的块。在您的演示中,未导出操作中的抛出将停止断开连接补偿的发生。

其次,Java的匿名类的丑陋使语言变得复杂,包括需要引入一堆“最终”变量,以便补偿器可以看到它们。这不是你的错,但我想知道治愈是否比疾病更糟糕。

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


答案 2

在我看来,你想要的是交易。您的补偿是交易,实现方式略有不同。我假设您没有使用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();
   }
}

推荐