Что это за идиома "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 с потоками try-with-resources и AutoClosable .
Хотя «распределение и очистка» является типичным приведенным примером, существует множество других возможных примеров - обработка транзакций, ведение журнала, выполнение некоторого кода с дополнительными привилегиями и т. д. Это в основном немного похоже на шаблон метода шаблон, но без наследования.
Чем это отличается от конструктора и деструктора, которые обрабатывают инициализацию и удаление?
Это детерминировано. Финализаторы в Java не вызываются детерминированно. Кроме того, как я сказал в последнем абзаце, это не Только, используемый для выделения ресурсов и очистки. Возможно, вообще не потребуется создавать новый объект. Обычно это «инициализация и разборка», но это может не быть выделением ресурсов.
Это как в C, где у вас есть функция, которую вы передаете в указатель на функцию, чтобы выполнить некоторую работу?
Я расширил концепцию EA в Java, насколько это болезненно, и предложил изменения языка, которые упростили бы задачу, здесь: metatechnology.blogspot.com/2007/02/… Это было в начале 2007 года - похоже, что замыкания до сих пор не вошли в Java :-(
Кроме того, Джон, вы имеете в виду замыкания в Java, которых до сих пор нет (если я это не пропустил). Вы описываете анонимные внутренние классы, а это не совсем одно и то же. Поддержка истинных закрытий (как было предложено - см. Мой блог) значительно упростила бы этот синтаксис.
@ Фил: Я думаю, это вопрос степени. Анонимные внутренние классы Java имеют доступ к окружающей среде в ограниченном смысле - поэтому, хотя они и не являются «полными» замыканиями, я бы сказал, что они «ограниченные» замыкания. Я бы определенно хотел видеть правильные закрытия в Java, хотя и проверено (продолжение)
исключения усложняют задачу. Я не совсем доволен какими-либо предложениями о закрытии, которые я видел. Они либо неуклюжи для гибкости, либо простые, но ограниченные :(
В Java 7 добавлена функция try-with-resource, а в Java 8 добавлены лямбда-выражения. Я знаю, что это старый вопрос / ответ, но я хотел указать на это всем, кто задумывался над этим вопросом пять с половиной лет спустя. Оба этих языковых инструмента помогут решить проблему, для решения которой был изобретен этот шаблон.
@JonSkeet: Отличный ответ, но как насчет «почему я могу не использовать его?» часть?
@Burkhard: Не думаю, что я мог бы сказать здесь что-то конкретное: используйте его, когда он упрощает код; не используйте его, если это не так.
Выполнить вокруг метода - это место, где вы передаете произвольный код методу, который может выполнять настройку и / или демонтаж кода и выполнять ваш код между ними.
Я бы предпочел не использовать Java для этого. Более стильно передать в качестве аргумента замыкание (или лямбда-выражение). Хотя объекты, возможно, эквивалент закрытия.
Мне кажется, что метод Execute Around в некотором роде похож на Инверсия контроля (внедрение зависимостей), который вы можете изменять произвольно каждый раз, когда вызываете метод.
Но его также можно интерпретировать как пример Control Coupling (в данном случае буквально в данном случае сообщая методу, что делать с помощью его аргумента).
Идиома 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)Эта идиома перемещает весь сложный избыточный код в одно место и делает вашу основную программу более читаемой (и поддерживаемой!)
Взгляните на эта почта для примера C# и эта статья для примера C++.
Я вижу, что у вас есть тег Java, поэтому я буду использовать Java в качестве примера, хотя шаблон не зависит от платформы.
Идея состоит в том, что иногда у вас есть код, который всегда включает один и тот же шаблон до запуска кода и после запуска кода. Хороший пример - JDBC. Вы всегда захватываете соединение и создаете оператор (или подготовленный оператор) перед запуском фактического запроса и обработкой набора результатов, а затем всегда выполняете ту же самую шаблонную очистку в конце - закрывая оператор и соединение.
Идея автономного выполнения заключается в том, что лучше исключить шаблонный код. Это сэкономит вам время на вводе текста, но причина глубже. Здесь используется принцип «не повторяйся» (DRY) - вы изолируете код в одном месте, поэтому, если есть ошибка, или вам нужно ее изменить, или вы просто хотите понять ее, все в одном месте.
Однако при таком разложении на множители возникает небольшая сложность в том, что у вас есть ссылки, которые необходимо видеть как "до", так и "после". В примере JDBC это будет включать Connection и (Prepared) Statement. Таким образом, чтобы справиться с этим, вы по существу "оборачиваете" свой целевой код стандартным кодом.
Возможно, вы знакомы с некоторыми типичными случаями в Java. Один из них - фильтры сервлетов. Другой - АОП вокруг советов. Третий - это различные классы xxxTemplate в Spring. В каждом случае у вас есть некоторый объект-оболочка, в который внедряется ваш «интересный» код (скажем, запрос JDBC и обработка набора результатов). Объект-оболочка выполняет часть «до», вызывает интересующий код, а затем выполняет часть «после».
Это напоминает мне шаблон разработки стратегии. Обратите внимание, что ссылка, на которую я указал, включает код Java для шаблона.
Очевидно, что можно выполнить «Execute Around», создав код инициализации и очистки и просто передав стратегию, которая затем всегда будет заключена в код инициализации и очистки.
Как и любой метод, используемый для уменьшения повторения кода, вы не должны использовать его, пока у вас не будет хотя бы 2 случаев, когда он вам нужен, возможно, даже 3 (по принципу YAGNI). Имейте в виду, что удаление повторения кода сокращает обслуживание (меньшее количество копий кода означает меньше времени, затрачиваемого на копирование исправлений для каждой копии), но также увеличивает объем обслуживания (больше общего кода). Таким образом, цена этого трюка состоит в том, что вы добавляете больше кода.
Этот тип техники полезен не только для инициализации и очистки. Это также хорошо, когда вы хотите упростить вызов своих функций (например, вы можете использовать его в мастере, чтобы кнопки «следующий» и «предыдущий» не нуждались в гигантских операторах регистра, чтобы решить, что делать, чтобы перейти к следующая / предыдущая страница.
Если вам нужны заводные идиомы, вот они:
//-- 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(); }
Если мое открытие терпит неудачу (скажем, получение реентерабельной блокировки), вызывается закрытие (скажем, освобождение реентерабельной блокировки, несмотря на то, что соответствующее открытие терпит неудачу).
См. Также Кодовые бутерброды, в котором рассматривается эта конструкция на многих языках программирования и предлагаются некоторые интересные идеи для исследований. Что касается конкретного вопроса о том, почему его можно использовать, вышеупомянутая статья предлагает несколько конкретных примеров:
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.
И позже:
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.
В документе не исследуется, почему нет использует эту идиому, но описывается, почему в этой идиоме легко ошибиться без помощи на уровне языка:
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
returnstatement 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.
Хорошее замечание, azurefrag. Я пересмотрел и расширил свой ответ, так что это действительно более самостоятельный ответ. Спасибо, что предложили это.
Я попытаюсь объяснить, как четырехлетнему ребенку:
Пример 1
Санта едет в город. Его эльфы кодируют все, что хотят, за его спиной, и, если они не изменят, все будет немного повторяться:
Или это:
.... до тошноты миллион раз с миллионом разных подарков: обратите внимание, что отличается только шаг 2. Если второй шаг - единственное, что отличается, то почему Санта дублирует код, т.е. почему он дублирует шаги 1 и 3 миллион раз? Миллион подарков означает, что он без нужды повторяет шаги 1 и 3 миллион раз.
Казнь помогает решить эту проблему. и помогает устранить код. Шаги 1 и 3 в основном постоянны, что позволяет изменять только шаг 2.
Пример # 2
Если вы все еще не понимаете его, вот еще один пример: представьте себе сэндвич: хлеб снаружи всегда один и тот же, но то, что внутри, меняется в зависимости от типа сэндвича, который вы выбираете (например, ветчина, сыр, джем, арахисовое масло и т. д.). Хлеб всегда снаружи, и вам не нужно повторять это миллиард раз для каждого типа создаваемого вами песка.
Теперь, если вы прочтете приведенные выше объяснения, возможно, вам будет легче понять. Надеюсь, это объяснение вам помогло.
+ для фантазии: D
Я не заметил, что это ты, Тэкс. В противном случае я мог бы ответить более саркастично;)