Kiedy powinienem używać programowania opartego na zdarzeniach?

65

Przekazywałam wywołania zwrotne lub po prostu uruchamiałam funkcje z innych funkcji w moich programach, aby wszystko działało po zakończeniu zadań. Kiedy coś się kończy, uruchamiam funkcję bezpośrednio:

var ground = 'clean';

function shovelSnow(){
    console.log("Cleaning Snow");
    ground = 'clean';
}

function makeItSnow(){
    console.log("It's snowing");
    ground = 'snowy';
    shovelSnow();
}

Ale czytałem o wielu różnych strategiach programistycznych, a jedna, którą uważam za potężną, ale której jeszcze nie ćwiczyłem, opiera się na zdarzeniach (myślę, że metoda, o której czytałem, nazywała się „pub-sub” ):

var ground = 'clean';

function shovelSnow(){
    console.log("Cleaning Snow");
    ground = 'clean';
}

function makeItSnow(){
    console.log("It's snowing");
    ground = 'snowy';
    $(document).trigger('snow');
}

$(document).bind('snow', shovelSnow);

Chciałbym zrozumieć obiektywne mocne i słabe strony programowania opartego na zdarzeniach, a nie tylko wywoływać wszystkie funkcje z poziomu innych funkcji. W jakich sytuacjach programowych warto zastosować programowanie oparte na zdarzeniach?

Viziionary
źródło
2
Nawiasem mówiąc, możesz po prostu użyć $(document).bind('snow', shovelShow). Nie ma potrzeby zawijania go w anonimowej funkcji.
Karl Bielefeldt,
4
Być może zainteresuje Cię także „programowanie reaktywne”, które ma wiele wspólnego z programowaniem opartym na zdarzeniach.
Eric Lippert

Odpowiedzi:

75

Wydarzenie jest zgłoszenie opisując zdarzenie z niedawnej przeszłości.

Typowa implementacja systemu sterowanego zdarzeniami wykorzystuje funkcje dyspozytora zdarzeń i funkcji obsługi (lub subskrybentów ). Dyspozytor udostępnia interfejs API do obsługi procedur obsługi zdarzeń (jQuery's bind) oraz metodę publikowania zdarzenia swoim subskrybentom ( triggerw jQuery). Gdy mówisz o zdarzeniach we / wy lub interfejsu użytkownika, zwykle występuje również pętla zdarzeń , która wykrywa nowe zdarzenia, takie jak kliknięcia myszą i przekazuje je do dyspozytora. W JS-land dyspozytor i pętla zdarzeń są dostarczane przez przeglądarkę.

W przypadku kodu, który współdziała bezpośrednio z użytkownikiem - reagując na naciśnięcia klawiszy i kliknięcia - programowanie sterowane zdarzeniami (lub jego odmiana, taka jak programowanie reaktywne ) jest prawie nieuniknione. Ty, programista, nie masz pojęcia, kiedy i gdzie użytkownik kliknie, więc to do GUI lub przeglądarki należy wykrycie akcji użytkownika w pętli zdarzeń i powiadomienie o kodzie. Tego typu infrastruktura jest również wykorzystywana w aplikacjach sieciowych (por. NodeJS).

Twój przykład, w którym wywołujesz zdarzenie w kodzie zamiast wywoływać funkcję bezpośrednio, ma kilka ciekawszych kompromisów, które omówię poniżej. Główną różnicą jest to, że wydawca zdarzenia ( makeItSnow) nie określa odbiorcy wywołania; jest to połączone gdzie indziej (w wywołaniu do bindw twoim przykładzie). Nazywa się to ogniem i zapomnieniem : makeItSnowogłasza światu, że pada śnieg, ale nie obchodzi go, kto słucha, co będzie dalej lub kiedy to nastąpi - po prostu transmituje wiadomość i odkurza ręce.


Podejście oparte na zdarzeniach rozdziela nadawcę wiadomości od odbiorcy. Jedną z zalet, jakie to daje, jest to, że dane zdarzenie może mieć wielu procedur obsługi. Możesz powiązać gritRoadsfunkcję ze zdarzeniem śniegowym bez wpływu na istniejący shovelSnowmoduł obsługi. Masz elastyczność w sposobie podłączenia aplikacji; Aby wyłączyć zachowanie, wystarczy usunąć bindpołączenie, a nie poszukać kodu, aby znaleźć wszystkie wystąpienia tego zachowania.

Kolejną zaletą programowania opartego na zdarzeniach jest to, że daje on możliwość postawienia problemów przekrojowych. Dyspozytor zdarzeń pełni rolę Mediatora , a niektóre biblioteki (takie jak Brighter ) wykorzystują potok, dzięki czemu można łatwo podłączyć ogólne wymagania, takie jak rejestrowanie lub jakość usług.

Pełne ujawnienie: Jaśniejszy został opracowany w Huddle, gdzie pracuję.

Trzecią zaletą oddzielenia nadawcy zdarzenia od odbiorcy jest to, że zapewnia on elastyczność podczas obsługi zdarzenia. Możesz przetwarzać każdy typ zdarzenia we własnym wątku (jeśli obsługuje go program wysyłający zdarzenia) lub możesz umieścić wywołane zdarzenia w brokerze komunikatów, takim jak RabbitMQ, i obsłużyć je za pomocą procesu asynchronicznego, a nawet przetworzyć je zbiorczo przez noc. Odbiorca zdarzenia może być w osobnym procesie lub na osobnej maszynie. Aby to zrobić, nie musisz zmieniać kodu wywołującego zdarzenie! Jest to wielka idea związana z architekturami „mikrousług”: usługi autonomiczne komunikują się za pomocą zdarzeń, a oprogramowanie pośredniczące w komunikacji stanowi kręgosłup aplikacji.

Aby uzyskać raczej inny przykład stylu opartego na zdarzeniach, spójrz na projekt oparty na domenie, w którym zdarzenia w domenie służą do oddzielenia agregatów. Rozważmy na przykład sklep internetowy, który poleca produkty na podstawie historii zakupów. CustomerMusi mieć swoją historię zakupów aktualizowany, gdy ShoppingCartjest opłacone. ShoppingCartKruszywo może powiadomić Customero podnoszenie CheckoutCompletedzdarzenie; Customerbędą aktualizowane w oddzielnej transakcji w odpowiedzi na zdarzenie.


Główną wadą tego modelu opartego na zdarzeniach jest pośrednictwo. Teraz trudniej jest znaleźć kod, który obsługuje zdarzenie, ponieważ nie można po prostu nawigować do niego za pomocą IDE; musisz dowiedzieć się, gdzie zdarzenie jest powiązane w konfiguracji i mieć nadzieję, że znalazłeś wszystkie procedury obsługi. W twojej głowie jest więcej rzeczy do zapamiętania. Pomocne mogą być tutaj konwencje stylu kodu (na przykład umieszczenie wszystkich wywołań bindw jednym pliku). Ze względu na twoje zdrowie psychiczne ważne jest, aby używać tylko jednego dyspozytora zdarzeń i konsekwentnie go używać.

Kolejną wadą jest to, że trudno jest refaktoryzować zdarzenia. Jeśli chcesz zmienić format zdarzenia, musisz także zmienić wszystkie odbiorniki. Zaostrza się to, gdy subskrybenci wydarzenia znajdują się na różnych komputerach, ponieważ teraz musisz zsynchronizować wersje oprogramowania!

W niektórych okolicznościach wydajność może stanowić problem. Podczas przetwarzania wiadomości dyspozytor musi:

  1. Wyszukaj poprawne procedury obsługi w jakiejś strukturze danych.
  2. Zbuduj potok przetwarzania komunikatów dla każdego modułu obsługi. Może to obejmować wiele przydziałów pamięci.
  3. Wywoływaj dynamicznie programy obsługi (ewentualnie używając refleksji, jeśli język tego wymaga).

Jest to z pewnością wolniejsze niż zwykłe wywołanie funkcji, które polega tylko na pchnięciu nowej ramki na stosie. Jednak elastyczność, jaką zapewnia architektura oparta na zdarzeniach, znacznie ułatwia izolowanie i optymalizację wolnego kodu. Możliwość przesłania pracy do procesora asynchronicznego jest tutaj dużą wygraną, ponieważ pozwala natychmiast obsłużyć żądanie, podczas gdy ciężka praca jest wykonywana w tle. W każdym razie, jeśli wchodzisz w interakcję z DB lub rysujesz rzeczy na ekranie, koszty IO całkowicie zwiększą koszty przetwarzania wiadomości. Jest to przypadek uniknięcia przedwczesnej optymalizacji.


Podsumowując, wydarzenia to świetny sposób na tworzenie luźno sprzężonego oprogramowania, ale nie są one bezpłatne. Błędem byłoby na przykład zastąpienie każdego wywołania funkcji w aplikacji zdarzeniem. Wykorzystuj wydarzenia, aby tworzyć znaczące podziały architektoniczne.

Benjamin Hodgson
źródło
2
Ta odpowiedź mówi tak samo jak odpowiedź 5377, którą wybrałem jako poprawną; Zmieniam swój wybór, aby zaznaczyć ten, ponieważ dalej się rozwija.
Viziionary
1
Czy szybkość jest istotną wadą kodu sterowanego zdarzeniami? Wygląda na to, że tak może być, ale nie do końca wiem.
raptortech97
1
@ raptortech97 z pewnością może być. W przypadku kodu, który musi być wyjątkowo szybki, prawdopodobnie nie chcesz wysyłać zdarzeń w wewnętrznej pętli; na szczęście w takich sytuacjach zwykle dobrze określa się, co należy zrobić, więc nie potrzebujesz dodatkowej elastyczności zdarzeń (ani publikowania / subskrybowania ani obserwatorów, które są równoważnymi mechanizmami o różnej terminologii).
Jules
1
Zauważ też, że istnieją pewne języki (np. Erlang) zbudowane wokół modelu aktora, w którym wszystko jest wiadomościami (zdarzeniami). W takim przypadku kompilator może zdecydować, czy zaimplementować komunikaty / zdarzenia jako bezpośrednie wywołania funkcji, czy jako komunikację.
Brendan,
1
W przypadku „wydajności” myślę, że musimy odróżnić wydajność jednowątkową od skalowalności. Komunikaty / zdarzenia mogą być gorsze dla wydajności jednowątkowej (ale mogą być konwertowane na wywołania funkcji dla zerowego dodatkowego kosztu i nie gorsze), a dla skalowalności są lepsze pod każdym względem (np. Prawdopodobnie spowodują znaczną poprawę wydajności w nowoczesnym multi -CPU i przyszłe systemy „wieloprocesorowe”).
Brendan,
25

Programowanie oparte na zdarzeniach jest stosowane, gdy program nie kontroluje sekwencji zdarzeń, które wykonuje. Zamiast tego przepływ programu jest kierowany przez proces zewnętrzny, taki jak użytkownik (np. GUI), inny system (np. Klient / serwer) lub inny proces (np. RPC).

Na przykład skrypt przetwarzania wsadowego wie, co musi zrobić, więc po prostu to robi. To nie oparte na zdarzeniach.

Edytor tekstu siedzi i czeka, aż użytkownik zacznie pisać. Naciśnięcia klawiszy to zdarzenia uruchamiające funkcję aktualizacji wewnętrznego bufora dokumentów. Program nie może wiedzieć, co chcesz wpisać, więc musi być sterowany zdarzeniami.

Większość programów GUI jest sterowana zdarzeniami, ponieważ są one oparte na interakcji użytkownika. Jednak programy oparte na zdarzeniach nie są ograniczone do GUI, co jest po prostu najbardziej znanym przykładem dla większości ludzi. Serwery WWW czekają, aż klienci się połączą i będą postępować według podobnego idiomu. Procesy działające w tle na twoim komputerze również mogą reagować na zdarzenia. Na przykład skaner antywirusowy na żądanie może otrzymać zdarzenie z systemu operacyjnego dotyczące nowo utworzonego lub zaktualizowanego pliku, a następnie przeskanować ten plik w poszukiwaniu wirusów.


źródło
18

W aplikacji opartej na zdarzeniach koncepcja nasłuchiwania zdarzeń umożliwia pisanie jeszcze większej liczby aplikacji z luźną sprzężeniem .

Na przykład moduł lub wtyczka innej firmy może usunąć rekord z bazy danych, a następnie wywołać receordDeletedzdarzenie, a resztę pozostawić detektorom zdarzeń, aby wykonali swoje zadanie. Wszystko będzie działać dobrze, nawet jeśli moduł wyzwalający nie wie nawet, kto nasłuchuje na to konkretne zdarzenie lub co powinno się stać dalej.

53777A
źródło
6

Prosta analogia, którą chciałem dodać, która pomogła mi:

Pomyśl o składnikach (lub obiektach) swojej aplikacji jako dużej grupie znajomych na Facebooku.

Gdy jeden z Twoich znajomych chce Ci coś powiedzieć, może zadzwonić bezpośrednio do Ciebie lub opublikować go na swojej ścianie na Facebooku. Gdy opublikują go na Facebooku, każdy może go zobaczyć i zareagować, ale wiele osób tego nie robi. Czasami jest coś ważnego, że ludzie prawdopodobnie będą musieli na to zareagować, na przykład „Mamy dziecko!” lub „Taki a taki zespół robi niespodziewany koncert w barze Drunkin 'Clam!”. W ostatnim przypadku reszta znajomych prawdopodobnie będzie musiała na to zareagować, szczególnie jeśli są zainteresowani tym zespołem.

Jeśli twój przyjaciel chce zachować tajemnicę między tobą a nimi, prawdopodobnie nie opublikuje go na swojej ścianie na Facebooku, zadzwoni do ciebie bezpośrednio i powie. Wyobraź sobie scenariusz, w którym mówisz dziewczynie, że ci się podoba, że ​​chcesz się z nią spotkać w restauracji na randkę. Zamiast dzwonić do niej bezpośrednio i pytać, opublikuj ją na swojej ścianie na Facebooku, aby wszyscy znajomi mogli ją zobaczyć. To działa, ale jeśli masz zazdrosną byłą, to może to zobaczyć i pojawić się w restauracji, aby zrujnować ci dzień.

Decydując o tym, czy należy wbudować detektory zdarzeń do implementacji, pomyśl o tej analogii. Czy ten element musi pokazać swoją firmę, aby ktokolwiek mógł to zobaczyć? A może muszą zadzwonić do kogoś bezpośrednio? Sprawy mogą stać się dość łatwe, więc bądź ostrożny.

arjabbar
źródło
0

Poniższa analogia może pomóc w zrozumieniu programowania we / wy sterowanego zdarzeniami poprzez narysowanie linii równoległej do oczekiwania w recepcji lekarza.

Blokowanie We / Wy jest jak, jeśli stoisz w kolejce, recepcjonistka prosi faceta przed tobą o wypełnienie formularza, a ona czeka, aż skończy. Musisz poczekać na swoją kolej, aż facet skończy swoją formę, to blokuje.

Jeśli wypełnienie pojedynczego faceta zajmuje 3 minuty, 10. gość musi poczekać do 30 minut. Teraz, aby skrócić ten 10. czas oczekiwania, rozwiązaniem byłoby zwiększenie liczby recepcjonistek, co jest kosztowne. Tak dzieje się w tradycyjnych serwerach internetowych. Jeśli zażądasz informacji o użytkowniku, kolejne żądanie innych użytkowników powinno poczekać, aż zakończy się bieżąca operacja pobierania z bazy danych. Zwiększa to „czas na odpowiedź” 10. żądania i rośnie wykładniczo dla n-tego użytkownika. Aby uniknąć tego tradycyjnego serwera WWW, dla każdego żądania tworzy wątek (równoważny rosnącej liczbie recepcjonistów), tzn. Zasadniczo tworzy kopię serwera dla każdego żądania, co stanowi kosztowne przerwy w zużyciu procesora, ponieważ każde żądanie wymaga systemu operacyjnego wątek. Aby skalować aplikację,

Zależnie od zdarzenia : Innym sposobem na zwiększenie „czasu odpowiedzi” kolejki jest podejście oparte na zdarzeniu, w którym facet w kolejce zostanie przekazany formularz, poproszony o wypełnienie i powrót po zakończeniu. Dlatego recepcjonistka zawsze może przyjąć prośbę. To właśnie robi javascript od samego początku. W przeglądarce javascript reaguje na zdarzenie kliknięcia użytkownika, przewijania, przeciągnięcia lub pobrania bazy danych i tak dalej. Jest to z natury możliwe w javascript, ponieważ javascript traktuje funkcje jako obiekty pierwszej klasy i mogą być przekazywane jako parametry do innych funkcji (zwanych wywołaniami zwrotnymi) i mogą być wywoływane po zakończeniu określonego zadania. To właśnie robi dokładnie node.js na serwerze. Więcej informacji na temat programowania sterowanego zdarzeniami i blokowania operacji we / wy w kontekście węzła można znaleźć tutaj

Vijay Kumar
źródło