Что такое идиома «Казни вокруг»?

151

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

Том Хотин - Tackline
источник
9
Я не заметил, что это был ты, галс. В противном случае я мог бы быть более саркастичным в своем ответе;)
Джон Скит
1
Так что это в основном аспект, верно? Если нет, то чем он отличается?
Лукас

Ответы:

147

По сути, это шаблон, в котором вы пишете метод, который делает вещи, которые всегда требуются, например, выделение ресурсов и очистка, и заставляет вызывающего передать «то, что мы хотим сделать с ресурсом». Например:

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 потоков.

Хотя типичным примером является «выделение и очистка», существует множество других возможных примеров - обработка транзакций, ведение журнала, выполнение некоторого кода с большим количеством привилегий и т. Д. Это в основном немного похоже на шаблонный шаблон, но без наследования.

Джон Скит
источник
4
Это детерминистично. Финализаторы в Java не называются детерминированно. Кроме того, как я уже говорил в последнем абзаце, он используется не только для распределения ресурсов и очистки. Возможно, не нужно создавать новый объект вообще. Обычно это «инициализация и разборка», но это может быть не распределение ресурсов.
Джон Скит
3
Так как в C, где у вас есть функция, которую вы передаете в указатель на функцию, чтобы сделать какую-то работу?
Пол Томблин
3
Кроме того, Джон, вы ссылаетесь на замыкания в Java - которых у него до сих пор нет (если я не пропустил это). То, что вы описываете, это анонимные внутренние классы, которые не совсем одно и то же. Поддержка истинных замыканий (как было предложено - см. Мой блог) значительно упростила бы этот синтаксис.
philsquared
8
@Phil: я думаю, что это вопрос степени. У анонимных внутренних классов Java есть доступ к окружающей их среде в ограниченном смысле - поэтому, хотя они и не являются «полными» замыканиями, они «ограничены» замыканиями, я бы сказал. Я, конечно, хотел бы видеть правильные замыкания в Java, хотя проверено (продолжение)
Jon Skeet
4
Java 7 добавила попытку с ресурсом, а Java 8 добавила лямбды. Я знаю, что это старый вопрос / ответ, но я хотел бы указать на это всем, кто рассматривает этот вопрос пять с половиной лет спустя. Оба эти языковых инструмента помогут решить проблему, которая была изобретена для исправления этого паттерна.
45

Идиома «Выполнить вокруг» используется, когда вам приходится делать что-то вроде этого:

//... 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 ++.

e.James
источник
7

Выполнить Around метод , где вы передаете произвольный код метода, который может выполнять настройку и / или Teardown код и выполнить код между ними.

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

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

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

Билл Карвин
источник
7

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

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

Идея с циклическим выполнением состоит в том, что было бы лучше, если бы вы могли выделить исходный код. Это экономит время при наборе текста, но причина кроется глубже. Здесь принцип «не повторяйся сам» («СУХОЙ») - вы изолируете код в одном месте, поэтому, если есть ошибка, или вам нужно ее изменить, или вы просто хотите ее понять, все это в одном месте.

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

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


источник
7

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

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

И позже:

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

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

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

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

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

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

Пример 1

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

  1. Получить упаковочную бумагу
  2. Получи Супер Нинтендо .
  3. Заверните это.

Или это:

  1. Получить упаковочную бумагу
  2. Получите куклу Барби .
  3. Заверните это.

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

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

Пример № 2

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

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

BKSpurgeon
источник
+ для фантазии: D
сэр. Ежик
3

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

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

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

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

Брайан
источник
0

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

//-- 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(); }
Флорин
источник
Если мое открытие не удается (скажем, получение блокировки повторного входа), закрытие вызывается (скажем, снятие блокировки повторного входа, несмотря на сбой соответствующего открытия).
Том Хотин - tackline