Global events considered harmful
The “global event architecture” is what could loosely be described as an architectural pattern used principally in composite applications (such as those built using the Smart Client Software Factory or, lately, Prism). It is characterized by loosely-coupled components communicating by means of broadcast events, typically marshaled through an event broker. The events are “broadcast” events in three senses: there is little or no expectation by the sender of what will be receiving the event; a given event may originate in multiple components; and the receipt of events is usually) application-wide, i.e. not confined to any particular context. Also characteristic of this pattern is that senders are connected to recipients implicitly, by the name or the type of the event.
In my article, "Global events considered harmful", I attack the global event architecture often found in composite applications. Having previously worked on three major applications that used the Smart Client Software Factory architecture or derivatives, I recently was privileged to be able to design a completely new architecture suitable for WPF applications.
By far the largest source of problems in the composite applications I had worked on is what I call the "global event architecture". The specific problems I identified were:
- Implicit contracts are formed by the events and their arguments.
- The contracts tend to overlap in function and meaning.
- State is scattered throughout the application.
- It is difficult to get a picture of the interdependencies of components, as they are implicitly coupled by the events.
- It can be difficult to localize the events (e.g. to the scope of a single dialog or view)
- Hanging subscriptions can cause serious application errors, and are hard to find and solve.
- Event receipt/publication requirements in one module may force code to be written in one or more apparently unrelated modules, introducing nasty cross-cutting concerns and delocalized code.
The new architecture solved these problems, and proved to be far easier to develop on and maintain. It is based around the idea of having a "wiring builder". The wiring builder essentially writes a script to run just one application-level controller. The controller constructs containers, binds component properties (including exported commands and command dependencies), and manages workflow where necessary. From the wiring builder, we can generate dependency diagrams between components, as well as workflow diagrams indicating the transitions between application states.
The bulk of the application consists of many simple components and services, each with a single function. The components are written so that their dependencies can easily be mocked; this means that contracts between components are well-defined and testable, and also that it is easy to develop the components in isolation. This also improves reusability.
This new architecture I call the service-tree model, and it has the following advantages:
- Having a wiring manager means that applications can quickly be reconfigured in form and function, without the necessity of a binary release.
- The wiring manager can give you a view of dependencies and workflows that corresponds to what you would see in a functional spec.
- It's much easier to develop the individual components, because they do not rely on external events to drive their state.
- The code has much better locality.
- The application configuration is centralized.
- State contracts are explicit.
If the article seems one-sided, it's because experience with the new architecture has left me absolutely convinced that I'll never again use a non-local event. Hopefully you'll have the same experience.