Jump to content
  • Advertisement
Sign in to follow this  

Clean APIs vs. dependency injection

This topic is 3277 days old which is more than the 365 day threshold we allow for new replies. Please post a new topic.

If you intended to correct an error in the post then please contact us.

Recommended Posts

I'd like to get some opinions on balancing both the need to decouple the dependencies within my code, and the desire to still provide clean APIs for the rest of the code to use. As an example, let's say I have a Model class which loads game object models from disk, for use in a game:
myModel& monster = Model( "BigMonster" );
myModel& door = Model( "RedDoor" );
Nice and clean and simple. However, in order for the Model class to actually load the data, it needs access to our Filesystem class, to pull the data off of the disk, and the Material class to skin the model. Since these classes are external to the Model class, they have to be injected:
myModel& monster = Model( "BigMonster", filesystemManager, materialManager );
myModel& door = Model( "RedDoor", filesystemManager, materialManager );
But now the API is getting clunky, especially if you start loading a lot of models. Is there a cleaner way to do this? I can think of two options so far, but both have their own problems: 1. Static initialization. Store the Filesystem and Material classes in static references, and initialize them before the Model class gets used:
Model::init( filesystemManager, materialManager );
myModel& monster = Model( "BigMonster" );
myModel& door = Model( "RedDoor" );
However, storing static references feels very fragile, almost like a hack. 2. Turn the Model class into a factory. Rather than creating a Model object each time we want a new model loaded, create a single Model instance, and then call on it each time we want a new model:
modelManager Model( filesystemManager, materialManager );
myModel& monster = modelManager.load( "BigMonster" );
myModel& door = modelManager.load( "RedDoor" );
The downside is it seems overkill. Some systems need to be centralized, by design. But here we are centralizing the Model class purely out of convenience. It also means that game code can't just create a Model object and move on, it has to actually get a reference to the one model Manager object. Is there a better way to do this? I would love to hear any opinions on these options, or any other options I haven't thought of.

Share this post


Link to post
Share on other sites
Advertisement
It sounds like you're looking for a blessing to have a global model factory and/or a global filesystem manager and material manager. I bless you thus. Go forth and be pragmatic.

Share this post


Link to post
Share on other sites
Haha. Actually I'm trying to avoid global objects. I consider that option to be worse than the ones I mentioned, I want to keep the code modular and explicit. In the examples I gave, nothing is global, the objects are just being passed around. I just wanted to see if there was another way of doing so that didn't clutter the class interfaces.

Share this post


Link to post
Share on other sites
Quote:
Original post by Nairou
I just wanted to see if there was another way of doing so that didn't clutter the class interfaces.
If you truly believe that globals have no place in C++, then passing loader interfaces around ad nauseam is not "clutter" but the laudable work of a concern well-separated. [grin] Explicitly passing the managers explicitly documents what the model loader is and is not allowed access to.

If you want to save yourself some typing, the third second approach is probably the best. Think of a ModelManager as nothing more than the Model creation function partially bound over the manager arguments, a necessary code wart in a language without actual support for partially bound functions. In particular, since a ModelManager can be created from nothing more than a FileManager and a MaterialManager, you can create one whenever you need one, rather than creating exactly one and keeping it around.

Share this post


Link to post
Share on other sites
API clutter - incorrect abstraction. Round peg and square hole.

Why does model need to know about file system?
Why does it need to know about texture manager?

A model is in-memory representation of some data. It doesn't know or care about file system, texture managers or anything else.

The method of loading something from disk (or other source) is known as serialization. Let's say that model is serializable (and we only support reading).
class Model {
void load(Source &);
};
Source is something that can read bytes, but implemented elsewhere and completely independent.

Quote:
filesystemManager
Apparently, something that manages a file system. So how do we make a model from file system?
Model loadModelFromFile(string s) {
Source s = filesystemManager.getSource(s);
Model m = new Model();
m.load(s);
return m;
};


But how does loadModelFromFile know where to get filesystemManager? And, how will model resolve textures?

Who really needs to know how to resolve textures? The Source, which will deserialize some opaque reference into live texture. But this means that file manager can no longer provide us with Source, but we must construct a specific one for model loading:
Model loadModelFromFile(string s) {
File f = filesystemManager.getFile(s);
Source s = new ModelLoaderSource(f, materialManager);
Model m = new Model();
m.load(s);
return m;
};
This is better, loading is now done. ModelLoaderSource implements Source interface, it takes reference to file from which to read (can be FileSource<-Source) and material manager which will resolve serialized references into loaded textures.

But, both filesystemManager and materialManager are now just assumed to exist. Another problem is, that if materialManager gets asked for a texture it doesn't have, it needs to load it from filesystemManager.

class Assets {
FileSystemManager filesystemManager;
MaterialManager materialManager;

Assets() {
filesystemManager = new FileSystemManager();
materialManager = new MaterialManager(filesystemManager);
}

Model loadModelFromFile(string s) {
// implemented above
}
};
And voila, the spaghetti are untangled.

To load a model, you call your Asset class instance, and use loadModelFromFile utility method.

Share this post


Link to post
Share on other sites
Quote:
Original post by Sneftel
In particular, since a ModelManager can be created from nothing more than a FileManager and a MaterialManager, you can create one whenever you need one, rather than creating exactly one and keeping it around.


Since that eliminates the one downside to his final approach, are there any other hidden downsides?

[edit] and thank you Antheus for answering before I asked.

Share this post


Link to post
Share on other sites
Quote:
Original post by popsoftheyear
Since that eliminates the one downside to his final approach, are there any other hidden downsides?
The downside is the inevitable downside of any OOP method for object creation based on dependencies. If an object A creates an object B, the creation of which depends on help from objects C and D, then A must be the kind of object which cares about C and D. If it doesn't, then it must at least be the kind of object which cares about and stores a B-creator, meaning that either it must care about C and D in its constructor in order to create a B-creator, or whatever object created A must itself care either about C and D, or about B-creators. OOP, particularly with strict use of the Law of Demeter, inevitably leads to this situation, where the concerns of an object must include the concerns of all objects whose creation it is directly or indirectly responsible for.

The solution is to think of something like a FileSystem manager as a software service, as opposed to a depended-on object. But that requires either globals (eek) or more wordy Law-of-Demeter-breaking tricks.

[Edited by - Sneftel on May 29, 2009 3:17:01 PM]

Share this post


Link to post
Share on other sites
In one 3D engine I did a study on, basically figuring out the kinds of OOP classes that can be made. I played on this structure of classes where there was the Engine class instantiated outside of main(), and that gets init()'ed in main. So during engine.init(), a lot of objects get created mostly managers.

For mesh/model loading, I simply called engine.loadmesh() and returned a pointer to it, before sending that to the renderer and physX (actor).

No singleton classes were implemented in this test program which I found to be a lot better than the previous version I wrote.

Share this post


Link to post
Share on other sites
Sign in to follow this  

  • Advertisement
×

Important Information

By using GameDev.net, you agree to our community Guidelines, Terms of Use, and Privacy Policy.

Participate in the game development conversation and more when you create an account on GameDev.net!

Sign me up!