Was ist die "Execute Around" -Sprache?

151

Was ist diese "Execute Around" -Sprache (oder ähnliches), von der ich gehört habe? Warum könnte ich es benutzen und warum könnte ich es nicht benutzen wollen?

Tom Hawtin - Tackline
quelle
9
Ich hatte nicht bemerkt, dass du es bist. Sonst wäre ich vielleicht sarkastischer in meiner Antwort gewesen;)
Jon Skeet
1
Das ist also im Grunde ein Aspekt, oder? Wenn nicht, wie unterscheidet es sich?
Lucas

Antworten:

147

Grundsätzlich ist es das Muster, in dem Sie eine Methode schreiben, um Dinge zu tun, die immer erforderlich sind, z. B. Ressourcenzuweisung und Bereinigung, und den Anrufer dazu bringen, "was wir mit der Ressource tun wollen" zu übergeben. Beispielsweise:

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

Der aufrufende Code muss sich nicht um die Open / Cleanup-Seite kümmern - er wird von erledigt executeWithFile.

Dies war in Java ehrlich gesagt schmerzhaft, da Schließungen so wortreich waren, dass Java 8-Lambda-Ausdrücke wie in vielen anderen Sprachen (z. B. C # -Lambda-Ausdrücke oder Groovy) implementiert werden können. Dieser Sonderfall wird seit Java 7 mit try-with-resourcesund AutoClosableStreams behandelt.

Obwohl "Zuweisen und Bereinigen" das typische Beispiel ist, gibt es viele andere mögliche Beispiele - Transaktionsbehandlung, Protokollierung, Ausführen von Code mit mehr Berechtigungen usw. Es ähnelt im Grunde dem Muster der Vorlagenmethode, ist jedoch nicht vererbt .

Jon Skeet
quelle
4
Es ist deterministisch. Finalizer in Java werden nicht deterministisch genannt. Wie ich bereits im letzten Absatz sagte, wird es nicht nur zur Ressourcenzuweisung und Bereinigung verwendet. Möglicherweise muss überhaupt kein neues Objekt erstellt werden. Es ist im Allgemeinen "Initialisierung und Abbau", aber das ist möglicherweise keine Ressourcenzuweisung.
Jon Skeet
3
Es ist also wie in C, wo Sie eine Funktion haben, die Sie in einem Funktionszeiger übergeben, um etwas Arbeit zu erledigen?
Paul Tomblin
3
Außerdem, Jon, beziehen Sie sich auf Schließungen in Java - die es immer noch nicht gibt (es sei denn, ich habe es verpasst). Was Sie beschreiben, sind anonyme innere Klassen - die nicht ganz dasselbe sind. Echte Schließungsunterstützung (wie vorgeschlagen - siehe mein Blog) würde diese Syntax erheblich vereinfachen.
Philsquared
8
@Phil: Ich denke, es ist eine Frage des Grades. Anonyme Java-Innenklassen haben in begrenztem Umfang Zugriff auf ihre Umgebung. Obwohl sie keine "vollständigen" Abschlüsse sind, sind sie "begrenzte" Abschlüsse, würde ich sagen. Ich würde sicherlich gerne die richtigen Schließungen in Java sehen, obwohl überprüft (Fortsetzung)
Jon Skeet
4
Java 7 fügte Try-with-Resource hinzu und Java 8 fügte Lambdas hinzu. Ich weiß, dass dies eine alte Frage / Antwort ist, aber ich wollte alle, die sich diese Frage fünfeinhalb Jahre später ansehen, darauf hinweisen. Beide Sprachwerkzeuge helfen bei der Lösung des Problems, für dessen Behebung dieses Muster erfunden wurde.
45

Die Execute Around-Redewendung wird verwendet, wenn Sie Folgendes tun müssen:

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

Um zu vermeiden, dass all dieser redundante Code wiederholt wird, der immer "um" Ihre eigentlichen Aufgaben ausgeführt wird, würden Sie eine Klasse erstellen, die sich automatisch darum kümmert:

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

Diese Redewendung verschiebt den gesamten komplizierten redundanten Code an einen Ort und macht Ihr Hauptprogramm viel lesbarer (und wartbarer!).

In diesem Beitrag finden Sie ein C # -Beispiel und in diesem Artikel ein C ++ - Beispiel.

James
quelle
7

Bei einer Execute Around-Methode übergeben Sie beliebigen Code an eine Methode, die Setup- und / oder Teardown-Code ausführen und Ihren Code dazwischen ausführen kann.

Java ist nicht die Sprache, in der ich dies wählen würde. Es ist eleganter, einen Abschluss (oder Lambda-Ausdruck) als Argument zu übergeben. Obwohl Objekte wohl gleichbedeutend mit Verschlüssen sind .

Es scheint mir, dass die Execute Around-Methode einer Art Inversion of Control (Abhängigkeitsinjektion) ähnelt , die Sie bei jedem Aufruf der Methode ad hoc variieren können.

Es könnte aber auch als Beispiel für eine Steuerkopplung interpretiert werden (indem man einer Methode anhand ihres Arguments sagt, was in diesem Fall wörtlich zu tun ist).

Bill Karwin
quelle
7

Ich sehe, dass Sie hier ein Java-Tag haben, daher verwende ich Java als Beispiel, obwohl das Muster nicht plattformspezifisch ist.

Die Idee ist, dass Sie manchmal Code haben, der immer dieselbe Boilerplate enthält, bevor Sie den Code ausführen und nachdem Sie den Code ausgeführt haben. Ein gutes Beispiel ist JDBC. Sie greifen immer zu einer Verbindung und erstellen eine Anweisung (oder eine vorbereitete Anweisung), bevor Sie die eigentliche Abfrage ausführen und die Ergebnismenge verarbeiten. Am Ende führen Sie immer die gleiche Bereinigung der Boilerplate durch, indem Sie die Anweisung und die Verbindung schließen.

Die Idee mit Execute-Around ist, dass es besser ist, wenn Sie den Boilerplate-Code herausrechnen können. Das erspart Ihnen das Tippen, aber der Grund liegt tiefer. Hier gilt das DRY-Prinzip (Don't-Repeat-Yourself): Sie isolieren den Code an einem Ort. Wenn also ein Fehler vorliegt oder Sie ihn ändern müssen oder ihn nur verstehen möchten, ist alles an einem Ort.

Die Sache, die bei dieser Art des Factorings etwas schwierig ist, ist, dass Sie Referenzen haben, die sowohl die "Vorher" - als auch "Nachher" -Teile sehen müssen. Im JDBC-Beispiel würde dies die Verbindung und die (vorbereitete) Anweisung enthalten. Um dies zu handhaben, "verpacken" Sie Ihren Zielcode im Wesentlichen mit dem Boilerplate-Code.

Möglicherweise kennen Sie einige häufige Fälle in Java. Eines sind Servlet-Filter. Ein anderer ist AOP um Beratung. Ein dritter sind die verschiedenen xxxTemplate-Klassen im Frühjahr. In jedem Fall haben Sie ein Wrapper-Objekt, in das Ihr "interessanter" Code (z. B. die JDBC-Abfrage und die Verarbeitung der Ergebnismenge) eingefügt wird. Das Wrapper-Objekt führt den "Vorher" -Teil aus, ruft den interessanten Code auf und führt dann den "Nachher" -Teil aus.


quelle
7

Siehe auch Code Sandwiches , das dieses Konstrukt in vielen Programmiersprachen untersucht und einige interessante Forschungsideen bietet. In Bezug auf die spezifische Frage, warum man es verwenden könnte, bietet das obige Papier einige konkrete Beispiele:

Solche Situationen treten immer dann auf, wenn ein Programm gemeinsam genutzte Ressourcen manipuliert. Für APIs für Sperren, Sockets, Dateien oder Datenbankverbindungen muss ein Programm möglicherweise eine zuvor erworbene Ressource explizit schließen oder freigeben. In einer Sprache ohne Speicherbereinigung ist der Programmierer dafür verantwortlich, Speicher vor seiner Verwendung zuzuweisen und nach seiner Verwendung freizugeben. Im Allgemeinen erfordern verschiedene Programmieraufgaben, dass ein Programm eine Änderung vornimmt, im Kontext dieser Änderung arbeitet und die Änderung dann rückgängig macht. Wir nennen solche Situationen Code-Sandwiches.

Und später:

Code-Sandwiches treten in vielen Programmiersituationen auf. Einige gängige Beispiele beziehen sich auf den Erwerb und die Freigabe knapper Ressourcen wie Sperren, Dateideskriptoren oder Socket-Verbindungen. In allgemeineren Fällen kann für jede vorübergehende Änderung des Programmstatus ein Code-Sandwich erforderlich sein. Beispielsweise kann ein GUI-basiertes Programm Benutzereingaben vorübergehend ignorieren oder ein Betriebssystemkern kann Hardware-Interrupts vorübergehend deaktivieren. Wenn in diesen Fällen der frühere Status nicht wiederhergestellt wird, treten schwerwiegende Fehler auf.

In diesem Artikel wird nicht untersucht, warum dieses Idiom nicht verwendet werden soll, es wird jedoch beschrieben, warum das Idiom ohne Hilfe auf Sprachebene leicht falsch zu verstehen ist:

Fehlerhafte Code-Sandwiches treten am häufigsten bei Ausnahmen und dem damit verbundenen unsichtbaren Kontrollfluss auf. In der Tat treten spezielle Sprachfunktionen zum Verwalten von Code-Sandwiches hauptsächlich in Sprachen auf, die Ausnahmen unterstützen.

Ausnahmen sind jedoch nicht die einzige Ursache für fehlerhafte Code-Sandwiches. Immer wenn Änderungen am Body- Code vorgenommen werden, können neue Steuerpfade entstehen, die den After- Code umgehen . Im einfachsten Fall muss ein Betreuer nur eine returnAussage zum Körper eines Sandwichs hinzufügen , um einen neuen Defekt einzuführen, der zu stillen Fehlern führen kann. Wenn der Body- Code groß ist und vorher und nachher weit voneinander entfernt sind, können solche Fehler visuell schwer zu erkennen sein.

Ben Liblit
quelle
Guter Punkt, Azurefrag. Ich habe meine Antwort überarbeitet und erweitert, sodass sie eigentlich eher eine eigenständige Antwort ist. Vielen Dank, dass Sie dies vorgeschlagen haben.
Ben Liblit
4

Ich werde versuchen zu erklären, wie ich es einem Vierjährigen tun würde:

Beispiel 1

Der Weihnachtsmann kommt in die Stadt. Seine Elfen codieren hinter seinem Rücken, was sie wollen, und wenn sie sich nicht ändern, wiederholen sich die Dinge ein wenig:

  1. Holen Sie sich Geschenkpapier
  2. Holen Sie sich Super Nintendo .
  3. Wickeln Sie es ein.

Oder dieses:

  1. Holen Sie sich Geschenkpapier
  2. Holen Sie sich Barbie Doll .
  3. Wickeln Sie es ein.

.... ad nauseam millionenfach mit millionen verschiedenen Geschenken: Beachten Sie, dass nur Schritt 2 anders ist. Wenn Schritt zwei das einzige ist, was anders ist, warum dupliziert der Weihnachtsmann den Code, dh warum dupliziert er Schritte? 1 und 3 eine Million Mal? Eine Million Geschenke bedeutet, dass er die Schritte 1 und 3 unnötig millionenfach wiederholt.

Execute around hilft, dieses Problem zu lösen. und hilft, Code zu beseitigen. Die Schritte 1 und 3 sind grundsätzlich konstant, sodass Schritt 2 der einzige Teil ist, der sich ändert.

Beispiel 2

Wenn Sie es immer noch nicht bekommen, ist hier ein anderes Beispiel: Stellen Sie sich ein Sandwich vor: Das Brot auf der Außenseite ist immer das gleiche, aber was auf der Innenseite ist, hängt von der Art des Sandes ab, das Sie wählen (z. B. Schinken, Käse, Marmelade, Erdnussbutter usw.). Brot ist immer außen und Sie müssen es nicht milliardenfach für jede Art von Sand wiederholen, die Sie herstellen.

Wenn Sie nun die obigen Erklärungen lesen, werden Sie es vielleicht leichter verstehen. Ich hoffe, diese Erklärung hat Ihnen geholfen.

BKSpurgeon
quelle
+ für die Phantasie: D
Sir. Igel
3

Dies erinnert mich an das Strategie-Design-Muster . Beachten Sie, dass der Link, auf den ich verwiesen habe, Java-Code für das Muster enthält.

Offensichtlich könnte man "Execute Around" durchführen, indem man Initialisierungs- und Bereinigungscode erstellt und einfach eine Strategie übergibt, die dann immer in Initialisierungs- und Bereinigungscode eingeschlossen wird.

Wie bei jeder Technik zur Reduzierung der Code-Wiederholung sollten Sie sie erst verwenden, wenn Sie mindestens zwei Fälle haben, in denen Sie sie benötigen, vielleicht sogar drei (nach dem YAGNI-Prinzip). Beachten Sie, dass die Wiederholung des Entfernens von Code die Wartung reduziert (weniger Codekopien bedeuten weniger Zeit für das Kopieren von Fixes für jede Kopie), aber auch die Wartung erhöht (mehr Gesamtcode). Die Kosten für diesen Trick bestehen also darin, dass Sie mehr Code hinzufügen.

Diese Art von Technik ist nicht nur für die Initialisierung und Bereinigung nützlich. Dies ist auch gut geeignet, wenn Sie das Aufrufen Ihrer Funktionen vereinfachen möchten (z. B. wenn Sie es in einem Assistenten verwenden möchten, sodass für die Schaltflächen "Weiter" und "Zurück" keine riesigen Groß- und Kleinschreibung erforderlich sind, um zu entscheiden, was zu tun ist die nächste / vorherige Seite.

Brian
quelle
0

Wenn Sie groovige Redewendungen wollen, hier ist es:

//-- 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(); }
Gulden
quelle
Wenn mein Öffnen fehlschlägt (z. B. ein Wiedereintrittsschloss erwerben), wird das Schließen aufgerufen (z. B. ein Wiedereintrittsschloss freigeben, obwohl das passende Öffnen fehlschlägt).
Tom Hawtin - Tackline