As an application grows, if there are no clear rules about what can depend on what, any potential change to your data can propagate in surprising ways. The chains of dependencies get longer and you lose track of them, and before you know it some of the paths form circles.
Once there are circles, there’s no longer necessarily any definitive “rest state” for the system. Things begin to depend heavily on timing – if this happens to update faster than that, we get one answer, otherwise we get another answer (or an Exception). Plus you now probably have performance problems, because a single change may chase itself around the loop repeatedly. And simple changes to one part of the system could break a far-away part of the system, because they alter the global timing!
So circular data dependencies are bad. And seeing the entire cycle is hard, because it requires you to have global understanding of the program. You can’t look at any one component or family of components in isolation to know they are safe. Effects are non-local. Spooky action at a distance.
What we need instead is a rule that can be enforced locally that defends us globally against cycles. That rule is “data down, actions up” (DDAU).
We can break DDAU into the four terms:
- Data: a way of propagating changes through your program by updating a variable to a new value
- Down: moving further away from the “top” of the application (which is the outermost stuff in
application.hbs), and toward the leafmost components.
- Action: a way of propagating changes through your program by calling a function
- Up: the opposite of down. Moving toward the top of the application.
Notice that both “data” and “action” are “ways of propagating changes”. In one sense, they’re equivalent. You could in principle make all communication between components be “data”, or you could make all communication between them be “actions”, and both systems would be equivalently powerful.
But we split them into these two separate categories because it helps us establish our cycle-breaking local rule. The rule, broken down into explicit detail, is these invariants:
- You may only invoke actions upward. Never downward.
- You may only propagate data changes downward. Never upward.
- Actions may cause more actions.
- Data changes may cause more data changes.
- Actions may cause data changes.
- Data changes may not cause actions.
If you maintain these invariants, no cycles can occur. If you break any one of them, you can get a cycle again. And you can examine an individual component to see if it’s breaking any of the invariants – you don’t need to examine the entire global program.
Now, this sets me up to finally answer the original question: the point of observers is usually to break invariant number 6.
I say usually because there are still legitimate uses for observers. They are legitimate for reflecting the state of Ember onto foreign interfaces that don’t understand Ember’s data flow conventions. That doesn’t cause cycles as long as those foreign interfaces aren’t looping back and propagating those changes back into Ember.
The remaining subtleties mostly come from knowing which way is “up” when you’re dealing with more than just a component hierarchy. For example, if two services are interacting with each other, which is “up”? For any given piece of state, you need to decide who “owns it”, and they are the “the top”, and actions get sent to them and everybody else consumes their data.