「実行」イディオムとは何ですか?


151

私が聞いていたこの「実行アラウンド」イディオム(または同様のもの)とは何ですか?なぜそれを使用し、なぜ使用したくないのですか?


9
気づかなかった、タック。そうでなければ、私の答えはもっと皮肉だったかもしれません;)
Jon Skeet

1
これは基本的には正しい側面ですか?そうでない場合、どのように違いますか?
Lucas

回答:


147

基本的には、リソースの割り当てやクリーンアップなど、常に必要なことを行うメソッドを記述し、呼び出し側に「リソースで実行したいこと」を渡すパターンです。例えば:

public interface InputStreamAction
{
    void useStream(InputStream stream) throws IOException;
}

// Somewhere else    

public void executeWithFile(String filename, InputStreamAction action)
    throws IOException
{
    InputStream stream = new FileInputStream(filename);
    try {
        action.useStream(stream);
    } finally {
        stream.close();
    }
}

// Calling it
executeWithFile("filename.txt", new InputStreamAction()
{
    public void useStream(InputStream stream) throws IOException
    {
        // Code to use the stream goes here
    }
});

// Calling it with Java 8 Lambda Expression:
executeWithFile("filename.txt", s -> System.out.println(s.read()));

// Or with Java 8 Method reference:
executeWithFile("filename.txt", ClassName::methodName);

呼び出し側のコードは、オープン/クリーンアップ側を気にする必要はありません-によって処理されexecuteWithFileます。

クロージャは冗長なので、8つのラムダ式は、他の多くの言語(例えば、C#ラムダ式、またはグルービー)のように実装することができ、この特殊なケースを使用してJava 7が処理されたJavaで開始したので、これは、Javaで率直に苦痛だったtry-with-resourcesAutoClosableストリーム。

「割り当てとクリーンアップ」は典型的な例ですが、トランザクション処理、ロギング、より多くの特権でのコードの実行など、他にも多くの例が考えられます。基本的に、テンプレートメソッドパターンに少し似ていますが、継承はありません。


4
それは決定論的です。Javaのファイナライザは確定的に呼び出されません。また、前の段落で述べたように、リソースの割り当てとクリーンアップだけに使用されるわけではありません。新しいオブジェクトを作成する必要がない場合もあります。通常は「初期化と破棄」ですが、リソース割り当てではない場合があります。
Jon Skeet

3
Cのように、関数ポインターを渡していくつかの作業を行う関数があるのでしょうか。
ポールトンブリン

3
また、ジョン、あなたはJavaのクロージャーを参照しています-それはまだありません(私がそれを逃したのでない限り)。あなたが説明するのは匿名の内部クラスです-これはまったく同じではありません。真のクロージャーのサポート(提案されているように-私のブログを参照)は、その構文をかなり単純化します。
philsquared 2009

8
@フィル:程度の問題だと思います。Javaの匿名内部クラスは、限られた意味で周囲の環境アクセスできます。つまり、「完全な」クロージャーではないものの、「制限された」クロージャーです。チェックされていますが、Javaで適切なクロージャーが確実に見られるようにしたい(続き)
Jon Skeet

4
Java 7はtry-with-resourceを追加し、Java 8はラムダを追加しました。私はこれが古い質問/回答であることを知っていますが、5年半後にこの質問を見ている人に指摘したいと思います。これらの言語ツールはどちらも、このパターンを修正するために考案された問題の解決に役立ちます。

45

Execute Aroundイディオムは、次のようなことをしなければならない場合に使用されます。

//... chunk of init/preparation code ...
task A
//... chunk of cleanup/finishing code ...

//... chunk of identical init/preparation code ...
task B
//... chunk of identical cleanup/finishing code ...

//... chunk of identical init/preparation code ...
task C
//... chunk of identical cleanup/finishing code ...

//... and so on.

常に実際のタスクの「周囲」で実行されるこの冗長なコードをすべて繰り返さないようにするには、自動的に処理するクラスを作成します。

//pseudo-code:
class DoTask()
{
    do(task T)
    {
        // .. chunk of prep code
        // execute task T
        // .. chunk of cleanup code
    }
};

DoTask.do(task A)
DoTask.do(task B)
DoTask.do(task C)

このイディオムは、複雑な冗長コードをすべて1か所に移動し、メインプログラムをはるかに読みやすく(そして保守可能にします!)

を見てみましょう 。このポストを C#の例のために、そしてこの記事 C ++たとえば。


7

メソッドの周りの実行、セットアップおよび/またはティアダウンコードを実行するとの間でコードを実行することもできる方法、に任意のコードを渡すところです。

Javaは、これを実行するために選択した言語ではありません。引数としてクロージャー(またはラムダ式)を渡す方がよりスタイリッシュです。オブジェクトは間違いなくクロージャ同等ですが

実行方法は制御の反転に似ているように思えます、メソッドを呼び出すたびにアドホックに変更できる(依存性注入)です。

しかし、これは制御結合の例として解釈することもできます(この場合、文字どおり、引数によって何をするかをメソッドに伝えます)。


7

ここにはJavaタグがあるので、パターンはプラットフォーム固有ではありませんが、例としてJavaを使用します。

アイデアは、コードを実行する前と実行した後、常に同じボイラープレートを含むコードがあるということです。良い例がJDBCです。実際のクエリを実行して結果セットを処理する前に、常に接続を取得してステートメント(または準備済みステートメント)を作成し、最後に常に同じボイラープレートクリーンアップを実行して、ステートメントと接続を閉じます。

execute-aroundのアイデアは、ボイラープレートコードを除外できるとよいということです。入力の手間が省けますが、理由はもっと深いです。これは、Don't-Repeat-Yourself(DRY)の原則です。コードを1つの場所に分離するので、バグがある場合、またはコードを変更する必要がある場合、または単に理解したい場合は、すべて1か所にあります。

ただし、この種の因数分解で少しトリッキーなのは、「前」と「後」の両方の部分を参照する必要がある参照があることです。JDBCの例では、これにはConnectionおよび(Prepared)Statementが含まれます。したがって、これを処理するには、基本的にボイラープレートコードでターゲットコードを「ラップ」します。

Javaのいくつかの一般的なケースに精通している場合があります。1つはサーブレットフィルタです。もう1つは、AOPに関するアドバイスです。3番目は、SpringのさまざまなxxxTemplateクラスです。いずれの場合も、「興味深い」コード(JDBCクエリや結果セットの処理など)が挿入されるラッパーオブジェクトがあります。ラッパーオブジェクトは「前」の部分を実行し、対象のコードを呼び出してから、「後」の部分を実行します。


7

多くのプログラミング言語にわたってこの構造を調査し、いくつかの興味深い研究アイデアを提供するコードサンドイッチも参照してください。なぜそれを使うのかという特定の質問に関して、上記の論文はいくつかの具体的な例を提供しています:

このような状況は、プログラムが共有リソースを操作するたびに発生します。ロック、ソケット、ファイル、またはデータベース接続用のAPIでは、プログラムが以前に取得したリソースを明示的にクローズまたは解放する必要がある場合があります。ガベージコレクションのない言語では、プログラマは使用前にメモリを割り当て、使用後に解放する必要があります。一般に、さまざまなプログラミングタスクでは、プログラムが変更を行い、その変更のコンテキストで動作し、変更を元に戻す必要があります。このような状況をコードサンドイッチと呼びます。

以降:

コードサンドイッチは、多くのプログラミング状況で表示されます。いくつかの一般的な例は、ロック、ファイル記述子、ソケット接続などの希少なリソースの取得と解放に関連しています。より一般的なケースでは、プログラムの状態が一時的に変更されると、コードサンドイッチが必要になる場合があります。たとえば、GUIベースのプログラムがユーザー入力を一時的に無視したり、OSカーネルがハードウェア割り込みを一時的に無効にしたりすることがあります。これらのケースで以前の状態を復元できないと、重大なバグが発生します。

なぜ紙が探求しないではない、このイディオムを使用することではなく、イディオムは言語レベルの助けを借りずに誤解しやすいですなぜそれが記載されていません:

欠陥のあるコードサンドイッチは、例外とそれに関連する目に見えない制御フローが存在する場合に最も頻繁に発生します。実際、コードサンドイッチを管理するための特別な言語機能は、主に例外をサポートする言語で発生します。

ただし、コードサンドイッチの欠陥の原因は例外だけではありません。本体コードに変更が加えられるたびに、後のコードをバイパスする新しい制御パスが発生する可能性があります。最も単純なケースでは、メンテナーreturnはサンドイッチの本体にステートメントを追加するだけで新しい欠陥を導入し、それが原因でサイレントエラーが発生する可能性があります。場合ボディ コードが大きくなる後の広く分離され、このようなミスは、視覚的に検出することは困難であることができます。


良い点は、azurefragです。私は自分の回答を修正および拡張して、実際にはそれだけで自己完結型の回答になるようにしました。これを提案してくれてありがとう。
Ben Liblit 14

4

4歳の人と同じように、説明しようと思います。

例1

サンタが町にやってくる。彼のエルフは背中の後ろに何でも欲しいものをコーディングします、そして彼らが変更しない限り少し反復します:

  1. 包装紙を入手
  2. スーパーニンテンドーを入手してください。
  3. 包んでください。

またはこれ:

  1. 包装紙を入手
  2. バービー人形を入手してください。
  3. 包んでください。

.... ad nauseam、100万回、100万の異なるプレゼント:ステップ2のみが異なることに注意してください。ステップ2だけが異なる場合、Santaがコードを複製するのはなぜですか。 1と3 100万回?100万のプレゼントは、彼が不必要にステップ1と3を100万回繰り返していることを意味します。

周りを実行すると、その問題の解決に役立ちます。コードを排除するのに役立ちます。ステップ1と3は基本的に一定であり、ステップ2が変更される唯一の部分になることができます。

例2

それでもうまくいかない場合は、次の例をご覧ください。sandwhichを考えてみます。外側のパンは常に同じですが、内側にあるものは、選択する砂の種類(例:ハム、チーズ、ジャム、ピーナッツバターなど)。パンは常に外側にあり、作成する砂の種類ごとに10億回繰り返す必要はありません。

さて、上記の説明を読めば理解しやすいでしょう。この説明がお役に立てば幸いです。


+想像力のため:D
卿。ハリネズミ

3

これは私に戦略設計パターンを思い出させます。私がポイントしたリンクには、パターンのJavaコードが含まれていることに注意してください。

明らかに、初期化とクリーンアップのコードを作成し、戦略を渡すだけで「Execute Around」を実行できます。これは、常に初期化とクリーンアップのコードにラップされます。

コードの繰り返しを減らすために使用される任意の手法と同様に、少なくとも2つのケースが必要になるまで、おそらく3つ(YAGNIの原則に従って)になるまで、それを使用しないでください。コードの繰り返しを削除するとメンテナンスが減ります(コードのコピーが少ないということは、各コピー間で修正をコピーするのにかかる時間が少なくなる)だけでなく、メンテナンスも増えます(コードの総数が増える)ことに注意してください。したがって、このトリックの代償は、コードを追加することです。

このタイプの手法は、初期化とクリーンアップ以外にも便利です。また、関数を簡単に呼び出せるようにする場合にも役立ちます(たとえば、ウィザードで使用して、「次へ」ボタンと「前へ」ボタンに、何をすべきかを決定するための巨大なケースステートメントを必要としないようにすることができます。次/前のページ。


0

グルーヴィーなイディオムが必要な場合は、次のとおりです。

//-- the target class
class Resource { 
    def open () { // sensitive operation }
    def close () { // sensitive operation }
    //-- target method
    def doWork() { println "working";} }

//-- the execute around code
def static use (closure) {
    def res = new Resource();
    try { 
        res.open();
        closure(res)
    } finally {
        res.close();
    }
}

//-- using the code
Resource.use { res -> res.doWork(); }

オープンが失敗した場合(リエントラントロックを取得するなど)、クローズが呼び出されます(一致するオープンが失敗したにもかかわらず、リエントラントロックを解放するなど)。
トムホーティン-タックライン
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.