Overriding a Service |
- Step One: A non-overridable service
- Step Two: Add some indirection
- Step Three: Override!
- Limitations
It is not uncommon to want to override an existing service and replace it with a new implementation. This goes beyond simply intercepting the service ... the goal is to replace the original implementation with a new implementation. This occurs frequently in Tapestry where frequently an existing service is replaced with a new implementation that handles application-specific cases (and delegates most cases to the default implementation).
HiveMind doesn't have an explicit mechanism for accomplishing this ... that's because its reasonable to replace and wrap existing services just with the mechanisms already available.
Step One: A non-overridable service
To describe this technique, we'll start with a ordinary, every day service. In fact, for discussion purposes, there will be two services: Consumer and Provider. Ultimately, we'll show how to override Provider. Also for discussion purposes, we'll do all of this in a single module, though (of course) you can as easily split it up across many modules.
To begin, we'll define the two services, and set Provider as a property of Consumer:
module (id=ex.override version="1.0.0") { service-point (id=Provider interface=ex.override.Provider) { create-instance (class=ex.override.impl.ProviderImpl) } service-point (id=Consumer interface=ex.override.Consumer) { invoke-factory (service-id=hivemind.BuilderFactory) { construct (class=ex.override.impl.Consumer) { set-service (property=provider service-id=Provider) } } } }
Step Two: Add some indirection
In this step, we still have just the two services ... Consumer and Provider, but they are linked together less explicitly, by using substitution symbols.
module (id=ex.override version="1.0.0") { service-point (id=Provider interface=ex.override.Provider) { create-instance (class=ex.override.impl.ProviderImpl) } contribution (configuration-id=hivemind.FactoryDefaults) { default (symbol=ex.override.Provider value=ex.override.Provider) } service-point (id=Consumer interface=ex.override.consumer) { invoke-factory (service-id=hivemind.BuilderFactory) { construct (class=ex.override.impl.Consumer) { set-service (property=provider service-id=${ex.override.Provider}) } } } }
The end result is the same ... the symbol ex.override.Provider evaluates to the service id ex.override.Provider and the end result is the same as step one. We needed to use a fully qualified service id because, ultimately, we don't know in which modules the symbol will be referenced.
Step Three: Override!
The final step is to define a second service and slip it into place. For kicks, the OverrideProvider service will get a reference to the original Provider service.
module (id=ex.override version="1.0.0") { service-point (id=Provider interface=ex.override.Provider) { create-instance (class=ex.override.impl.ProviderImpl) } contribution (configuration-id=hivemind.FactoryDefaults) { default (symbol=ex.override.Provider value=ex.override.Provider) } service-point (id=OverrideProvider interface=ex.override.Provider) { invoke-factory (service-id=hivemind.BuilderFactory) { construct (class=ex.override.impl.OverrideProviderImpl) { set-service (property=defaultProvider service-id=Provider) } } } // ApplicationDefaults overrides FactoryDefaults contribution (configuration-id=hivemind.ApplicationDefaults) { // Must specify the fully qualified service id (the symbol // may be evaluated in an unknown context later) default (symbol=ex.override.Provider value=ex.override.OverrideProvider) } service-point (id=Consumer interface=ex.override.consumer) { invoke-factory (service-id=hivemind.BuilderFactory) { construct (class=ex.override.impl.Consumer) { set-service (property=provider service-id=${ex.override.Provider}) } } } }
The new service, OverrideProvider, gets a reference to the original service using its real id. It can't use the symbol that the Consumer service uses, because that would end up pointing it at itself. Again, in this example it's all happening in a single module, but it could absolutely be split up, with OverrideProvider and the configuration to hivemind.ApplicationDefaults in an entirely different module.
hivemind.ApplicationDefaults overrides hivemind.FactoryDefaults. This means that the Consumer will be connected to ex.override.OverrideProvider.
Note that the <service-point> for the Consumer doesn't change between steps two and three.
Limitations
The main limitation to this approach is that you can only do it once for a service; there's no way to add an EvenMoreOverridenProvider service that wraps around OverrideProvider (that wraps around Provider). Making multiple contributions to the hivemind.ApplicationDefaults configuration point with the name symbol name will result in a runtime error ... and unpredictable results.
This could be addressed by adding another source to the hivemind.SymbolSources configuration.
To be honest, if this kind of indirection becomes extremely frequent, then HiveMind should change to accomidate the pattern, perhaps adding an <override> element, similar to a <interceptor> element.