Object lifecycle control
First published at Tuesday 5 April 2011
Warning: This blog post is more then 13 years old – read and use with care.
Object lifecycle control
From time to time I trip over APIs, which do not allow me to control the object lifecycle of the used objects myself. This is a bad thing, because it breaks with several concepts of object oriented programming, and forces you back into the dark ages of class oriented programming.
The problem I am talking about is that the API expects a class name instead of an instance (object). The PHP stream wrapper API, for example, let you register a class for a certain schema:
stream_wrapper_register( 'virtual', 'myVirtualFileSystemHandler' );
In the example above a new object of the class myVirtualFileSystemHandler
would be created for each file system operation, by the internal stream wrapper implementation.
Such class based plugin mechanisms are not uncommon, but do have several drawbacks I want to outline here - and also provide you with solutions to this very problem.
Why is this bad?
The main problem with all class-name based APIs is, that it is not possible to inject further dependencies into the objects resulting from the class name dependency.
Let's examine this in further detail: When we only pass a class name for dependency injection, there are two ways the component in question is using our class:
It only calls static methods (this is bad).
It creates the object internally, itself.
If the object is created internally, the user of the component is not able to inject additional dependencies into the object just created.
Depending on the API we are interfacing with, the object to be created could for example need a database connection or something alike. The user of the API now only has one choice: Introduce a global state, so that the class can access this global state to fetch a database connection. Since the object is not provided explicitely with the dependency, there is no way but fetching it from some globally known place.
The global state can either be a static property, singleton, registry or something alike - all are global states, and introduce various problems, which are discussed elsewhere.
To summarize: Class based APIs force the user to create a global state in his / her application, if he / she wants to do something non-trivial with your API. It breaks dependency inversion.
How can I solve this?
In most cases it should be sufficient to let the user pass an object instead of a class name. If you call methods on that object which require data generated by your code, you can pass this data as an argument to the called method.
Here we get to something different - a sometimes difficult descision: When should I pass something as an argument to a method, and when should I pass dependencies to the constructor of an object? A good rule of thumb is: If an object requires something to exist and cannot live without it, then use the constructor. For everything else use method arguments.
This should solve most issues, but for example the PHP stream API does not just use one object, but wants to create new objects for all kind of use cases. Everytime you access a stream URL somehow, a new object needs to be created for that path. (The constructor is only called sometimes, btw. For example a stat() call on URL does not trigger the constructor, but you get a new object.)
There we have the situation that a parameter is vital to the object (the URL). The stream wrapper API needs to construct multiple objects and cannot cope with a single instance we can inject upfront. Therefore you currently specify a class name and the stream-wrapper implementation creates needed instances of this class for you. If you now want to maintain connections (HTTP, FTP) or maybe emulate a file system in memory (PHP variables), you need to introduce a global state, because there is no way to inject any state into those internally constructed objects.
The better way for the stream-wrapper-API would be to require an instance of a factory to be registered for some URL schema. The API can then request a new object for a given path from the factory. While the factory needs to follow a certain interface, the implementation is left entirely to the user, who can decide to pass additional other dependencies to the created object - like a connection handler, or a data storage.
The user can even decide to re-use objects, if this is really desired. A typical use for this would be some kind of identity management inside of the factory: Reusing the same object for the same path. (Note that this might not make sense in case of the stream wrapper.)
Conclusion
From this elaboration you can learn one very important rule for your API design: Do not create class based APIs, since they force everybody using your API to create a global state, sooner or later. Allowing to pass an object or a factory keeps the user of your API in control of the lifecycle of his objects. He / she can act far more flexible then.
Subscribe to updates
There are multiple ways to stay updated with new posts on my blog: