Qual è il linguaggio "Execute Around"?


151

Cos'è questo linguaggio "Esegui intorno" (o simile) di cui ho sentito parlare? Perché dovrei usarlo e perché non dovrei volerlo usare?


9
Non avevo notato che eri tu, tack. Altrimenti avrei potuto essere più sarcastico nella mia risposta;)
Jon Skeet il

1
Quindi questo è fondamentalmente un aspetto giusto? In caso contrario, in cosa differisce?
Lucas,

Risposte:


147

Fondamentalmente è il modello in cui si scrive un metodo per fare le cose che sono sempre necessarie, ad esempio l'allocazione e la pulizia delle risorse, e far passare il chiamante in "ciò che vogliamo fare con la risorsa". Per esempio:

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

Il codice chiamante non deve preoccuparsi del lato aperto / clean-up, ma sarà curato da executeWithFile.

Questo è stato francamente doloroso in Java perché le chiusure erano così prolifiche, a partire da Java 8 le espressioni lambda possono essere implementate come in molte altre lingue (ad esempio espressioni lambda C # o Groovy), e questo caso speciale è gestito da Java 7 con try-with-resourcese AutoClosablestream.

Sebbene "allocare e ripulire" sia l'esempio tipico fornito, ci sono molti altri possibili esempi: gestione delle transazioni, registrazione, esecuzione di un codice con più privilegi, ecc. Fondamentalmente è un po 'come il modello di metodo del modello ma senza ereditarietà.


4
È deterministico. I finalizzatori in Java non sono chiamati in modo deterministico. Inoltre, come ho detto nell'ultimo paragrafo, non viene utilizzato solo per l'allocazione e la pulizia delle risorse. Potrebbe non essere necessario creare un nuovo oggetto. In genere si tratta di "inizializzazione e smontaggio", ma potrebbe non essere l'allocazione delle risorse.
Jon Skeet,

3
Quindi è come in C dove hai una funzione che passi in un puntatore a funzione per fare un po 'di lavoro?
Paul Tomblin,

3
Inoltre, Jon, ti riferisci alle chiusure in Java - che ancora non ha (a meno che non mi sia perso). Quello che descrivi sono classi interne anonime, che non sono esattamente la stessa cosa. Il vero supporto per le chiusure (come è stato proposto - vedi il mio blog) semplificherebbe notevolmente questa sintassi.
philsquared del

8
@Phil: penso che sia una questione di laurea. Le classi interne anonime Java hanno accesso al loro ambiente circostante in un senso limitato - quindi mentre non sono chiusure "complete" sono chiusure "limitate", direi. Mi piacerebbe sicuramente vedere chiusure adeguate in Java, anche se controllato (continua)
Jon Skeet,

4
Java 7 ha aggiunto try-with-resource e Java 8 ha aggiunto lambda. So che si tratta di una vecchia domanda / risposta, ma volevo segnalarlo a chiunque guardasse questa domanda cinque anni e mezzo dopo. Entrambi questi strumenti linguistici aiuteranno a risolvere il problema che questo schema è stato inventato per risolvere.

45

Il linguaggio Execute Around viene utilizzato quando ti ritrovi a dover fare qualcosa del genere:

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

Al fine di evitare di ripetere tutto questo codice ridondante che viene sempre eseguito "attorno" alle attività effettive, è necessario creare una classe che si occupi automaticamente di esso:

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

Questo idioma sposta tutto il complicato codice ridondante in un unico posto e lascia il tuo programma principale molto più leggibile (e gestibile!)

Dai un'occhiata a questo post per un esempio C # e questo articolo per un esempio C ++.


7

Un metodo Execute Around è il passaggio del codice arbitrario a un metodo, che può eseguire il codice di installazione e / o smontaggio ed eseguire il codice in mezzo.

Java non è il linguaggio in cui sceglierei di farlo. È più elegante passare una chiusura (o espressione lambda) come argomento. Sebbene gli oggetti siano probabilmente equivalenti alle chiusure .

Mi sembra che il metodo Execute Around sia un po 'come Inversion of Control (Dependency Injection) che puoi variare ad hoc, ogni volta che chiami il metodo.

Ma potrebbe anche essere interpretato come un esempio di Control Coupling (dire a un metodo cosa fare con il suo argomento, letteralmente in questo caso).


7

Vedo che hai un tag Java qui, quindi userò Java come esempio anche se il modello non è specifico della piattaforma.

L'idea è che a volte si dispone di codice che coinvolge sempre la stessa piastra di caldaia prima di eseguire il codice e dopo aver eseguito il codice. Un buon esempio è JDBC. Prendi sempre una connessione e crei un'istruzione (o un'istruzione preparata) prima di eseguire la query effettiva e di elaborare il set di risultati, quindi esegui sempre la stessa pulizia del boilerplate alla fine - chiudendo l'istruzione e la connessione.

L'idea con execute-around è che è meglio se si può fattorizzare il codice del boilerplate. Ciò ti consente di risparmiare un po 'di battitura, ma il motivo è più profondo. Qui è il principio di non ripeterti (DRY): isola il codice in una posizione, quindi se c'è un bug o devi cambiarlo, o vuoi solo capirlo, è tutto in un posto.

La cosa un po 'complicata con questo tipo di factoring fuori è che hai riferimenti che entrambe le parti "prima" e "dopo" devono vedere. Nell'esempio JDBC questo include l'istruzione Connection e (Prepared). Quindi, per gestire il fatto che essenzialmente "avvolgi" il tuo codice target con il codice boilerplate.

Potresti avere familiarità con alcuni casi comuni in Java. Uno è filtri servlet. Un altro è AOP in materia di consulenza. Un terzo sono le varie classi xxxTemplate in primavera. In ogni caso hai un oggetto wrapper in cui viene iniettato il tuo codice "interessante" (diciamo la query JDBC e l'elaborazione del set di risultati). L'oggetto wrapper esegue la parte "prima", richiama il codice interessante e quindi esegue la parte "dopo".


7

Vedi anche Code Sandwiches , che analizza questo costrutto in molti linguaggi di programmazione e offre alcune interessanti idee di ricerca. Per quanto riguarda la domanda specifica sul perché uno potrebbe usarlo, il documento sopra offre alcuni esempi concreti:

Tali situazioni sorgono ogni volta che un programma manipola le risorse condivise. Le API per blocchi, socket, file o connessioni al database potrebbero richiedere che un programma chiuda o rilasci esplicitamente una risorsa acquisita in precedenza. In una lingua senza garbage collection, il programmatore è responsabile dell'allocazione della memoria prima del suo utilizzo e del rilascio dopo il suo utilizzo. In generale, una serie di attività di programmazione richiede che un programma effettui una modifica, operi nel contesto di tale modifica e quindi annulli la modifica. Chiamiamo tali situazioni codici sandwich.

E più tardi:

I sandwich di codice compaiono in molte situazioni di programmazione. Diversi esempi comuni riguardano l'acquisizione e il rilascio di risorse scarse, come blocchi, descrittori di file o connessioni socket. In casi più generali, qualsiasi modifica temporanea dello stato del programma può richiedere un codice sandwich. Ad esempio, un programma basato su GUI può ignorare temporaneamente gli input dell'utente o un kernel del sistema operativo può disabilitare temporaneamente gli interrupt di processo. In questi casi il mancato ripristino dello stato precedente causerà gravi errori.

Il documento non esplora perché non usare questo linguaggio, ma descrive perché il linguaggio è facile sbagliare senza un aiuto a livello linguistico:

I sandwich di codice difettoso si presentano più frequentemente in presenza di eccezioni e del flusso di controllo invisibile associato. In effetti, le funzioni linguistiche speciali per gestire i sandwich di codice sorgono principalmente nelle lingue che supportano le eccezioni.

Tuttavia, le eccezioni non sono l'unica causa di sandwich di codice difettoso. Ogni volta che vengono apportate modifiche al codice del corpo , possono insorgere nuovi percorsi di controllo che bypassano il codice after . Nel caso più semplice, un manutentore deve solo aggiungere una returndichiarazione al corpo di un sandwich per introdurre un nuovo difetto, che può portare a errori silenziosi. Quando il codice corporeo è grande e prima e dopo sono ampiamente separati, tali errori possono essere difficili da rilevare visivamente.


Buon punto, azzurro. Ho rivisto e ampliato la mia risposta in modo che sia più una risposta autonoma a sé stante. Grazie per avermelo suggerito.
Ben Liblit,

4

Proverò a spiegare, come farei per un bambino di quattro anni:

Esempio 1

Babbo Natale sta arrivando in città. I suoi elfi codificano ciò che vogliono alle sue spalle, e se non cambiano le cose diventano un po 'ripetitivi:

  1. Ottieni carta da imballaggio
  2. Ottieni Super Nintendo .
  3. Avvolgilo.

O questo:

  1. Ottieni carta da imballaggio
  2. Ottieni Barbie Doll .
  3. Avvolgilo.

.... fino alla nausea un milione di volte con un milione di regali diversi: nota che l'unica cosa diversa è il passaggio 2. Se il passaggio due è l'unica cosa diversa, allora perché Babbo Natale sta duplicando il codice, cioè perché sta duplicando i passi 1 e 3 un milione di volte? Un milione di regali significa che sta inutilmente ripetendo i passaggi 1 e 3 un milione di volte.

Eseguire intorno aiuta a risolvere quel problema. e aiuta ad eliminare il codice. I passaggi 1 e 3 sono sostanzialmente costanti, consentendo al passaggio 2 di essere l'unica parte che cambia.

Esempio n. 2

Se ancora non lo capisci, ecco un altro esempio: pensa a un sandwhich: il pane all'esterno è sempre lo stesso, ma ciò che è all'interno cambia a seconda del tipo di sandwhich che scegli (prosciutto, formaggio, marmellata, burro di arachidi ecc.). Il pane è sempre all'esterno e non è necessario ripeterlo un miliardo di volte per ogni tipo di sabbia che stai creando.

Ora, se leggi le spiegazioni sopra, forse troverai più facile da capire. Spero che questa spiegazione ti abbia aiutato.


+ per l'immaginazione: D
Signore. Riccio

3

Questo mi ricorda il modello di progettazione della strategia . Si noti che il collegamento che ho indicato include il codice Java per il modello.

Ovviamente si potrebbe eseguire "Execute Around" eseguendo l'inizializzazione e il codice di pulizia e passando semplicemente una strategia, che verrà sempre racchiusa nel codice di inizializzazione e pulizia.

Come con qualsiasi tecnica utilizzata per ridurre la ripetizione del codice, non dovresti usarlo fino a quando non hai almeno 2 casi in cui ne hai bisogno, forse anche 3 (come il principio YAGNI). Tenere presente che la ripetizione della rimozione del codice riduce la manutenzione (un numero minore di copie del codice significa meno tempo impiegato a copiare le correzioni su ciascuna copia), ma aumenta anche la manutenzione (più codice totale). Pertanto, il costo di questo trucco è che stai aggiungendo più codice.

Questo tipo di tecnica è utile non solo per l'inizializzazione e la pulizia. È utile anche quando si desidera semplificare la chiamata delle proprie funzioni (ad esempio, è possibile utilizzarlo in una procedura guidata in modo che i pulsanti "successivo" e "precedente" non necessitino di dichiarazioni di casi giganti per decidere cosa fare per andare a la pagina successiva / precedente.


0

Se vuoi idiomi groovy, eccolo qui:

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

Se la mia apertura fallisce (diciamo acquisendo un blocco rientrante) viene chiamata la chiusura (diciamo rilasciando un blocco rientrante nonostante la mancata apertura corrispondente).
Tom Hawtin - tackline il
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.