web-dev-qa-db-fra.com

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

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

147

Fondamentalement, c'est le modèle où vous écrivez une méthode pour faire des choses qui sont toujours nécessaires, par exemple l'allocation des ressources et le nettoyage, et faire 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 appelant n'a pas besoin de s'inquiéter du côté ouverture/nettoyage - il sera pris en charge par executeWithFile.

Cela a été franchement douloureux dans Java parce que les fermetures étaient si verbeuses, à commencer par Java 8 expressions lambda peuvent être implémentées comme dans de nombreux autres langages (par exemple les expressions lambda C #, ou Groovy), et ce cas spécial est géré depuis Java 7 avec try-with-resources et AutoClosable flux.

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 un peu comme le modèle de méthode de modèle = mais sans héritage.

140
Jon Skeet

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 devez créer une classe qui s'en occupe 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 oeil à cet article pour un exemple C # et cet article pour un exemple C++.

44
e.James

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

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

Il me semble que la méthode Execute Around est un peu comme Inversion of Control (Dependency Injection) que vous pouvez varier ad hoc, chaque fois que vous appelez la méthode.

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

7
Bill Karwin

Je vois que vous avez une balise Java ici, donc je vais 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 l'exécution est qu'il est préférable de factoriser le code passe-partout. 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 juste le comprendre, tout est au même endroit.

Ce qui est un peu délicat avec ce type d'affacturage, c'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 (préparée). Donc, pour gérer cela, vous "enveloppez" essentiellement votre code cible avec le code passe-partout.

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

7
Willie Wheeler

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 de 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, une variété de tâches de programmation nécessitent un programme pour effectuer un changement, opérer dans le contexte de ce changement, puis annuler le changement. Nous appelons de telles situations des sandwichs de code.

Et ensuite:

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

Le document n'explore pas pourquoi pas pour utiliser cet idiome, mais il décrit pourquoi l'idiome est facile à se tromper sans aide au niveau de la langue:

Les sandwichs 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 de langage spéciales pour gérer les sandwichs de code surviennent principalement dans les langages 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 après code. Dans le cas le plus simple, un responsable n'a qu'à ajouter une instruction return au corps d'un sandwich pour introduire un nouveau défaut, ce qui peut conduire à erreurs silencieuses. Lorsque le code du corps est grand et avant et après que soient largement séparés, de telles erreurs peuvent être difficiles à détecter visuellement.

7
Ben Liblit

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 tout ce qu'ils veulent derrière son dos, et à moins qu'ils ne changent, les choses deviennent un peu répétitives:

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

Ou ca:

  1. Obtenez du papier d'emballage
  2. Obtenez Barbie Doll.
  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 différente, alors pourquoi le Père Noël duplique-t-il le code, c'est-à-dire pourquoi est-il en train de dupliquer 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, permettant à l'étape 2 d'être la seule partie qui change.

Exemple # 2

Si vous ne l'obtenez toujours pas, voici un autre exemple: pensez à un sable qui: le pain à l'extérieur est toujours le même, mais ce qui à l'intérieur change en fonction du type de sable que vous choisissez (jambon .eg, 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é.

3
BKSpurgeon

Cela me rappelle le modèle de conception de stratégie . Notez que le lien que j'ai pointé inclut Java code 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 ensuite toujours enveloppée dans du code d'initialisation et de nettoyage.

Comme pour toute technique utilisée pour réduire la répétition du code, vous ne devez pas l'utiliser avant d'avoir au moins 2 cas où vous en avez besoin, peut-être même 3 (à la manière du principe YAGNI). Gardez à l'esprit que la suppression de la répétition de 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. Il 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 d'instructions de cas géantes pour décider quoi faire pour aller à la page suivante/précédente.

3
Brian

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