Что такое "Execute Around" идиома?

Что это за "Идущий вокруг" идиома (или подобное), о котором я слышал? Почему я могу использовать его, и почему я не хочу его использовать?

Ответ 1

В основном это шаблон, в котором вы пишете способ делать то, что всегда необходимо, например. распределение ресурсов и очистка, а также заставить вызывающего передать "то, что мы хотим делать с ресурсом". Например:

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 лямбда-выражений можно реализовать, как на многих других языках (например, лямбда-выражения С# или Groovy), и этот частный случай обрабатывается с Java 7 с потоками try-with-resources и AutoClosable.

Несмотря на то, что типичный пример представлен как "распределение и очистка", существует множество других возможных примеров: обработка транзакций, ведение журнала, выполнение некоторого кода с большим количеством привилегий и т.д. В основном это похоже на шаблон шаблона шаблона, но без наследования.

Ответ 2

Идтиком 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)

Эта идиома переводит весь сложный избыточный код в одно место и оставляет вашу основную программу более читабельной (и поддерживаемой!)

Взгляните на этот пост для примера С# и this статью для примера на С++.

Ответ 3

Я вижу, что у вас есть тег Java, поэтому я буду использовать Java в качестве примера, даже если шаблон не зависит от платформы.

Идея заключается в том, что иногда у вас есть код, который всегда включает один и тот же шаблон, прежде чем запускать код и после запуска кода. Хорошим примером является JDBC. Вы всегда получаете соединение и создаете оператор (или подготовленный оператор) перед запуском фактического запроса и обрабатываете результирующий набор, а затем вы всегда выполняете ту же самую очистку шаблонов в конце - закрываете оператор и соединение.

Идея с выполнением заключается в том, что это лучше, если вы можете отделить шаблонный код. Это экономит вас на типизации, но причина глубже. Это принцип don't-repeat-yourself (DRY) здесь - вы изолируете код в одном месте, поэтому, если есть ошибка или вам нужно ее изменить, или вы просто хотите это понять, все это в одном месте.

То, что немного сложно с этим факторингом, состоит в том, что у вас есть ссылки, которые должны видеть как "до", так и "после". В примере JDBC это будет включать в себя соединение и (подготовленное) выражение. Поэтому, чтобы справиться с тем, что вы по существу "завершаете" свой целевой код с помощью шаблона кода.

Вы можете быть знакомы с некоторыми распространенными случаями в Java. Один из них - сервлет-фильтры. Другое - это АОП вокруг совета. Третий - это различные классы xxxTemplate в Spring. В каждом случае у вас есть некоторый объект-оболочка, в который вводится ваш "интересный" код (например, запрос JDBC и обработка набора результатов). Объект-обертка выполняет "перед" часть, вызывает интересный код, а затем выполняет "после" часть.

Ответ 4

См. также Code Sandwiches, который исследует эту конструкцию на многих языках программирования и предлагает интересные исследовательские идеи. Что касается конкретного вопроса о том, почему его можно использовать, в приведенном выше документе представлены некоторые конкретные примеры:

Такие ситуации возникают, когда программа управляет совместно используемыми ресурсами. API для блокировок, сокетов, файлов или подключений к базам данных может потребовать программа для явного закрытия или выпуска ресурса, который он ранее приобрела. На языке без сбора мусора программист ответственный за распределение памяти перед ее использованием и освобождение после его использования. В целом, различные задачи программирования требуют программы, чтобы внести изменения, действовать в контексте этих изменений и затем отмените изменение. Мы называем такие ситуации бутербродами кода.

И позже:

Сэндвичи кода появляются во многих ситуациях программирования. Несколько общих примеры связаны с приобретением и выпуском ограниченных ресурсов, таких как блокировки, дескрипторы файлов или соединения сокетов. В большей степени общих случаях, любое временное изменение состояния программы может потребовать сэндвич с кодом. Например, программа на основе графического интерфейса пользователя может временно игнорировать пользовательские входы или ядро ​​ОС могут временно отключить аппаратное обеспечение перебивает. Неспособность восстановить прежнее состояние в этих случаях приведет к серьезные ошибки.

В статье не рассматривается, почему бы не использовать эту идиому, но она описывает, почему идиома легко ошибиться без помощи на уровне языка:

Дефектные сэндвичи с кодом чаще всего возникают в присутствии исключений и связанного с ними невидимого потока управления. В самом деле, специальные функции языка для управления бутербродами кода возникают главным образом в языки, которые поддерживают исключения.

Однако исключения не являются единственной причиной неисправного кода бутерброды. Всякий раз, когда в код кузова вносятся изменения, новые пути управления может возникнуть, что обойти код после. В простейшем случае сопровождающему нужно добавить только оператор return в тело сэндвичей, чтобы ввести новый дефект, что может привести к бесшумным ошибкам. Когда тело код большой и до и после широко разделены, такие ошибки может быть трудно обнаружить визуально.

Ответ 5

An Выполнять метод окружающего звука - это то, где вы передаете произвольный код методу, который может выполнять установку и/или разрывать код и выполнять ваш код между ними.

Java - это не тот язык, на который я бы выбрал. Это более стильно, чтобы передать закрытие (или лямбда-выражение) в качестве аргумента. Хотя объекты, возможно, эквивалент закрытия.

Мне кажется, что метод Execute Around похож на Inversion of Control (Dependency Injection), который вы можете варьировать ad hoc, каждый раз, когда вы вызываете метод.

Но это также можно было бы интерпретировать как пример Control Coupling (говорящий о методе, что делать по его аргументу, буквально в этом случае).

Ответ 6

Это напоминает мне шаблон шаблона стратегии. Обратите внимание, что ссылка, на которую я указал, включает код Java для шаблона.

Очевидно, что можно выполнить "Execute Around", выполнив код инициализации и очистки и просто перейдя в стратегию, которая всегда будет обернута в код инициализации и очистки.

Как и в случае любого метода, используемого для уменьшения повторения кода, вы не должны использовать его, пока у вас не будет, по крайней мере, 2 случая, когда вам это нужно, возможно, даже 3 (a la the YAGNI). Имейте в виду, что удаление кода повторяется, сокращает обслуживание (меньшее количество копий кода означает меньше времени на копирование исправлений в каждой копии), но также увеличивает обслуживание (более общий код). Таким образом, стоимость этого трюка заключается в том, что вы добавляете больше кода.

Этот тип техники полезен не только для инициализации и очистки. Это также полезно, когда вы хотите облегчить вызов своих функций (например, вы можете использовать его в мастере, чтобы кнопки "next" и "предыдущие" не нуждались в гигантских заявлениях о случаях, чтобы решить, что делать, чтобы перейти к следующей/предыдущей странице.

Ответ 7

Я попытаюсь объяснить, как я бы хотел четыре года:

Пример 1

Санта приезжает в город. Его эльфы кодируют все, что захотят за его спиной, и, если они не меняются, все становится немного повторяющимся:

  • Получить упаковочную бумагу
  • Получить Super Nintendo.
  • Оберните его.

Или это:

  • Получить упаковочную бумагу
  • Получить Barbie Doll.
  • Оберните его.

.... ad nauseam миллион раз с миллионом разных подарков: обратите внимание, что единственное отличие - это шаг 2. Если второй шаг - это единственное, что отличается, то почему Санта дублирует код, то есть почему он дублирует шаги 1 и 3 миллиона раз? Миллион подарков означает, что он бесполезно повторяет шаги 1 и 3 миллиона раз.

Выполнение задачи помогает решить эту проблему. и помогает устранить код. Шаги 1 и 3 в основном являются постоянными, что позволяет изменять только часть 2.

Пример # 2

Если вы все еще не понимаете этого, вот еще один пример: подумайте о песке: хлеб снаружи всегда один и тот же, но что внутри меняется в зависимости от типа песка, который вы выбираете (.eg ham, сыр, джем, арахисовое масло и т.д.). Хлеб всегда снаружи, и вам не нужно повторять, что миллиард раз для каждого типа песка, который вы создаете.

Теперь, если вы прочтете приведенные выше объяснения, возможно, вам будет легче понять. Надеюсь, это объяснение вам помогло.

Ответ 8

Если вы хотите groovy идиомы, вот оно:

//-- 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(); }