私が聞いてきたこの「Execute Around」イディオム(または同様のもの)とは何ですか?なぜそれを使用するのか、なぜ使用したくないのか?
基本的には、常に必要なことを行うメソッドを記述するパターンです。リソースの割り当てとクリーンアップを行い、呼び出し元に「リソースを使用してやりたいこと」を渡します。例えば:
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
によって処理されます。
これは、Javaでクロージャーが非常に冗長だったため、Java 8ラムダ式は他の多くの言語と同様に実装できます(C#ラムダ式、またはGroovy)、そしてこの特別なケースはJava 7 with try-with-resources
およびAutoClosable
ストリーム。
「アロケートとクリーンアップ」は典型的な例ですが、トランザクション処理、ロギング、より多くの権限でのコードの実行など、他にも多くの例があります。基本的には template method pattern =しかし、継承なし。
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#の例については this post を、C++の例については this article をご覧ください。
メソッドの周囲で実行 は、メソッドに任意のコードを渡す場所です。メソッドは、セットアップおよび/またはティアダウンコードを実行し、その間にコードを実行します。
Javaはこれを行うために選択する言語ではありません。クロージャー(またはラムダ式)を引数として渡す方がよりスタイリッシュです。オブジェクトは間違いなく クロージャに相当 です。
メソッドの実行は、メソッドを呼び出すたびにアドホックに変更できる 制御の反転 (依存性注入)のようなものであるように思えます。
しかし、コントロールカップリングの例として解釈することもできます(文字列でこの場合、引数によって何をするかをメソッドに伝えます)。
Code Sandwiches も参照してください。これは多くのプログラミング言語でこの構成を調査し、興味深い研究のアイデアを提供します。なぜそれを使用するのかという特定の質問に関して、上記の論文はいくつかの具体的な例を提供します:
このような状況は、プログラムが共有リソースを操作するたびに発生します。ロック、ソケット、ファイル、またはデータベース接続用のAPIでは、以前に取得したリソースを明示的に閉じたり解放したりするプログラムが必要になる場合があります。ガベージコレクションのない言語では、プログラマは使用前にメモリを割り当て、使用後にメモリを解放する必要があります。一般に、さまざまなプログラミングタスクでは、プログラムが変更を行い、その変更のコンテキストで動作し、変更を元に戻すことが必要です。このような状況をコードサンドイッチと呼びます。
以降:
コードサンドイッチは、多くのプログラミング状況で表示されます。いくつかの一般的な例は、ロック、ファイル記述子、ソケット接続などの乏しいリソースの取得と解放に関連しています。より一般的な場合、プログラムの状態の一時的な変更には、コードサンドイッチが必要になる場合があります。たとえば、GUIベースのプログラムは一時的にユーザー入力を無視したり、OSカーネルはハードウェア割り込みを一時的に無効にしたりする場合があります。これらの場合に以前の状態を復元しないと、重大なバグが発生します。
この記事では、このイディオムを使用する理由notについては説明していませんが、言語レベルのヘルプがなくてもイディオムが間違っているのは簡単です:
例外とそれらに関連する目に見えない制御フローが存在する場合、欠陥コードサンドイッチが最も頻繁に発生します。実際、コードサンドイッチを管理するための特別な言語機能は、主に例外をサポートする言語で発生します。
ただし、コードサンドイッチの欠陥の原因は例外だけではありません。 bodyコードに変更が加えられるたびに、afterコードをバイパスする新しい制御パスが発生する場合があります。最も単純なケースでは、メンテナーは
return
ステートメントをサンドイッチのbodyに追加するだけで、新しいエラーが発生し、サイレントエラーにつながる可能性があります。 bodyコードが大きく、beforeとafterが大きく分離されている場合、そのような間違いは視覚的に検出するのが難しい場合があります。
私は4歳になるように説明しようと思います:
例1
サンタが町にやってくる。彼のエルフは、彼の背中の後ろで好きなものをコーディングします。
またはこれ:
.... 100万回のプレゼントで何回も吐き気がする:異なるのはステップ2だけであることに注意してください。 1と3百万回? 100万回のプレゼントは、彼がステップ1と3を不必要に100万回繰り返していることを意味します。
実行することは、その問題の解決に役立ちます。コードを排除するのに役立ちます。手順1と3は基本的に一定であり、変更するのは手順2のみです。
例#2
それでも手に入らない場合は、別の例を次に示します。外側のパンは常に同じですが、内側にあるものは、選択する砂の種類(ハム、チーズ、ジャム、ピーナッツバターなど)。パンは常に外側にあり、作成しているすべての種類の砂に対して10億回繰り返す必要はありません。
さて、上記の説明を読むと、おそらく理解しやすいでしょう。この説明がお役に立てば幸いです。
これは strategy design pattern を思い出させます。私が指したリンクには、パターンのコードJavaが含まれています。
明らかに、初期化およびクリーンアップコードを作成し、単に初期化およびクリーンアップコードでラップされる戦略を渡すだけで、「実行」を実行できます。
コードの繰り返しを減らすために使用される技術と同様に、少なくとも2つの場合、おそらく3(YAGNIの原則)が必要になるまで使用しないでください。コードの繰り返しを削除するとメンテナンスが減ります(コードのコピーが少なくなると、各コピーで修正をコピーする時間が減ります)が、メンテナンスも増えます(コードの合計が増える)。したがって、このトリックのコストは、コードを追加することです。
このタイプの手法は、初期化とクリーンアップ以外にも便利です。また、関数の呼び出しを簡単にしたい場合にも役立ちます(たとえば、ウィザードで使用して、「次へ」ボタンと「前へ」ボタンに何をするかを決定するための巨大なcaseステートメントが必要ないようにすることができます次/前のページ。
グルーヴィーなイディオムが必要な場合は、次のとおりです。
//-- 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(); }