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