Rails Observers - When to and when not to use observers in Rails
In an application that I'm working currently, I see lots of observers. This is indeed creating lot trouble for me while I make code changes, add new functionality, as these observers cause tons of side-effects.
I would like to know the occasions that demand an Observer and the ones people have experiences either empirical or personal on开发者_StackOverflow when one gets tempted to fall in the observer trap.
Your valuable experience, war-stories and thoughts are in demand. Please do shout out!
I feel that observers get a bad rap largely because people lump them in with ActiveRecord lifecycle callbacks as being the same thing. I do agree with a lot of the popular opinion on lifecycle callbacks being easy to misuse, getting yourself in a tangle, but I'm personally a big fan of observers for keeping things out of model classes that are not the particular model's core responsibility. Here's a hint: Rails' observers were partly inspired by aspect-oriented programming -- they're about cross-cutting concerns. If you're putting business logic in observers that is tightly coupled to the models they're observing, you're doing it wrong IMO.
They're ideal for keeping clutter out of model classes, like cache expiration (sweepers), notifications of various sorts, activity stream updates, kicking off background jobs to track custom analytics events, warm caches, etc.
I emphatically disagree with BlueFish about observers being difficult to properly unit test. This is precisely the biggest point that distinguishes them from lifecycle callbacks: you can test observers in isolation, and doing so discourages you from falling into many of the state- and order-heavy design pitfalls BlueFish refers to (which again I think is more often true of lifecycle callbacks).
Here's my prescription:
- Disable all observers in your test suite by default. They should not be complicating your model tests because they should have separate concerns anyway. You don't need to unit test that observers actually fire, because ActiveRecord's test suite does that, and your integration tests will cover it. Use the block form of
ActiveRecord::Base.observers.enable
if you really believe there is a good reason to enable an observer for some small piece of your unit tests, but it's likely an indicator of misuse or a design problem. - Enable observers for your integration tests only. Integration tests of course should be full-stack and you should be verifying observer behavior in them like everything else.
- Unit-test your observer classes in isolation (invoke the methods like
after_create
directly). If an observer isn't part of its observed model's business logic, it probably will not have much dependence on state details of the model instance, and shouldn't require much test setup. You can often mock collaborators here if you're reasonably confident that your integration tests cover what you most care about.
Here's my standard boilerplate spec/support/observers.rb
for apps using RSpec:
RSpec.configure do |config|
# Assure we're testing models in isolation from Observer behavior. Enable
# them explicitly in a block if you need to integrate against an Observer --
# see the documentation for {ActiveModel::ObserverArray}.
config.before do
ActiveRecord::Base.observers.disable :all
end
# Integration tests are full-stack, lack of isolation is by design.
config.before(type: :feature) do
ActiveRecord::Base.observers.enable :all
end
end
And here is a real-world example that I hope illustrates a good case for using an observer, and testing it without pain.
IMHO - Observers Suck
I will go through a number of reasons why I think they do. Mind you this applies in general to the use of before_x or after_x methods as well - which are more piecemeal examples of the general Observer.
Makes it hard to write proper unit tests
Typically, when you write unit tests, you are testing a specific piece of functionality. To test an observer however, you need to 'trigger' the event in order to test it, and sometimes that is just downright inconvenient.
E.g. If you hook up an observer to before_save, then to trigger the code, you need to save the model. This makes it tricky to test, given that you might be testing business logic, not persistence. If you mock out the save, your trigger might not work. And if you let it save, then your tests are slow.
Requires state
Following on from the fact that observers tend to be hard to test, observers tend to also require a lot of state. The reason is because the logic in an observer is trying to distinguish between various 'business events' and the only way to do so is to look at the state of the object. This requires a lot of setup in your tests, and therefore makes testing hard, tedious and problematic.
Unintended consequences
No doubt, you have just experienced that because you can hook up multiple observations, you have no idea what could be triggering various behaviours. This leads to unintended consequences, one which you can only pick up through integration/system testing (slow feedback). Tracing your observers is also not a lot of fun.
Order assumptions problems
There is no guarantee when an observer may kick in. You are only guaranteed that it will be kicked off. If you have implicit order as part of your business rules, then Observers are wrong.
Leads to poor design
Adding things by hooking up observers leads to poor designs. It tends to lead you to hook everything up to save, delete, create events - which whilst convenient, is also hard to understand. E.g. saving a user could mean that you are updating the user's details, or it could mean that you are adding a new account name to it. Knowing what you can specifically do to an object is part of the reason why you have methods, and meaningful action-based names. If everything is an observer, then this gets lost and everything is now responding to events, and within your observation logic, you tend to try to distinguish the event, belonging to which business event.
There are some places where an observer is nice, but that is usually an exception. It is far better to engraft what can be done explicitly, rather than encode the logic implicitly via callbacks.
I partly agree with BlueFish, in that observers can introduce unnecessary complexity, however observers are useful for separating concerns from the object.
For example, in a Payment AR model one might want to deliver a receipt after create. Using a regular AR after_create callback, if the deliver_receipt method failed the payment wouldn't get written to the database - oops! However in an observer, the payment would still get saved. One could argue that failures should be handled by rescue, but I still think it doesn't belong there; it belongs in an observer.
精彩评论