Cos'è questo linguaggio "Esegui intorno" (o simile) di cui ho sentito parlare? Perché dovrei usarlo e perché non dovrei volerlo usare?
Cos'è questo linguaggio "Esegui intorno" (o simile) di cui ho sentito parlare? Perché dovrei usarlo e perché non dovrei volerlo usare?
Risposte:
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-resources
e AutoClosable
stream.
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à.
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 ++.
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).
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".
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
return
dichiarazione 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.
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:
O questo:
.... 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.
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.
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(); }