leon said:
Pseudo-code:
reg.register
config) { Configuration.new }
reg.intercept
config).with { |reg| ConfigurationInterceptor.new(reg} }
reg.register
logger) { |reg| Logger.new(reg.config.filename) }
....
somewhere else in application: reg.config.filename = "newfile"
Because the construction block for :logger called reg.config, the
registry is aware that it depends on :config. The reg.config.filename
assign would have been caught by the interceptor as a "write", and
once completed, the interceptor would send a dirty
config) message to
the registry.
Hmmm. But there's still the issue of the reg.config.filename invocation
(to use the example above) knowing the context in which it is being
called. It would need to know, specifically, which service point
logger) was being instantiated, so that it could relate the dependency
back to that service... hmmm.
I was thinking, for this particular approach to work, |reg| in the
:logger construction block might need to be a proxy that collects the
services used in the block, and adds them to a dependency graph,
instead of being the registry itself (for thread-safety?)
But what about something like this happening:
reg.register
logger) { |r| Logger.new( reg.config.filename ) }
^^^ ^^^ ^^^
In other words, there is nothing that prevents the block from referring
to registries and containers outside the block. This means that it
really can't be a proxy object--it has to be the container itself that
knows the dependency information.
Perhaps you could use a thread-local variable to keep track of the
service that is being constructed. It would need to be a stack, though,
since instantiating one service can cascade into multiple service
instantiations.
If the #register method stored a stack of service names that are
currently being constructed by each thread in a thread-local variable,
your interceptor could use that variable to relate the dependency back
to the registry...
Hmm. But this falls apart when the container itself is a dependency,
since the service that depends thus on the container could query the
container at any time, not just during the service's instantiation. This
means that the service could effectively have new dependencies at
arbitrary points during its lifecycle:
reg.register
svc) { SomeNewService.new(reg) }
reg.svc.reg.another_service
...
I guess what I'm feeling is that there is no general way to know,
definitively, all of the dependencies of a service, especially in a
dynamic language like Ruby. Without putting restrictions on when and how
services are defined (like Copland does), I'm not sure how to come up
with a general solution to this.
That said, what about a slightly less transparent approach:
reg.register
config) { RefreshableProxy.new { Config.new } }
reg.register
logger) { RefreshableProxy.new( reg, :config ) {
Logger.new( reg.config.filename } }
logger = reg.logger
reg.config.refresh!
...
What the above snippet is trying to demonstrate is a kind of observer
pattern. In the first registration, we create a (imaginary)
RefreshableProxy instance that wraps the given configuration instance.
In the second, we create another RefreshableProxy instance that wraps
the Logger.new instantiation. The second one also declares itself to be
an observer of the :config service in the given container.
When #refresh! is invoked on a RefreshableProxy, it notifies all of its
observers that it changed, and the next time a method is invoked on that
proxy it re-executes its associated block. It's really a specialized
kind of deferred instantiation.
Does that make sense? It could be added to the framework so that EVERY
service is automatically wrapped in a RefreshableProxy, but that would
add a lot of overhead that would rarely be needed. Doing it manually
requires you to explicitly specify the dependency graph in the form of
observers, but also incurs a lot less overhead (in general).
Now, if that is what you were wanting, the above COULD be implemented as
a new service model:
class RefreshableServiceModel
...
end
reg.service_models[:refreshable] = RefreshableServiceModel
reg.register( :config, :model=>:refreshable ) { Config.new }
reg.register( :logger, :model=>:refreshable,
bserve=>[:config] ) {
Logger.new( reg.config.filename ) }
config = reg.config
logger = reg.logger
config.refresh!
...
If you would like a concrete implementation of RefreshableServiceModel,
let me know. Otherwise, I'll leave it as an exercise for the reader.
- Jamis