Qu'est-ce que l'idiome «Exécuter autour»?

151

Quel est cet idiome «Exécuter autour» (ou similaire) dont j'ai entendu parler? Pourquoi pourrais-je l'utiliser et pourquoi pourrais-je ne pas vouloir l'utiliser?

Tom Hawtin - Tacle
la source
9
Je n'avais pas remarqué que c'était toi, tacle. Sinon, j'aurais pu être plus sarcastique dans ma réponse;)
Jon Skeet
1
C'est donc fondamentalement un aspect non? Sinon, en quoi est-ce différent?
Lucas

Réponses:

147

Fondamentalement, c'est le modèle dans lequel vous écrivez une méthode pour faire les choses qui sont toujours nécessaires, par exemple l'allocation et le nettoyage des ressources, et faites passer l'appelant "ce que nous voulons faire avec la ressource". Par exemple:

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

Le code d'appel n'a pas à se soucier du côté ouvert / nettoyage - il sera pris en charge par executeWithFile.

C'était franchement pénible en Java car les fermetures étaient si verbeuses, à commencer par Java 8, les expressions lambda peuvent être implémentées comme dans de nombreux autres langages (par exemple les expressions C # lambda, ou Groovy), et ce cas particulier est géré depuis Java 7 avec try-with-resourceset AutoClosablestreams.

Bien que "allouer et nettoyer" soit l'exemple typique donné, il existe de nombreux autres exemples possibles - gestion des transactions, journalisation, exécution de code avec plus de privilèges, etc. C'est fondamentalement un peu comme le modèle de méthode de modèle mais sans héritage.

Jon Skeet
la source
4
C'est déterministe. Les finaliseurs en Java ne sont pas appelés de manière déterministe. Aussi, comme je l'ai dit dans le dernier paragraphe, il n'est pas seulement utilisé pour l'allocation des ressources et le nettoyage. Il n'est peut-être pas nécessaire de créer un nouvel objet du tout. C'est généralement "l'initialisation et le démontage", mais ce n'est peut-être pas une allocation de ressources.
Jon Skeet
3
Donc, c'est comme en C où vous avez une fonction que vous passez dans un pointeur de fonction pour faire du travail?
Paul Tomblin
3
De plus, Jon, vous faites référence aux fermetures en Java - ce qu'il n'a toujours pas (sauf si je l'ai manqué). Ce que vous décrivez sont des classes internes anonymes - qui ne sont pas tout à fait la même chose. Le vrai support des fermetures (comme cela a été proposé - voir mon blog) simplifierait considérablement cette syntaxe.
philsquared le
8
@Phil: Je pense que c'est une question de degré. Les classes internes anonymes Java ont accès à leur environnement environnant dans un sens limité - donc, même si elles ne sont pas des fermetures «complètes», elles sont des fermetures «limitées», je dirais. Je voudrais certainement voir des fermetures appropriées à Java, bien que vérifiées (suite)
Jon Skeet
4
Java 7 a ajouté try-with-resource et Java 8 a ajouté des lambdas. Je sais que c'est une vieille question / réponse, mais je voulais le signaler à quiconque se pencherait sur cette question cinq ans et demi plus tard. Ces deux outils linguistiques aideront à résoudre le problème que ce modèle a été inventé pour résoudre.
45

L'idiome Execute Around est utilisé lorsque vous devez faire quelque chose comme ceci:

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

Afin d'éviter de répéter tout ce code redondant qui est toujours exécuté "autour" de vos tâches réelles, vous créeriez une classe qui s'en charge automatiquement:

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

Cet idiome déplace tout le code redondant compliqué en un seul endroit et laisse votre programme principal beaucoup plus lisible (et maintenable!)

Jetez un œil à cet article pour un exemple C # et cet article pour un exemple C ++.

e.James
la source
7

Une méthode Execute Around est l'endroit où vous passez du code arbitraire à une méthode, qui peut exécuter le code de configuration et / ou de démontage et exécuter votre code entre les deux.

Java n'est pas le langage dans lequel je choisirais de faire cela. Il est plus élégant de passer une fermeture (ou une expression lambda) comme argument. Bien que les objets soient sans doute équivalents à des fermetures .

Il me semble que la méthode Execute Around est un peu comme l' inversion de contrôle (injection de dépendance) que vous pouvez faire varier ad hoc, chaque fois que vous appelez la méthode.

Mais cela pourrait aussi être interprété comme un exemple de couplage de contrôle (indiquant à une méthode ce qu'il faut faire par son argument, littéralement dans ce cas).

Bill Karwin
la source
7

Je vois que vous avez une balise Java ici, je vais donc utiliser Java comme exemple même si le modèle n'est pas spécifique à la plate-forme.

L'idée est que parfois vous avez du code qui implique toujours le même passe-partout avant d'exécuter le code et après avoir exécuté le code. Un bon exemple est JDBC. Vous saisissez toujours une connexion et créez une instruction (ou une instruction préparée) avant d'exécuter la requête réelle et de traiter le jeu de résultats, puis vous effectuez toujours le même nettoyage standard à la fin - en fermant l'instruction et la connexion.

L'idée avec execute-around est qu'il vaut mieux que vous puissiez factoriser le code standard. Cela vous évite de taper, mais la raison est plus profonde. C'est le principe de ne pas se répéter (DRY) ici - vous isolez le code à un endroit, donc s'il y a un bogue ou si vous devez le changer, ou si vous voulez simplement le comprendre, tout est en un seul endroit.

La chose qui est un peu délicate avec ce type d'affacturage est que vous avez des références que les parties «avant» et «après» doivent voir. Dans l'exemple JDBC, cela inclurait la connexion et l'instruction (Prepared). Donc, pour gérer cela, vous "enveloppez" essentiellement votre code cible avec le code standard.

Vous connaissez peut-être certains cas courants en Java. L'un est les filtres de servlet. Un autre est AOP autour des conseils. Un troisième est les différentes classes xxxTemplate de Spring. Dans chaque cas, vous avez un objet wrapper dans lequel votre code "intéressant" (par exemple, la requête JDBC et le traitement de l'ensemble de résultats) est injecté. L'objet wrapper fait la partie "avant", invoque le code intéressant puis fait la partie "après".


la source
7

Voir aussi Code Sandwiches , qui examine cette construction dans de nombreux langages de programmation et propose des idées de recherche intéressantes. Concernant la question spécifique de savoir pourquoi on pourrait l'utiliser, le document ci-dessus offre quelques exemples concrets:

De telles situations surviennent chaque fois qu'un programme manipule des ressources partagées. Les API pour les verrous, les sockets, les fichiers ou les connexions à la base de données peuvent nécessiter qu'un programme ferme ou libère explicitement une ressource qu'il a précédemment acquise. Dans un langage sans garbage collection, le programmeur est responsable d'allouer de la mémoire avant son utilisation et de la libérer après son utilisation. En général, diverses tâches de programmation nécessitent qu'un programme effectue un changement, opère dans le contexte de ce changement, puis annule le changement. Nous appelons de telles situations des sandwiches de code.

Et ensuite:

Les sandwiches de code apparaissent dans de nombreuses situations de programmation. Plusieurs exemples courants concernent l'acquisition et la libération de ressources limitées, telles que les verrous, les descripteurs de fichiers ou les connexions de socket. Dans des cas plus généraux, tout changement temporaire d'état du programme peut nécessiter un sandwich de code. Par exemple, un programme basé sur l'interface graphique peut ignorer temporairement les entrées utilisateur, ou un noyau du système d'exploitation peut désactiver temporairement les interruptions matérielles. Le fait de ne pas restaurer l'état antérieur dans ces cas entraînera de graves bogues.

Le document n'explore pas pourquoi ne pas utiliser cet idiome, mais il décrit pourquoi il est facile de se tromper sans aide au niveau du langage:

Les sandwiches de code défectueux surviennent le plus souvent en présence d'exceptions et de leur flux de contrôle invisible associé. En effet, les fonctionnalités linguistiques spéciales pour gérer les sandwiches de code surviennent principalement dans les langues qui prennent en charge les exceptions.

Cependant, les exceptions ne sont pas la seule cause de sandwichs de code défectueux. Chaque fois que des modifications sont apportées au code du corps , de nouveaux chemins de contrôle peuvent apparaître qui contournent le code après . Dans le cas le plus simple, un responsable n'a qu'à ajouter une returninstruction au corps d' un sandwich pour introduire un nouveau défaut, ce qui peut conduire à des erreurs silencieuses. Lorsque le code corporel est volumineux et que l' avant et l' après sont largement séparés, de telles erreurs peuvent être difficiles à détecter visuellement.

Ben Liblit
la source
Bon point, azurefrag. J'ai révisé et élargi ma réponse pour qu'elle soit vraiment une réponse autonome à part entière. Merci d'avoir suggéré cela.
Ben Liblit
4

Je vais essayer d'expliquer, comme je le ferais à un enfant de quatre ans:

Exemple 1

Le Père Noël arrive en ville. Ses elfes codent ce qu'ils veulent derrière son dos, et à moins qu'ils ne changent les choses, ils deviennent un peu répétitifs:

  1. Obtenez du papier d'emballage
  2. Obtenez Super Nintendo .
  3. Emballe-le.

Ou ca:

  1. Obtenez du papier d'emballage
  2. Obtenez la poupée Barbie .
  3. Emballe-le.

.... ad nauseam un million de fois avec un million de cadeaux différents: notez que la seule chose différente est l'étape 2. Si la deuxième étape est la seule chose qui est différente, alors pourquoi le Père Noël duplique-t-il le code, c'est-à-dire pourquoi duplique-t-il les étapes 1 et 3 un million de fois? Un million de cadeaux signifie qu'il répète inutilement les étapes 1 et 3 un million de fois.

Exécuter autour permet de résoudre ce problème. et aide à éliminer le code. Les étapes 1 et 3 sont fondamentalement constantes, ce qui permet à l'étape 2 d'être la seule partie qui change.

Exemple # 2

Si vous ne comprenez toujours pas, voici un autre exemple: pensez à un sandwhich: le pain à l'extérieur est toujours le même, mais ce qu'il y a à l'intérieur change en fonction du type de sable que vous choisissez (.eg jambon, fromage, confiture, beurre d'arachide, etc.). Le pain est toujours à l'extérieur et vous n'avez pas besoin de le répéter un milliard de fois pour chaque type de sable que vous créez.

Maintenant, si vous lisez les explications ci-dessus, vous trouverez peut-être plus facile à comprendre. J'espère que cette explication vous a aidé.

BKSpurgeon
la source
+ pour l'imagination: D
Monsieur. Hérisson
3

Cela me rappelle le modèle de conception de la stratégie . Notez que le lien que j'ai indiqué inclut du code Java pour le modèle.

Évidemment, on pourrait effectuer "Execute Around" en faisant du code d'initialisation et de nettoyage et en passant simplement une stratégie, qui sera alors toujours enveloppée dans le code d'initialisation et de nettoyage.

Comme pour toute technique utilisée pour réduire la répétition du code, vous ne devriez pas l'utiliser avant d'avoir au moins 2 cas où vous en avez besoin, peut-être même 3 (selon le principe YAGNI). Gardez à l'esprit que la suppression de la répétition du code réduit la maintenance (moins de copies de code signifie moins de temps passé à copier les correctifs sur chaque copie), mais augmente également la maintenance (plus de code total). Ainsi, le coût de cette astuce est que vous ajoutez plus de code.

Ce type de technique est utile pour plus que l'initialisation et le nettoyage. C'est également utile lorsque vous souhaitez faciliter l'appel de vos fonctions (par exemple, vous pouvez l'utiliser dans un assistant afin que les boutons "suivant" et "précédent" n'aient pas besoin de déclarations de cas géantes pour décider quoi faire pour aller à la page suivante / précédente.

Brian
la source
0

Si vous voulez des idiomes groovy, voici:

//-- 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(); }
Florin
la source
Si mon ouverture échoue (par exemple l'acquisition d'un verrou réentrant), la fermeture est appelée (par exemple, la libération d'un verrou réentrant malgré l'échec de l'ouverture correspondante).
Tom Hawtin - tackline