22

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


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

显然( Java 数据访问:这种 Java 数据访问代码的风格很好,还是 try finally 太多了?Java io 丑陋的 try-finally 块等等)我并不孤单。

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

  1. 显式维护程序状态:resource1aquiredfileopened...,并有条件地清理:if (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();
}

我很高兴:无论有多少异常路径,控制流都保持线性,并且清理代码在视觉上位于原始代码旁边。最重要的是,它不需要人为限制的closeQuietly方法,这使得它更加灵活(即不仅是Closeable对象,还有DisconnectableRollbackable以及其他任何东西)。

但...

我发现其他地方没有提到这种技术。那么问题来了:


这种技术有效吗?你在其中看到了什么错误?

非常感谢。

4

6 回答 6

4

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

第一个是在原始代码中,早期的 finally 块中的 throw 不会影响后面的块。在您的演示中,抛出 unexport 操作将停止断开补偿的发生。

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

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

于 2011-08-31T09:21:59.347 回答
3

在我看来,你想要的是交易。您的补偿是交易,实施方式略有不同。我假设您没有使用 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();
   }
}
于 2011-08-31T09:37:48.830 回答
3

一个类似于 Java 的析构函数,将在词法作用域的末尾被调用,这是一个有趣的话题。最好在语言层面上解决它,但是语言沙皇并不觉得它很有说服力。

在讨论了施工行动之后立即指定破坏行动(在阳光下没有新鲜事)。一个例子是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
于 2011-08-31T18:57:40.253 回答
1

很好。

没有大的抱怨,我头顶的小事:

  • 一点性能负担
  • 你需要做一些东西final让补偿看到它们。也许这可以防止一些用例
  • 无论如何,您都应该在补偿期间捕获异常并继续运行补偿
  • (有点牵强)由于编程错误,您可能会在运行时意外清空补偿队列。OTOH 编程错误无论如何都会发生。使用您的补偿队列,您将获得“有条件的最终块”。
  • 不要把这推得太远。在单个方法中似乎没问题(但无论如何你可能不需要太多的 try/finally 块),但不要在调用堆栈上下传递补偿队列。
  • 它延迟了对“最外层”的补偿,这对于需要尽早清理的东西可能是个问题
  • 仅当您仅在 finally 块中需要 try 块时才有意义。如果你有一个 catch 块,你可能只是在那里添加 finally。
于 2011-08-31T09:19:46.647 回答
0

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

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

我原以为断开连接意味着您不需要取消导出连接正在使用的资源。

于 2011-08-31T09:21:59.117 回答
0

您应该研究一下在 Java 7 中首次引入的 try-with-resources。这应该可以减少必要的嵌套。

于 2011-08-31T09:33:19.293 回答