What is the "Execute Around" idiom?
What is this "Execute Around" idiom (or similar) I've been hearing about? Why might I use it, and why might I not want to use it?
Basically it's the pattern where you write a method to do things which are always required, e.g. resource allocation and clean-up, and make the caller pass in "what we want to do with the resource". For example:
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);
The calling code doesn't need to worry about the open/clean-up side - it will be taken care of by executeWithFile
.
This was frankly painful in Java because closures were so wordy, starting with Java 8 lambda expressions can be implemented like in many other languages (e.g. C# lambda expressions, or Groovy), and this special case is handled since Java 7 with try-with-resources
and AutoClosable
streams.
Although "allocate and clean-up" is the typical example given, there are plenty of other possible examples - transaction handling, logging, executing some code with more privileges etc. It's basically a bit like the template method pattern but without inheritance.
The Execute Around idiom is used when you find yourself having to do something like this:
//... 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.
In order to avoid repeating all of this redundant code that is always executed "around" your actual tasks, you would create a class that takes care of it automatically:
//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)
This idiom moves all of the complicated redundant code into one place, and leaves your main program much more readable (and maintainable!)
Take a look at this post for a C# example, and this article for a C++ example.
See also Code Sandwiches, which surveys this construct across many programming languages and offers some interesting research’y ideas. Concerning the specific question of why one might use it, the above paper offers some concrete examples:
Such situations arise whenever a program manipulates shared resources. APIs for locks, sockets, files, or database connections may require a program to explicitly close or release a resource that it previously acquired. In a language without garbage collection, the programmer is responsible for allocating memory before its use and releasing it after its use. In general, a variety of programming tasks call for a program to make a change, operate in the context of that change, and then undo the change. We call such situations code sandwiches.
And later:
Code sandwiches appear in many programming situations. Several common examples relate to the acquisition and release of scarce resources, such as locks, file descriptors, or socket connections. In more general cases, any temporary change of program state may require a code sandwich. For example, a GUI-based program may temporarily ignore user inputs, or an OS kernel may temporarily disable hardware interrupts. Failure to restore earlier state in these cases will cause serious bugs.
The paper does not explore why not to use this idiom, but it does describe why the idiom is easy to get wrong without language-level help:
Defective code sandwiches arise most frequently in the presence of exceptions and their associated invisible control flow. Indeed, special language features to manage code sandwiches arise chiefly in languages that support exceptions.
However, exceptions are not the only cause of defective code sandwiches. Whenever changes are made to body code, new control paths may arise that bypass the after code. In the simplest case, a maintainer need only add a
return
statement to a sandwich’s body to introduce a new defect, which may lead to silent errors. When the body code is large and before and after are widely separated, such mistakes can be hard to detect visually.