Co to jest idiom „Execute Around”?

151

Co to za idiom „Execute Around” (lub podobny), o którym słyszałem? Dlaczego mogę go używać i dlaczego nie chcę go używać?

Tom Hawtin - haczyk
źródło
9
Nie zauważyłem, że to ty, halsie. Inaczej mógłbym być bardziej sarkastyczny w mojej odpowiedzi;)
Jon Skeet
1
Więc to jest w zasadzie aspekt, prawda? Jeśli nie, czym się różni?
Lucas

Odpowiedzi:

147

Zasadniczo jest to wzorzec, w którym piszesz metodę wykonującą rzeczy, które są zawsze wymagane, np. Alokacja zasobów i czyszczenie, oraz sprawiasz, że obiekt wywołujący przekazuje „co chcemy zrobić z zasobem”. Na przykład:

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

Kod wywoławczy nie musi martwić się o stronę otwierania / czyszczenia - zajmie się nim executeWithFile.

To było szczerze bolesne w Javie, ponieważ zamknięcia były tak rozwlekłe, że począwszy od Java 8 wyrażenia lambda można zaimplementować tak jak w wielu innych językach (np. Wyrażenia lambda w C # lub Groovy), a ten specjalny przypadek jest obsługiwany od wersji Java 7 z try-with-resourcesiAutoClosable strumieniami .

Chociaż typowym podanym przykładem jest „przydzielanie i porządkowanie”, istnieje wiele innych możliwych przykładów - obsługa transakcji, logowanie, wykonywanie kodu z większymi uprawnieniami itp. Jest to w zasadzie trochę podobne do wzorca metody szablonu, ale bez dziedziczenia.

Jon Skeet
źródło
4
To jest deterministyczne. Finalizatory w Javie nie są wywoływane deterministycznie. Również, jak powiedziałem w ostatnim akapicie, jest używany nie tylko do alokacji zasobów i czyszczenia. Może wcale nie być konieczne tworzenie nowego obiektu. Zwykle jest to „inicjalizacja i usuwanie”, ale może to nie być alokacja zasobów.
Jon Skeet
3
Więc to jest tak, jak w C, gdzie masz funkcję, którą przekazujesz we wskaźniku funkcji, aby wykonać jakąś pracę?
Paul Tomblin
3
Jon, odnosisz się również do zamknięć w Javie - których nadal nie ma (chyba że to przegapiłem). To, co opisujesz, to anonimowe klasy wewnętrzne - które nie są tym samym. Obsługa prawdziwych domknięć (jak zaproponowano - patrz mój blog) znacznie uprościłaby tę składnię.
philsquared
8
@Phil: Myślę, że to kwestia stopnia. Anonimowe klasy wewnętrzne w Javie mają dostęp do otaczającego ich środowiska w ograniczonym sensie - więc chociaż nie są „pełnymi” zamknięciami, to powiedziałbym, że są „ograniczonymi” zamknięciami. Z pewnością chciałbym zobaczyć poprawne zamknięcia w Javie, chociaż zaznaczone (kontynuacja)
Jon Skeet,
4
Java 7 dodała try-with-resource, a Java 8 dodała lambdy. Wiem, że to stare pytanie / odpowiedź, ale chciałem zwrócić na to uwagę każdemu, kto przyjrzy się temu pytaniu pięć i pół roku później. Oba te narzędzia językowe pomogą rozwiązać problem, który ten wzorzec został wymyślony.
45

Idiom Execute Around jest używany, gdy musisz zrobić coś takiego:

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

Aby uniknąć powtarzania całego tego nadmiarowego kodu, który jest zawsze wykonywany „wokół” Twoich rzeczywistych zadań, możesz utworzyć klasę, która zajmie się tym automatycznie:

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

Ten idiom przenosi cały skomplikowany, nadmiarowy kod w jedno miejsce, pozostawiając główny program znacznie bardziej czytelnym (i łatwiejszym w utrzymaniu!)

Zapoznaj się z tym postem, aby zapoznać się z przykładem w języku C #, i tym artykułem, aby zapoznać się z przykładem w języku C ++.

e.James
źródło
7

Execute Around Method jest tam, gdzie przechodzą dowolny kod do metody, która może wykonywać konfiguracji i / lub przerywaniem kod i wykonanie kodu pomiędzy.

Java nie jest językiem, w którym bym to zrobił. Bardziej stylowe jest przekazanie zamknięcia (lub wyrażenia lambda) jako argumentu. Chociaż obiekty są prawdopodobnie równoważne z zamknięciami .

Wydaje mi się, że metoda Execute Around jest czymś w rodzaju odwrócenia kontroli (iniekcji zależności), którą można zmieniać ad hoc, za każdym razem, gdy wywołujesz metodę.

Ale można to również zinterpretować jako przykład sprzężenia sterującego (mówienie metodzie, co ma zrobić, używając argumentu, dosłownie w tym przypadku).

Bill Karwin
źródło
7

Widzę, że masz tutaj tag Java, więc użyję Java jako przykładu, mimo że wzorzec nie jest specyficzny dla platformy.

Chodzi o to, że czasami masz kod, który zawsze zawiera ten sam schemat standardowy przed uruchomieniem kodu i po jego uruchomieniu. Dobrym przykładem jest JDBC. Zawsze przechwytujesz połączenie i tworzysz instrukcję (lub przygotowaną instrukcję) przed uruchomieniem rzeczywistego zapytania i przetworzeniem zestawu wyników, a następnie zawsze wykonujesz to samo standardowe czyszczenie na końcu - zamykanie instrukcji i połączenia.

Pomysł z execute-around polega na tym, że lepiej jest rozliczyć kod standardowy. Oszczędza to trochę pisania, ale przyczyna jest głębsza. Jest to tutaj zasada „nie powtarzaj się” (DRY) - izolujesz kod w jednym miejscu, więc jeśli jest błąd lub musisz go zmienić, lub po prostu chcesz go zrozumieć, wszystko jest w jednym miejscu.

Rzecz, która jest trochę skomplikowana w przypadku tego rodzaju faktoryzacji, to fakt, że istnieją odniesienia, które muszą być widoczne zarówno w części „przed”, jak i „po”. W przykładzie JDBC obejmowałoby to instrukcję Connection and (Prepared). Aby sobie z tym poradzić, zasadniczo „opakowujesz” swój kod docelowy kodem standardowym.

Być może znasz niektóre typowe przypadki w Javie. Jednym z nich są filtry serwletów. Innym jest AOP dotyczący porad. Trzecia to różne klasy xxxTemplate na wiosnę. W każdym przypadku masz jakiś obiekt opakowujący, do którego jest wstrzykiwany twój „interesujący” kod (powiedzmy, zapytanie JDBC i przetwarzanie zestawu wyników). Obiekt otoki wykonuje część „przed”, wywołuje interesujący kod, a następnie wykonuje część „po”.


źródło
7

Zobacz także Code Sandwiches , w którym omówiono tę konstrukcję w wielu językach programowania i przedstawiono kilka interesujących pomysłów badawczych. Jeśli chodzi o konkretne pytanie, dlaczego można go używać, powyższy artykuł podaje kilka konkretnych przykładów:

Takie sytuacje pojawiają się, gdy program manipuluje współdzielonymi zasobami. Interfejsy API dla blokad, gniazd, plików lub połączeń z bazami danych mogą wymagać od programu jawnego zamknięcia lub zwolnienia zasobu, który wcześniej uzyskał. W języku bez czyszczenia pamięci programista jest odpowiedzialny za przydzielanie pamięci przed jej użyciem i zwalnianie jej po użyciu. Ogólnie rzecz biorąc, różnorodne zadania programistyczne wymagają od programu wprowadzenia zmiany, działania w kontekście tej zmiany, a następnie cofnięcia zmiany. Takie sytuacje nazywamy kanapkami.

I później:

Kanapki z kodem pojawiają się w wielu sytuacjach programistycznych. Kilka typowych przykładów odnosi się do pozyskiwania i zwalniania ograniczonych zasobów, takich jak blokady, deskryptory plików lub połączenia gniazd. W bardziej ogólnych przypadkach każda tymczasowa zmiana stanu programu może wymagać wprowadzenia kanapki z kodem. Na przykład program oparty na graficznym interfejsie użytkownika może tymczasowo ignorować dane wejściowe użytkownika lub jądro systemu operacyjnego może tymczasowo wyłączać przerwania sprzętowe. Brak przywrócenia wcześniejszego stanu w takich przypadkach spowoduje poważne błędy.

Artykuł nie wyjaśnia, dlaczego nie należy używać tego idiomu, ale opisuje, dlaczego łatwo jest się pomylić w idiomie bez pomocy na poziomie języka:

Wadliwe kanapki kodu pojawiają się najczęściej w obecności wyjątków i związanego z nimi niewidocznego przepływu sterowania. Rzeczywiście, specjalne funkcje językowe do zarządzania warstwami kodu pojawiają się głównie w językach obsługujących wyjątki.

Jednak wyjątki nie są jedyną przyczyną wadliwych kanapek kodu. Za każdym razem zmiany są wprowadzane do ciała kodu, nowe ścieżki kontroli może wynikać, że obejście po kodzie. W najprostszym przypadku opiekun musi tylko dodać returnoświadczenie do korpusu kanapki, aby wprowadzić nową usterkę, która może prowadzić do cichych błędów. Gdy ciało kod jest duża i przed i po są szeroko rozdzielone, takie błędy mogą być trudne do wykrycia wizualnie.

Ben Liblit
źródło
Słuszna uwaga, azurefrag. Poprawiłem i rozszerzyłem moją odpowiedź, tak aby była bardziej samodzielną odpowiedzią samą w sobie. Dzięki za zasugerowanie tego.
Ben Liblit,
4

Spróbuję wyjaśnić, jakbym to zrobił czterolatkowi:

Przykład 1

Święty Mikołaj przyjeżdża do miasta. Jego elfy kodują za jego plecami, co chcą, i jeśli nie zmienią rzeczy, staną się trochę powtarzalne:

  1. Zdobądź papier do pakowania
  2. Zdobądź Super Nintendo .
  3. Owiń to.

Albo to:

  1. Zdobądź papier do pakowania
  2. Zdobądź lalkę Barbie .
  3. Owiń to.

… aż do mdłości milion razy z milionem różnych prezentów: zauważ, że jedyną różnicą jest krok 2. Jeśli krok drugi jest jedyną różnicą, to dlaczego Mikołaj kopiuje kod, tj. dlaczego powiela kroki 1 i 3 milion razy? Milion prezentów oznacza, że ​​niepotrzebnie powtarza kroki 1 i 3 milion razy.

Wykonywanie wokół pomaga rozwiązać ten problem. i pomaga wyeliminować kod. Kroki 1 i 3 są zasadniczo stałe, dzięki czemu krok 2 jest jedyną częścią, która się zmienia.

Przykład nr 2

Jeśli nadal tego nie rozumiesz, oto kolejny przykład: pomyśl o kanapce: chleb na zewnątrz jest zawsze taki sam, ale to, co jest w środku, zmienia się w zależności od rodzaju kanapki, którą wybierzesz (np. Szynka, ser, dżem, masło orzechowe itp.). Chleb jest zawsze na zewnątrz i nie musisz powtarzać tego miliard razy dla każdego rodzaju kanapki, którą tworzysz.

Teraz, jeśli przeczytasz powyższe wyjaśnienia, być może łatwiej będzie Ci je zrozumieć. Mam nadzieję, że to wyjaśnienie ci pomogło.

BKSpurgeon
źródło
+ dla wyobraźni: D
Sir. Jeż
3

To przypomina mi wzorzec projektowania strategii . Zauważ, że odsyłacz, który wskazałem, zawiera kod Java dla wzorca.

Oczywiście można wykonać "Wykonaj dookoła", wykonując inicjalizację i oczyszczanie kodu i po prostu przekazując strategię, która następnie będzie zawsze opakowana w kod inicjujący i czyszczący.

Podobnie jak w przypadku każdej techniki używanej do ograniczenia powtórzeń kodu, nie powinieneś jej używać, dopóki nie będziesz mieć co najmniej 2 przypadków, w których jej potrzebujesz, a może nawet 3 (a la zasada YAGNI). Należy pamiętać, że usuwanie powtórzeń kodu ogranicza konserwację (mniej kopii kodu oznacza mniej czasu spędzonego na kopiowaniu poprawek w każdej kopii), ale także zwiększa konserwację (więcej całkowitego kodu). Zatem koszt tej sztuczki polega na tym, że dodajesz więcej kodu.

Ten typ techniki jest przydatny nie tylko do inicjalizacji i czyszczenia. Jest to również dobre, gdy chcesz ułatwić wywoływanie swoich funkcji (np. Możesz go użyć w kreatorze, aby przyciski "następny" i "poprzedni" nie wymagały olbrzymich instrukcji, aby zdecydować co zrobić następna / poprzednia strona.

Brian
źródło
0

Jeśli chcesz fajnych idiomów, oto jest:

//-- 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
źródło
Jeśli moje otwieranie się nie powiedzie (powiedzmy, że uzyskam blokadę ponownego wejścia), zostanie wywołane zamknięcie (powiedzmy zwolnienie blokady ponownego wejścia pomimo niepowodzenia dopasowania otwarcia).
Tom Hawtin - tackline