java Qual è l'idioma "Execute Around"?




language-agnostic design-patterns (7)

Cos'è questo idioma "Execute Around" (o simile) di cui ho sentito parlare? Perché potrei usarlo e perché non dovrei usarlo?


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

Ovviamente si può eseguire "Execute Around" effettuando il codice di inizializzazione e pulitura e passando semplicemente una strategia, che sarà sempre racchiusa nel codice di inizializzazione e pulizia.

Come con qualsiasi tecnica usata per ridurre la ripetizione del codice, non dovresti usarla finché non hai almeno 2 casi in cui ne hai bisogno, forse anche 3 (secondo il principio YAGNI). Tieni presente che la ripetizione della rimozione del codice riduce la manutenzione (un numero inferiore di copie di codice significa meno tempo impiegato per copiare le correzioni su ogni copia), ma aumenta anche la manutenzione (più codice totale). Quindi, il costo di questo trucco è che stai aggiungendo altro codice.

Questo tipo di tecnica è utile per qualcosa di più della semplice inizializzazione e pulizia. È anche utile quando vuoi rendere più facile chiamare le tue funzioni (ad esempio puoi usarlo in una procedura guidata in modo che i pulsanti "successivo" e "precedente" non abbiano bisogno di istruzioni casistiche giganti per decidere cosa fare per andare a la pagina successiva / precedente.


L'idioma di 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.

Per evitare di ripetere tutto questo codice ridondante che viene sempre eseguito "attorno" alle tue attività effettive, devi 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 codice ridondante complicato in un unico posto e lascia il tuo programma principale molto più leggibile (e mantenibile!)

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


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

Esempio 1

Babbo Natale sta arrivando in città. I suoi elfi codificano ciò che vogliono dietro la sua schiena, ea meno che non cambino le cose diventano un po 'ripetitive:

  1. Prendi carta da pacchi
  2. Ottieni Super Nintendo .
  3. Avvolgilo.

O questo:

  1. Prendi carta da pacchi
  2. Ottieni Barbie Doll .
  3. Avvolgilo.

.... ad nauseam un milione di volte con un milione di regali diversi: nota che l'unica cosa diversa è il passo 2. Se il secondo passo è l'unica cosa diversa, allora perché Babbo Natale duplica il codice, cioè perché duplica i passaggi 1 e 3 un milione di volte? Un milione di regali significa che sta ripetendo inutilmente i passaggi 1 e 3 un milione di volte.

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

Esempio # 2

Se ancora non 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 sabbia 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 si sta creando.

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


Fondamentalmente è lo schema in cui si scrive un metodo per fare cose che sono sempre richieste, ad esempio l'allocazione delle risorse e la pulizia, e far passare il chiamante "cosa 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 ha bisogno di preoccuparsi del lato open / clean-up - sarà curato da executeWithFile .

Questo è stato francamente doloroso in Java perché le chiusure erano così verbose, a partire da espressioni lambda di Java 8 che possono essere implementate come in molti altri linguaggi (es. Espressioni lambda C #, o Groovy), e questo caso speciale viene gestito da Java 7 con try-with-resources e flussi AutoClosable .

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


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

Vedo che hai un tag Java qui, quindi utilizzerò Java come esempio anche se il pattern non è specifico per la piattaforma.

L'idea è che a volte hai un codice che coinvolge sempre lo stesso boilerplate prima di eseguire il codice e dopo aver eseguito il codice. Un buon esempio è JDBC. Si prende sempre una connessione e si crea un'istruzione (o un'istruzione preparata) prima di eseguire la query effettiva e l'elaborazione del set di risultati, e quindi si esegue sempre la stessa pulizia di boilerplate alla fine, chiudendo la dichiarazione e la connessione.

L'idea con execute-around è che è meglio se riesci a calcolare il codice boilerplate. Questo ti risparmia un po 'di battitura, ma la ragione è più profonda. È il principio di non ripetere te stesso (ASCIUTTO) qui: puoi isolare il codice in una posizione, quindi se c'è un bug o devi cambiarlo o vuoi solo capirlo, è tutto in un unico posto.

La cosa che è un po 'complicata con questo tipo di factoring-out è che tu hai dei riferimenti che entrambe le parti "prima" e "dopo" devono vedere. Nell'esempio JDBC questo includerebbe la connessione e l'istruzione (preparata). Quindi per gestire che essenzialmente "avvolgere" il codice di destinazione con il codice boilerplate.

Potresti avere familiarità con alcuni casi comuni in Java. Uno è filtri servlet. Un altro è AOP in merito ai consigli. Un terzo sono le varie classi xxxTemplate in primavera. In ogni caso si dispone di un oggetto wrapper in cui viene iniettato il codice "interessante" (ad esempio la query JDBC e l'elaborazione del set di risultati). L'oggetto wrapper esegue la parte "before", invoca il codice interessante e quindi esegue la parte "after".


Vedi anche Code Sandwiches , che esamina questo costrutto attraverso molti linguaggi di programmazione e offre alcune interessanti idee di ricerca. Riguardo alla domanda specifica sul perché si possa usare, il documento di cui sopra offre alcuni esempi concreti:

Tali situazioni sorgono ogni volta che un programma manipola risorse condivise. Le API per i blocchi, i socket, i file o le connessioni al database possono richiedere che un programma chiuda o rilasci esplicitamente una risorsa che ha precedentemente acquisito. In una lingua senza garbage collection, il programmatore è responsabile dell'allocazione della memoria prima del suo utilizzo e il suo rilascio dopo l'uso. In generale, una serie di attività di programmazione richiede che un programma esegua un cambiamento, operi nel contesto di tale modifica e annulli la modifica. Chiamiamo tali panini di codice di situazioni.

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 potrebbe richiedere un codice sandwich. Ad esempio, un programma basato su GUI può temporaneamente ignorare gli input dell'utente oppure un kernel del sistema operativo può disabilitare temporaneamente gli interrupt hardware. Il mancato ripristino dello stato precedente in questi casi causerà gravi problemi.

Il documento non esplora perché non usare questo idioma, ma descrive perché l'idioma è facile da sbagliare senza un aiuto a livello di linguaggio:

I sandwich di codici difettosi si presentano più frequentemente in presenza di eccezioni e del loro flusso di controllo invisibile associato. In effetti, le caratteristiche 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 panini di codici difettosi. Ogni volta che vengono apportate modifiche al codice del corpo , possono sorgere nuovi percorsi di controllo che bypassano il codice successivo . Nel caso più semplice, un maintainer deve solo aggiungere un'istruzione return al corpo di un sandwich per introdurre un nuovo difetto, che può portare a errori muti. Quando il codice del corpo è grande e prima e dopo sono ampiamente separati, tali errori possono essere difficili da rilevare visivamente.





idioms