Qual é o idioma "Execute Around"?

151

O que é esse idioma "Execute Around" (ou similar) sobre o qual tenho ouvido falar? Por que devo usá-lo e por que não desejo usá-lo?

Tom Hawtin - linha de orientação
fonte
9
Eu não tinha notado que era você, tacha. Caso contrário, eu poderia ter sido mais sarcástico na minha resposta;)
Jon Skeet
1
Então isso é basicamente um aspecto, certo? Se não, como isso difere?
Lucas

Respostas:

147

Basicamente, é o padrão em que você escreve um método para fazer as coisas sempre necessárias, por exemplo, alocação e limpeza de recursos e faz com que o chamador passe "o que queremos fazer com o recurso". Por exemplo:

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);

O código de chamada não precisa se preocupar com o lado aberto / de limpeza - ele será tratado por executeWithFile.

Isso foi francamente doloroso em Java porque os fechamentos eram muito prolixo, a partir das expressões lambda do Java 8 pode ser implementado como em muitas outras linguagens (por exemplo, expressões lambda C # ou Groovy), e esse caso especial é tratado desde o Java 7 com try-with-resourcese AutoClosablefluxos.

Embora "alocar e limpar" seja o exemplo típico dado, existem muitos outros exemplos possíveis - manipulação de transações, registro em log, execução de algum código com mais privilégios etc. É basicamente um pouco como o padrão do método de modelo, mas sem herança.

Jon Skeet
fonte
4
É determinístico. Os finalizadores em Java não são chamados deterministicamente. Também como eu disse no último parágrafo, ele não é usado apenas para alocação e limpeza de recursos. Pode não ser necessário criar um novo objeto. Geralmente é "inicialização e desmontagem", mas isso pode não ser a alocação de recursos.
Jon Skeet
3
Então é como em C onde você tem uma função que você passa em um ponteiro de função para fazer algum trabalho?
Paul Tomblin
3
Além disso, Jon, você se refere aos fechamentos em Java - que ele ainda não possui (a menos que eu tenha perdido). O que você descreve são classes internas anônimas - que não são exatamente a mesma coisa. O suporte a verdadeiros fechamentos (como foi proposto - consulte meu blog) simplificaria consideravelmente essa sintaxe.
Philsquared
8
@ Phil: Eu acho que é uma questão de grau. As classes internas anônimas Java têm acesso ao ambiente circundante em um sentido limitado - portanto, embora não sejam encerramentos "completos", são encerramentos "limitados", eu diria. Eu certamente gostaria de ver fechamentos apropriados em Java, embora marcada (continuação)
Jon Skeet
4
O Java 7 adicionou try-with-resource e o Java 8 adicionou lambdas. Sei que essa é uma pergunta / resposta antiga, mas gostaria de salientar isso para quem olha para essa pergunta cinco anos e meio depois. Ambas as ferramentas de linguagem ajudarão a resolver o problema que esse padrão foi inventado para corrigir.
45

O idioma Execute Around é usado quando você precisa fazer algo assim:

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

Para evitar repetir todo esse código redundante que é sempre executado "em torno de" suas tarefas reais, você deve criar uma classe que cuida dele automaticamente:

//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)

Esse idioma move todo o código redundante complicado para um único local e deixa o programa principal muito mais legível (e fácil de manter!)

Dê uma olhada nesta postagem para obter um exemplo de C # e neste artigo para um exemplo de C ++.

e.James
fonte
7

Um método Execute Around é o local onde você passa o código arbitrário para um método, que pode executar o código de instalação e / ou desmontagem e executar seu código no meio.

Java não é a linguagem na qual eu escolheria fazer isso. É mais elegante passar um fechamento (ou expressão lambda) como argumento. Embora objetos sejam sem dúvida equivalentes a fechamentos .

Parece-me que o método Execute Around é como Inversão de controle (injeção de dependência) que você pode variar ad hoc, toda vez que chama o método.

Mas também poderia ser interpretado como um exemplo de Control Coupling (dizendo a um método o que fazer por seu argumento, literalmente neste caso).

Bill Karwin
fonte
7

Vejo que você tem uma tag Java aqui, então usarei o Java como exemplo, mesmo que o padrão não seja específico da plataforma.

A idéia é que, às vezes, você tenha um código que sempre envolva o mesmo padrão antes de executar o código e depois de executá-lo. Um bom exemplo é o JDBC. Você sempre pega uma conexão e cria uma instrução (ou instrução preparada) antes de executar a consulta real e processar o conjunto de resultados e, em seguida, faz sempre a mesma limpeza no final - fechando a instrução e a conexão.

A idéia com executar ao redor é que é melhor se você puder fatorar o código padrão. Isso economiza sua digitação, mas o motivo é mais profundo. É o princípio de não repetir a si mesmo (DRY) aqui - você isola o código em um local, por isso, se houver um bug ou precisar alterá-lo ou apenas quiser entendê-lo, está tudo em um só lugar.

O que é um pouco complicado com esse tipo de fatoração é que você tem referências que as partes "antes" e "depois" precisam ver. No exemplo JDBC, isso incluiria a conexão e a instrução (preparada). Portanto, para lidar com isso, você "envolve" seu código de destino com o código padrão.

Você pode estar familiarizado com alguns casos comuns em Java. Um é o filtro de servlet. Outra é AOP em torno de conselhos. Um terceiro são as várias classes xxxTemplate na primavera. Em cada caso, você tem algum objeto wrapper no qual seu código "interessante" (digamos, a consulta JDBC e o processamento do conjunto de resultados) é injetado. O objeto wrapper faz a parte "antes", chama o código interessante e depois faz a parte "depois".


fonte
7

Veja também Code Sandwiches , que examina esse construto em muitas linguagens de programação e oferece algumas idéias interessantes de pesquisa. Com relação à questão específica de por que alguém pode usá-lo, o artigo acima oferece alguns exemplos concretos:

Tais situações surgem sempre que um programa manipula recursos compartilhados. APIs para bloqueios, soquetes, arquivos ou conexões com o banco de dados podem exigir que um programa feche ou libere explicitamente um recurso adquirido anteriormente. Em um idioma sem coleta de lixo, o programador é responsável por alocar memória antes do uso e liberá-lo após o uso. Em geral, várias tarefas de programação exigem que um programa faça uma alteração, opere no contexto dessa alteração e desfaça a alteração. Chamamos essas situações de sanduíches de código.

E depois:

Sanduíches de código aparecem em muitas situações de programação. Vários exemplos comuns estão relacionados à aquisição e liberação de recursos escassos, como bloqueios, descritores de arquivo ou conexões de soquete. Em casos mais gerais, qualquer alteração temporária do estado do programa pode exigir um sanduíche de código. Por exemplo, um programa baseado em GUI pode ignorar temporariamente as entradas do usuário ou um kernel do SO pode desativar temporariamente as interrupções de hardware. A falha ao restaurar o estado anterior nesses casos causará erros graves.

O artigo não explora por que não usar esse idioma, mas descreve por que é fácil errar o idioma sem a ajuda no nível do idioma:

Sanduíches de código com defeito surgem com mais freqüência na presença de exceções e seu fluxo de controle invisível associado. De fato, recursos especiais de linguagem para gerenciar sanduíches de código surgem principalmente em idiomas que suportam exceções.

No entanto, as exceções não são a única causa de sanduíches de código com defeito. Sempre que são feitas alterações no código do corpo , podem surgir novos caminhos de controle que ignoram o código posterior . No caso mais simples, um mantenedor precisa apenas adicionar uma returndeclaração ao corpo de um sanduíche para introduzir um novo defeito, o que pode levar a erros silenciosos. Quando o código do corpo é grande e o antes e o depois são amplamente separados, esses erros podem ser difíceis de detectar visualmente.

Ben Liblit
fonte
Bom ponto, azurefrag. Revi e ampliei minha resposta para que ela realmente seja mais uma resposta independente por si só. Obrigado por sugerir isso.
precisa saber é o seguinte
4

Vou tentar explicar, como faria com uma criança de quatro anos:

Exemplo 1

Papai Noel está vindo para a cidade. Seus elfos codificam o que quiserem pelas costas e, a menos que mudem, as coisas ficam um pouco repetitivas:

  1. Obter papel de embrulho
  2. Adquira Super Nintendo .
  3. Enrole.

Ou isto:

  1. Obter papel de embrulho
  2. Adquira Barbie Doll .
  3. Enrole.

.... ad nauseam um milhão de vezes com um milhão de presentes diferentes: observe que a única coisa diferente é a etapa 2. Se a segunda etapa é a única coisa diferente, por que o Papai Noel está duplicando o código, ou seja, por que ele está duplicando as etapas 1 e 3 um milhão de vezes? Um milhão de presentes significa que ele está repetindo desnecessariamente as etapas 1 e 3 um milhão de vezes.

Executar ajuda a resolver esse problema. e ajuda a eliminar o código. Os passos 1 e 3 são basicamente constantes, permitindo que o passo 2 seja a única parte que muda.

Exemplo 2

Se você ainda não entendeu, aqui está outro exemplo: pense em um sanduíche: o pão por fora é sempre o mesmo, mas o que está por dentro muda dependendo do tipo de sanduíche que você escolher (presunto, queijo, queijo, geléia, manteiga de amendoim etc). O pão está sempre do lado de fora e você não precisa repetir isso um bilhão de vezes para cada tipo de areia que estiver criando.

Agora, se você ler as explicações acima, talvez seja mais fácil entender. Espero que esta explicação tenha ajudado.

BKSpurgeon
fonte
+ para a imaginação: D
senhor. Hedgehog
3

Isso me lembra o padrão de design da estratégia . Observe que o link que eu apontei inclui código Java para o padrão.

Obviamente, pode-se executar "Execute Around" criando código de inicialização e limpeza e passando apenas uma estratégia, que será sempre envolvida no código de inicialização e limpeza.

Como em qualquer técnica usada para reduzir a repetição de código, você não deve usá-la até ter pelo menos 2 casos em que precisar, talvez até 3 (à la o princípio YAGNI). Lembre-se de que a remoção da repetição do código reduz a manutenção (menos cópias de código significa menos tempo gasto na cópia de correções em cada cópia), mas também aumenta a manutenção (mais código total). Portanto, o custo desse truque é que você está adicionando mais código.

Esse tipo de técnica é útil para mais do que apenas inicialização e limpeza. Também é bom para quando você deseja facilitar a chamada de suas funções (por exemplo, você pode usá-lo em um assistente para que os botões "próximo" e "anterior" não precisem de instruções de caso gigantescas para decidir o que fazer para acessar). a página seguinte / anterior.

Brian
fonte
0

Se você deseja expressões idiomáticas, aqui está:

//-- 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(); }
Florim
fonte
Se minha abertura falhar (digamos, adquirir um bloqueio de reentrada), o fechamento será chamado (digamos, liberar um bloqueio de reentrada, apesar da falha de abertura correspondente).
Tom Hawtin - tackline