22

ぶつかったテクニックについてアドバイスをお願いします。コード スニペットを見れば簡単に理解できますが、次の段落でもう少し詳しく説明します。


「コード サンドイッチ」イディオムの使用は、リソース管理を扱うのに一般的です。C++ の RAII イディオムに慣れていた私は、Java に切り替えたところ、例外セーフなリソース管理の結果、コードが深くネストされていることがわかりました。このコードでは、通常の制御フローを把握するのに非常に苦労しました。

どうやら ( Java データ アクセス: この Java データ アクセス コードのスタイルは良いのか、それとも最後に試行しすぎているのか?Java io の醜い try-finally ブロックなど) 私は一人ではありません。

これに対処するために、さまざまな解決策を試しました。

  1. プログラムの状態を明示的に維持します: resource1aquired, fileopened..., そして条件付きでクリーンアップします: 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

このアプローチは気に入っていますが、いくつかの制限があります。

1 つ目は、オリジナルでは、初期の finally ブロックでのスローが後のブロックに影響を与えないことです。あなたのデモンストレーションでは、アンエクスポート アクションをスローすると、切断補償が停止します。

2 つ目は、Java の無名クラスの醜さによって言語が複雑になっていることです。これには、「最終」変数のヒープを導入してコンペンセータから見えるようにする必要があります。これはあなたのせいではありませんが、治療法は病気よりも悪いのだろうか.

でも全体的にアプローチが好きで、とてもかわいいです。

于 2011-08-31T09:21:59.347 に答える
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 に答える
3

私の見方では、あなたが望むのはトランザクションです。あなたの補償はトランザクションであり、実装が少し異なります。JPA リソースや、トランザクションとロールバックをサポートするその他のリソースを使用していないことを前提としています。JTA (Java Transaction 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 に答える
1

それはいいです。

大きな不満はありませんが、私の頭の上にある小さなことは次のとおりです。

  • 少しのパフォーマンス負担
  • final報酬がそれらを見るためにいくつかのものを作る必要があります。多分これはいくつかのユースケースを防ぎます
  • 補正中に例外をキャッチし、何があっても補正を実行し続ける必要があります
  • (少し大げさですが) プログラミング エラーが原因で、実行時に Compensation キューを誤って空にしてしまう可能性があります。とにかく、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 に答える