Что это за идиома "Execute Around" (или похожая), о которой я слышал? Почему я могу его использовать и почему я могу не захотеть его использовать?
Что такое идиома 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), и этот особый случай обрабатывается, начиная с Java 7 с потоками 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)
Эта идиома перемещает весь сложный избыточный код в одно место и делает вашу основную программу более читаемой (и поддерживаемой!)
Взгляните на этот пост для C # пример и эта статья для примера C ++ .
См. Также Code Sandwiches, в котором исследуется эта конструкция на многих языках программирования и в предложениях. несколько интересных исследовательских идей. Что касается конкретного вопроса о том, почему его можно использовать, вышеупомянутая статья предлагает несколько конкретных примеров:
Такие ситуации возникают всякий раз, когда программа манипулирует общими ресурсами. API-интерфейсы для блокировок, сокетов, файлов или соединений с базой данных могут потребовать от программы явного закрытия или освобождения ранее полученного ресурса. В языке без сборки мусора программист несет ответственность за выделение памяти перед ее использованием и освобождение после ее использования. В общем, различные задачи программирования требуют, чтобы программа внесла изменение, работала в контексте этого изменения, а затем отменила это изменение. Мы называем такие ситуации кодовыми бутербродами.
И позже:
Бутерброды с кодом появляются во многих ситуациях программирования. Несколько распространенных примеров относятся к получению и освобождению ограниченных ресурсов, таких как блокировки, файловые дескрипторы или соединения сокетов. В более общих случаях для любого временного изменения состояния программы может потребоваться сэндвич с кодом. Например, программа на основе графического интерфейса пользователя может временно игнорировать вводимые пользователем данные, или ядро ОС может временно отключать аппаратные прерывания. Невозможность восстановить предыдущее состояние в этих случаях вызовет серьезные ошибки.
В документе не исследуется, почему нельзя использовать эту идиому, но описывается, почему в этой идиоме легко ошибиться без помощи на уровне языка:
Сэндвичи с дефектным кодом чаще всего возникают при наличии исключений и связанных с ними невидимых потоков управления. Действительно, специальные языковые функции для управления сэндвичами с кодом возникают в основном в языках, поддерживающих исключения.
Однако исключения - не единственная причина появления дефектных бутербродов с кодом. Каждый раз, когда в код body вносятся изменения, могут возникнуть новые пути управления, которые обходят код после. В простейшем случае специалисту по сопровождению достаточно добавить оператор
returnв тело сэндвича, чтобы ввести новый дефект, который может привести к скрытым ошибкам. Когда основной код большой и до и после сильно разделены, такие ошибки может быть трудно обнаружить визуально.
Execute Around Method - это когда вы передаете произвольный код методу, который может выполнять настройку и / или разорвите код и выполните свой код между ними.
Я бы предпочел не использовать Java для этого. Более стильно передать в качестве аргумента замыкание (или лямбда-выражение). Хотя, возможно, объекты эквивалентны замыканиям.
Мне кажется, что метод Execute Around в некотором роде похож на инверсию управления (внедрение зависимостей), вы можете изменять ad hoc каждый раз, когда вызываете метод.
Но это также можно интерпретировать как пример Control Coupling (в данном случае буквально сообщая методу, что делать с помощью его аргумента).
Я вижу, что у вас здесь есть тег Java, поэтому я буду использовать Java в качестве примера, хотя шаблон не зависит от платформы.
Идея состоит в том, что иногда у вас есть код, который всегда включает один и тот же шаблон до запуска кода и после запуска кода. Хороший пример - JDBC. Вы всегда захватываете соединение и создаете оператор (или подготовленный оператор) перед запуском фактического запроса и обработкой набора результатов, а затем всегда выполняете ту же самую шаблонную очистку в конце - закрывая оператор и соединение.
Идея автономного выполнения заключается в том, что лучше исключить шаблонный код. Это сэкономит вам время на вводе текста, но причина глубже. Здесь действует принцип «не повторяйся» (DRY) - вы изолируете код в одном месте, поэтому, если есть ошибка, или вам нужно ее изменить, или вы просто хотите понять ее, все в одном месте.
Но вот что немного сложно при таком разложении на множители, так это то, что у вас есть ссылки, которые должны видеть как части «до», так и «после». В примере JDBC это будет включать в себя соединение и (подготовленный) оператор. Таким образом, чтобы справиться с этим, вы по существу "оборачиваете" свой целевой код стандартным кодом.
Возможно, вы знакомы с некоторыми типичными случаями в Java. Один из них - это фильтры сервлетов. Другой - АОП вокруг советов. Третий - это различные классы xxxTemplate в Spring. В каждом случае у вас есть некий объект-оболочка, в который внедряется ваш «интересный» код (скажем, запрос JDBC и обработка набора результатов). Объект-оболочка выполняет часть «до», вызывает интересующий код, а затем выполняет часть «после».
Попробую объяснить, как четырехлетнему:
Пример 1
Санта едет в город. Его эльфы кодируют за его спиной все, что хотят, и, если они не изменят ситуацию, будут повторяться:
- Получите оберточную бумагу
- Купите Super Nintendo.
- Заверните.
Или это:
- Получите оберточную бумагу
- Купите куклу Барби.
- Заверните.
.... до тошноты миллион раз с миллионом разных подарков: обратите внимание, что отличается только шаг 2. Если второй шаг - единственное, что отличается, то почему Санта дублирует код, т.е. почему он дублирует шаги 1 и 3 миллион раз? Миллион подарков означает, что он без нужды повторяет шаги 1 и 3 миллион раз.
Казнь помогает решить эту проблему. и помогает устранить код. Шаги 1 и 3 в основном постоянны, поэтому изменяется только шаг 2.
Пример 2
Если вы все еще не понимаете его, вот еще один пример: представьте себе сэндвич: хлеб снаружи всегда один и тот же, но то, что внутри, меняется в зависимости от типа сэндвича, который вы выбираете (например, ветчина, сыр, джем, арахисовое масло и т. д.). Хлеб всегда снаружи, и вам не нужно повторять это миллиард раз для каждого типа создаваемого вами песка.
Теперь, если вы прочитаете приведенные выше объяснения, возможно, вам будет легче понять. Надеюсь, это объяснение вам помогло.
Это напоминает мне шаблон разработки стратегии. Обратите внимание, что ссылка, на которую я указал, включает код 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(); }