When I first read about dependency injection, the pattern made total sense to me in a number of ways. It fits the way I think about problems and my mental model of designing object representations of the stuff I am implementing. I’ve decided to roll my own DI solution, though, because there are a few subtle changes that align even better with a few of my basic premises in programming.
When I write code, I think about what I want to do and what I need to do that. When I write a class that does database stuff, I know it requires a database connection. I think this is information that should be apparent in the class implementation and nowhere else. So, what is more obvious to have the class know what it requires in the class itself? And this is how our Factory class approaches this: it can ask a class it is just instantiating what it needs.
The code flow on instantiation is basically
- we ask the Factory for an object:
- Factory instantiates
- (see below)
- Factory asks the object
- NewObject returns an array of assets that it needs
- Factory keeps a list of registered assets (like an open and ready-to-use database connection) or knows how to find assets it does not yet have registered.
- Factory also registers itself as an asset with newObject
- The new object is returned to the caller
One of the lovely side effects of this flow is that in most cases, we can eliminate the need for singletons; we can just open the database connection once and pass it on or we can register a cache once and have multiple objects use it.
You may have realized that we skipped a step on the first flow. Before Factory actually asks newObject what it requires, it passes Context on. Context is – as the name implies – a collection of information about the current application in its environment. For instance, we have the name and class of the system we are running on in there, or whether it is an anonymous session or one of a logged-in user. By first knowing this, the object we are setting up gets to decide what it actually requires before it answers the question. For instance, it may well be that an anonymous session does not require a database connection where a logged-in user will need that. So we additionally have:
3. Inject Context:
Another thing that is important to me when I design stuff is to only keep around the stuff I need and only when I need it. I don’t think you should pull in all code of all libraries you use for every call. This means that some methods may need additional assets that other methods in the same class won’t need. By having Factory as an asset, we can let those methods ask for registered assets from our class hierarchy at the time they need them; not when first initializing the object.
To have an asset, Factory has a method
registerAsset(name, asset). asset can be an object of the name of a class. If it is a class name that is registered, it will be instantiated the first time it is actually required and then also passed on to other objects that also require it. If our objects need new instances of a class, it is possible to specify that at registration (because it is more likely that the providing object knows about that). To register an asset emailTemplate that will actually provide every object with a new emailTemplate, we need just to write
and are all set.
As a final fallback, when Factory is asked for an asset that it doesn’t know about, it just tries to instantiate a class by that name.
Our Object class – which, as the name implies, is the parent class of most objects in our hierachy – encapsulates the default behavior. It uses one protected variable
_requiredAssets that contains the array of the assets the current class requires. And that way, we can just concentrate on getting on with the stuff that the class is actually about.