In one paragraph, “Inversion of Control” is a design pattern that describes the practice of delegating some aspect of control to another part of the program (usually called the “container”). For example, you might invert control over instantiating the dependencies of an object, so that instead of the object instantiating its own dependencies, the container instantiates them for it. This particular inversion of control is called the Dependency Injection pattern by some, because the container is “injecting” the dependencies into the system. “Inversion of Control” is a more general pattern than “Dependency Injection”, although you’ll sometimes hear them used interchangeably.
The fact is, most IoC containers provide a few more bells and whistles than just dependency injection. (Other kinds of control that might be inverted (besides dependencies) include logging, configuration, concurrency, localization, and lifecycle management.) Because these containers provide a meta-programming interface to your objects, they can do a lot of fancy stuff behind the scenes, such as adding AOP-like “before” and “after” hooks, or multicasting. These meta-programming tools make it easier to extend, debug, and unit-test your sub-systems, and with the dependency injection pattern that is at the core of all IoC containers, you can easily wire your sub-systems together in a non-intrusive, easily-scalable manner.
IoC also makes unit testing easier, because every service is dependency injected. This means that instead of the object instantiating all of its dependencies manually, it accepts them as either constructor parameters or properties, so when unit testing you can easily assign mock objects to those dependencies. This simplifies the test cases considerably.
To be sure, IoC is not a panacea. It will not solve every problem that has ever plagued you as a software engineer. Nor will it make every task easier. However, for large and complex systems (and the larger and more complex, the better), the features provided by an IoC container can make designing, implementing, and maintaining that system much easier, since a large part of the complexity of such a system is maintaining the dependencies inside the system.
Ruby (and scripting languages in general) are not widely viewed by most decision makers as acceptible solutions for enterprise-level applications. In perfect honesty, it must be admitted that there is no single programming language that is the perfect solution to every problem. However, there are niches where scripting languages (and Ruby in particular) are becoming more and more accepted for large-scale systems. Unfortunately, tools are sadly lacking in Ruby for building large, scalable, and maintainable systems.
As of this writing, there is only one other IoC container for Ruby, Rico. Unfortunately, it is still fairly early in its development, and although it is usable for basic dependency injection needs, even the developers admit that “it is primarily a proof of concept at this stage.”
Copland is fairly new, too, but is making great strides forward. It has already gone through one significant rewrite and as a result is much more robust and internally consistant than it was before. It already supports at least as much functionality as most other mature IoC systems, and sports some features that I haven’t seen anywhere else. At this point, I have to admit that Copland is perhaps not yet ready for the enterprise, but if enough people play with it and submit suggestions and bug reports, it may not be long before it is.
The following case study has been shamelessly borrowed from the one used in the HiveMind documentation. It is a beautiful and compelling example of the power of IoC. I’ve paraphrased (and rephrased) portions of it, and removed the lengthy Java and XML sample snippets. The original author of this case study is Howard Lewis Ship, the creator of HiveMind, and the original version of the case study may be viewed in the HiveMind documentation.
Consider a hypothetical product called “Panorama.” It is a large web application deployed via WEBrick, and consists of several hundred classes, divided into a large number of tools and services.
Even though Panorama is an enterprise application, it is still logically a number of subsystems. Many of these subsystems require some form of startup and shutdown logic. As an example, the Help service caches help data, which is stored in a database, and the Mail service sets up database cleanup jobs to run at specified intervals. In toto, there are over 40 startup tasks, and a few shutdown tasks.
One way to architect this without an IoC container (because it certainly can be done), is to create a controller object that manages all of the startup and shutdown activity. This controller would be invoked at application start-up, it would run the necessary startup tasks in the required order (since order is important, for some of them), and then when the application shuts down the controller invokes all of the necessary shutdown tasks.
This works, of course. However, you’ve had to hard-code the dependencies between the controller and the various services; the controller must be aware of every service, and anytime you add a new service, you have to edit the controller. This can be mitigated to a large degree by having each service register itself with the controller, but there you’ve got the same problem, to a smaller degree: each service now has an explicit dependency on the controller object, and if you ever refactor that controller class into a different location or a different name, that’s 40+ services you have to modify!
IoC to the Rescue
IoC moves all of those dependencies out of the code and into configuration files, called (in Copland) package descriptors. These descriptors contain service definitions, configuration information, and so forth.
We do more-or-less what we described in the non-IoC solution, above: we create a service called “Startup”, and we have services register themselves with that service. However, the registration is done indirectly, via what is called a “configuration point.” Services are added (along with the order in which they want to be started) to that configuration point, and then the Startup service reads from that configuration point to determine who gets started.
Similarly, we define a “Shutdown” service that reads from yet another configuration point. Services that wish to be notified of system shutdown are added to that configuration point, and the Shutdown service takes care of the rest.
Finally, we have the Startup service add itself as a listener to the registry, so that when the registry is initialized, the Startup service will be notified. Then, we add Shutdown as a listener to the registry as well, so that when the registry is shutdown, the Shutdown service can take any necessary actions.
Certainly, you could implement all of this yourself. It’s nothing magical or new. However, you’d just be implementing a special case of an IoC container, and if you ever wanted to add logging or security to your “services”, you’d find yourself diving back into your code to add the necessary calls at various points. Before you know it, you’d find that you’d have implemented an IoC container after all.
Copland offers these features “out-of-the-box”. Applications can easily take advantage of them. Developers don’t need to spend time reinventing the wheel (even though that is often most of the fun of some projects).