An event is a notification describing an occurrence from the recent past.
A typical implementation of a event-driven system utilises an event dispatcher and handler functions (or subscribers). The dispatcher provides an API to wire handlers up to events (jQuery's
bind), and a method to publish an event to its subscribers (
trigger in jQuery). When you're talking about IO or UI events, there's also usually an event loop, which detects new events such as mouse-clicks and passes them to the dispatcher. In JS-land, the dispatcher and event loop are provided by the browser.
For code that interacts directly with the user - responding to keypresses and clicks - event-driven programming (or a variation thereof, such as functional reactive programming) is almost unavoidable. You, the programmer, have no idea when or where the user is going to click, so it's down to the GUI framework or browser to detect the user's action in its event loop and notify your code. This type of infrastructure is also used in networking applications (cf NodeJS).
Your example, wherein you raise an event in your code rather than calling a function directly has some more interesting tradeoffs, which I will discuss below. The main difference is that the publisher of an event (
makeItSnow) does not specify the receiver of the call; that's wired up elsewhere (in the call to
bind in your example). This is called fire-and-forget:
makeItSnow announces to the world that it's snowing, but it doesn't care who's listening, what happens next, or when it happens - it simply broadcasts the message and dusts off its hands.
So the event-driven approach decouples the sender of the message from the receiver. One advantage this affords you is that a given event may have multiple handlers. You could bind a
gritRoads function to your snow event without affecting the existing
shovelSnow handler. You have flexibility in the way your application is wired up; to turn off a behaviour you just need to remove the
bind call rather than go hunting through the code to find all the instances of the behaviour.
Another advantage of event-driven programming is that it gives you somewhere to put cross-cutting concerns. The event dispatcher plays the role of Mediator, and some libraries (such as Brighter) utilise a pipeline so that you can easily plug-in generic requirements such as logging or quality-of-service.
Full disclosure: Brighter is developed at Huddle, where I work.
A third advantage of decoupling the sender of an event from the receiver is that it gives you flexibility in when you handle the event. You could process each type of event on its own thread (if your event dispatcher supports it), or you can put raised events onto a message broker such as RabbitMQ and handle them with an asynchronous process or even process them in bulk overnight. The receiver of the event could be in a separate process or on a separate machine. You don't have to change the code which raises the event to do this! This is the Big Idea behind "microservice" architectures: autonomous services communicate using events, with messaging middleware as the backbone of the application.
For a rather different example of event-driven style, look to domain-driven design, where domain events are used to help keep aggregates separate. For example, consider an online store which recommends products based on your purchase history. A
Customer needs to have its purchase history updated when a
ShoppingCart is paid for. The
ShoppingCart aggregate might notify the
Customer by raising a
CheckoutCompleted event; the
Customer would get updated in a separate transaction in response to the event.
The main downside of this event-driven model is indirection. It's now harder to find the code which handles the event because you can't just navigate to it using your IDE; you have to figure out where the event is bound in the configuration and hope that you've found all the handlers. There's more stuff to keep in your head at any one time. Code style conventions can help here (for example, putting all the calls to
bind in one file). For the sake of your sanity, it's important to only use one event dispatcher and to use it consistently.
Another disadvantage is that it's difficult to refactor events. If you need to change the format of an event you also need to change all the receivers. This is exacerbated when the subscribers of an event are on different machines, because now you need to synchronise software releases!
In certain circumstances, performance may be a concern. When processing a message, the dispatcher has to:
- Look up the correct handlers in some data structure.
- Build a message processing pipeline for each handler. This may involve a bunch of memory allocations.
- Dynamically call the handlers (possibly using reflection if the language requires it).
This is certainly slower than a regular function call, which only involves pushing a new frame on the stack. However, the flexibility that an event-driven architecture affords you makes it much easier to isolate and optimise slow code. Having the ability to submit work to an asynchronous processor is a big win here, as it allows you to serve a request immediately while the hard work is dealt with in the background. In any case, if you're interacting with the DB or drawing stuff on the screen then the costs of IO will totally swamp the costs of processing a message. It's a case of avoiding premature optimisation.
In summary, events are a great way to build loosely coupled software, but they are not without cost. It would be a mistake, for example, to replace every function call in your application with an event. Use events to make meaningful architectural divisions.