I've got a few more examples I'd like to put out there, as I'm still in pursuit of a solution that makes all parties happy. Basically to elaborate on my previous example (to give a concrete situation where people can provide alternatives), in the loading routines each object is simply loaded from an XML node. You have a generic XML loader, which a graphics plugin (extension) can register custom types against. So <GraphicsEntity type="UserGraphicsEntity"/> is encountered, and the XML node is forwarded to some pre-registered load interface. The graphics engine knows nothing of what might be loaded, and thus cannot have any specific information 'passed in' to its load calls.
std::unique_ptr<IGraphicsEntity> UserGraphicsEntity::LoadGraphicsEntity(LoadContext& context)
{
// general loading
// resolve waveform interface
std::unique_ptr<IWaveFormData> wfd = context.ResolveInterface<IWaveFormData>(guid1);
// resolve temperature interface
std::unique_ptr<ITemperatureData> td = context.ResolveInterface<ITemperatureData>(guid2);
}
Now the nice thing here is 'guid' may refer to an object that doesn't implement ITemperatureData, but 'ResolveInterface' can choose to automatically construct an agent that does the conversion for you. It might instance a TemperatureAgent object, that does whatever is required to extract temperature data from the specified object - and the source object providing the temperature need never know about this intermediary interface. It creates a well defined place where the translation between modules takes place - whether that be waveform data from the sound engine, or temperature data from your dynamics engine. Don't think the source object should ever be available to other modules? That's fine, don't have it 'publish' its information, so it's effectively hidden from its peers.
So where does one access this 'ResolveInterface' method? You can certainly pass it around in the load context as above, you could have a singleton Publishers::getInstance().ResolveInterface method. Anyone can publish itself to the directory, and since we're using GUIDs, by definition you will
never need more than one directory.
Now you could have this publisher interface contained in a factory, so instead we get
std::unique_ptr<IGraphicsEntity> UserGraphicsEntityFactory::LoadGraphicsEntity(XMLNode& node)
{
// general loading
// resolve waveform interface
std::unique_ptr<IWaveFormData> wfi = m_publishers.ResolveInterface<IWaveFormData>(guid1);
// resolve temperature interface
std::unique_ptr<ITemperatureData> ti = m_publishers.ResolveInterface<ITemperatureData>(guid2);
}
I think this would be a perfectly adequate substitute, I don't think it would require a terribly large amount of overhead to implement. It is a plausible alternative to use of a global instance that doesn't inhibit productivity, because when you add a new 'publisher', you do it in precisely one place, and you don't have to update a bunch of code to get access to this new object - it happens for free. This allows objects from any module to access data from any other module though, and I can't help but feel some will step in and say that's not acceptable. That's what a forum is for though.. The other thing is you'll get a proliferation of 'm_publishers' type entities scattered far and wide, an acceptable, but I can't help but feel a little messy, side effect of such an implementation.
One other thought is I believe people get too caught up in what is the bad part of globals (and their cousin singletons), it's not the 'how' that is the problem, meaning how you got a reference to that piece of data - but the 'why'. To illustrate
void Gun::Update()
{
if(g_triggerActive)
{
Fire();
}
}
What's wrong with the above? It's pretty evident, why would a global state, g_gunFiring, be used in a generic Gun class. Doesn't make a lot of sense. How about we get rid of the global
void Gun::Update(bool triggerActive)
{
if(triggerActive)
{
Fire();
}
}
Ignore for a moment it's a bit weird passing in the trigger state to the Update method, but that's not relevant. Now if we call
void main()
{
// ...
gun->Update(g_triggerActive);
Well it hardly fixes a thing. This is the problem I see with so called 'solutions' to the global instance problem. Simply storing the global shared instance as a member variable doesn't change the fundamental behavior of your program. It is mathematically identical in pretty well all respects, except for cache locality related items. Most notably, it changes precisely nil in terms of thread safety when we're talking about references to global data, an often harped on about benefit of not using globals (which I've yet to understand). The problem is not so much the global, but whether what the global represents really is global - and whether referencing that global state from the code in question makes sense.