私が聞いてきたこの "Execute Around" イディオム (または類似の) とは何ですか? なぜそれを使うことができ、なぜそれを使いたくないのでしょうか?
8 に答える
基本的には、リソースの割り当てやクリーンアップなど、常に必要なことを実行するメソッドを記述し、呼び出し元に「リソースで何をしたいのか」を渡すパターンです。例えば:
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など)と同じように実装できます。この特殊なケースは、Java7以降で処理されtry-with-resources
ますAutoClosable
。
「割り当てとクリーンアップ」が典型的な例ですが、トランザクション処理、ロギング、より多くの特権を持つコードの実行など、他にも多くの可能な例があります。これは基本的にテンプレートメソッドパターンに少し似ていますが、継承はありません。
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 ++の例についてはこの記事をご覧ください。
Code Sandwichesも参照してください。これは、多くのプログラミング言語でこの構造を調査し、興味深い研究アイデアを提供しています。なぜそれを使用するのかという具体的な質問に関して、上記の論文はいくつかの具体的な例を提供しています:
このような状況は、プログラムが共有リソースを操作するたびに発生します。ロック、ソケット、ファイル、またはデータベース接続用の API では、プログラムが以前に取得したリソースを明示的に閉じたり解放したりする必要がある場合があります。ガベージ コレクションのない言語では、プログラマは使用前にメモリを割り当て、使用後に解放する責任があります。一般に、さまざまなプログラミング タスクでは、プログラムが変更を行い、その変更のコンテキストで動作し、変更を元に戻す必要があります。このような状況をコード サンドイッチと呼びます。
以降:
コード サンドイッチは、多くのプログラミング状況で見られます。いくつかの一般的な例は、ロック、ファイル記述子、ソケット接続などの希少なリソースの取得と解放に関連しています。より一般的なケースでは、プログラム状態の一時的な変更には、コード サンドイッチが必要になる場合があります。たとえば、GUI ベースのプログラムがユーザー入力を一時的に無視したり、OS カーネルがハードウェア割り込みを一時的に無効にしたりすることがあります。このような場合に以前の状態を復元しないと、重大なバグが発生します。
このペーパーでは、このイディオムを使用しない理由については説明していませんが、言語レベルのヘルプがないとイディオムが簡単に間違ってしまう理由について説明しています。
欠陥のあるコード サンドイッチは、例外とそれに関連する目に見えない制御フローが存在する場合に最も頻繁に発生します。実際、コード サンドイッチを管理するための特別な言語機能は、主に例外をサポートする言語で発生します。
ただし、コード サンドイッチの欠陥の原因は例外だけではありません。本体コードに変更が加えられるたびに、 afterコードをバイパスする新しい制御パスが発生する可能性があります。最も単純なケースでは、保守担当者
return
はサンドイッチの本体にステートメントを追加して新しい欠陥を導入するだけでよく、サイレント エラーが発生する可能性があります。本体 コードが大きく、前後が大きく離れている場合、このようなミスは視覚的にはわかりにくい場合があります。
Execute Aroundメソッドは、任意のコードをメソッドに渡す場所です。メソッドは、セットアップやティアダウンコードを実行し、その間にコードを実行する場合があります。
Javaは、私がこれを行うために選択する言語ではありません。引数としてクロージャ(またはラムダ式)を渡す方がスタイリッシュです。オブジェクトはほぼ間違いなくクロージャと同等ですが。
Execute Aroundメソッドは、メソッドを呼び出すたびにアドホックに変更できる制御の反転(依存性注入)のようなもののように思えます。
ただし、制御結合の例として解釈することもできます(この場合、文字通り、引数によってメソッドに何をするかを指示します)。
ここにJavaタグがあるので、パターンがプラットフォーム固有ではない場合でも、例としてJavaを使用します。
アイデアは、コードを実行する前と実行した後に、常に同じ定型文を含むコードがある場合があるということです。良い例はJDBCです。実際のクエリを実行して結果セットを処理する前に、常に接続を取得してステートメント(またはプリペアドステートメント)を作成し、最後に常に同じ定型的なクリーンアップを実行して、ステートメントと接続を閉じます。
実行アラウンドの考え方は、ボイラープレートコードを除外できればより良いということです。タイピングの手間は省けますが、その理由はもっと深いです。ここでは、Do n't-repeat-yourself(DRY)の原則です。コードを1つの場所に分離するため、バグがある場合や変更する必要がある場合、または単に理解したい場合は、すべて1か所にまとめられます。
ただし、この種のファクタリングで少し注意が必要なのは、「前」と「後」の両方の部分で確認する必要のある参照があることです。JDBCの例では、これにはConnectionと(Prepared)Statementが含まれます。したがって、これを処理するには、基本的にターゲットコードをボイラープレートコードで「ラップ」します。
あなたはJavaのいくつかの一般的なケースに精通しているかもしれません。1つはサーブレットフィルターです。もう1つはアドバイスに関するAOPです。3つ目は、SpringのさまざまなxxxTemplateクラスです。いずれの場合も、「興味深い」コード(JDBCクエリや結果セットの処理など)が挿入されるラッパーオブジェクトがあります。ラッパーオブジェクトは「前」の部分を実行し、対象のコードを呼び出してから「後」の部分を実行します。
これは戦略設計パターンを思い出させます。私が指摘したリンクには、パターンの 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(); }