Versions Compared

Key

  • This line was added.
  • This line was removed.
  • Formatting was changed.
Comment: Spelling, reduced heading levels

...

By contrast, web applications are a managed environment. You don't write a main(), you don't control startup. You configure the Servlet API to tell it about your servlet classes to be instantiated, and their lifecycle life cycle is totally controlled by the servlet container.

Inversion of Control is just a more general application of this approach. The container is ultimately responsible for instantiating and configuring the objects you tell it about, and running their entire lifecycle life cycle of those objects.

Building web applications are more complicated than monolithic applications, largely because of multithreading. Your code will be servicing many different users simultaneously across many different threads. This tends to complicate the code you write, since some fundamental aspects of object oriented development get called into question: in particular, the use of internal state, values stored inside instance variables, since in a multi-threaded environment, that's no longer the safe place it is in traditional development. Shared objects plus internal state plus multiple threads equals an broken, unpredictable application.

...

Using unit tests, in collaboration with tools such as EasyMock, you can have a code base that is easy to maintain, easy to extend, and easy to test. And by factoring out a lot of plumbing code, your code base will not only be easier to work with, it will be smaller.

Living on the Frontier

Coding applications the traditional way is like being a homesteader on the American frontier in the 1800's. You're responsible for every aspect of your house: every board, every nail, every stick of furniture is something you personally created. There is a great comfort in total self reliance. Even if your house is small, the windows are a bit drafty or the floorboards creak a little, you know exactly why things are not-quite perfect.

...

To extend the metaphor, a house in a town is not alone and self-reliant the way a frontier house is. The town house is situated on a street, in a neighborhood, within a town. The town provides services (utilities, police, fire control, streets and sewers) to houses in a uniform way. Each house just needs to connect up to those services.

The World of the Container

So the IoC container is the "town" and in the world of the IoC container, everything has a name, a place, and a relationship to everything else in the container. Tapestry calls this world "The Registry".

Image Added

Here !images/ioc-overview.png!IoC OverviewHere we're seeing a few services from the built-in Tapestry IoC module, and a few of the services from the Tapestry web framework module. In fact, there are over 100 services, all interrelated, in the Registry ... and that's before you add your own to the mix. The IoC Registry treats all the services uniformly, regardless of whether they are part of Tapestry, or part of your application, or part of an add-on library.

Tapestry IoC's job is to make all of these services available to each other, and to the outside world. The outside world could be a standalone application, or it could be an application built on top of the Tapestry web framework.

Service

...

Life Cycle

Tapestry services are lazy, which means they are not fully instantiated until they are absolutely needed. Often, what looks like a service is really a proxy object ... the first time any method of the proxy is invoked, the actual service is instantiated and initialized (Tapestry uses the term realized for this process). Of course, this is all absolutely thread-safe.

Initially a service is defined, meaning some module has defined the service. Later, the service will be virtual, meaning a proxy has been created. This occurs most often because some other service depends on it, but hasn't gotten around to invoking methods on it. Finally, a service that is ready to use is realized. What's nice is that your code neither knows nor cares about the lifecycle life cycle of the service, because of the magic of the proxy.

In fact, when a Tapestry web application starts up, before it services its first request, only about 20% of the services have been realized; the remainder are defined or virtual.

Class vs. Service

A Tapestry service is more than just a class. First of all, it is a combination of an interface that defines the operations of the service, and an implementation class that implements the interface.

...

Tapestry IoC also has support for other configuration that may be provided to services when they are realized.

Dependency Injection

Inversion of Control refers to the fact that the container, here Tapestry IoC's Registry, instantiates your classes. It decides on when the classes get instantiated.

...

In any case, injection "just happens". Tapestry finds the constructor of your class and analyzes the parameters to determine what to pass in. In some cases, it uses just the parameter type to find a match, in other cases, annotations on the parameters may also be used. It also scans through the fields of your service implementation class to identify which should have injected values written into them.

Why can't I just use new?

I've had this question asked me many a time. All these new concepts seem alien. All that XML (in the Spring or HiveMind IoC containers; Tapestry IoC uses no XML) is a burden. What's wrong with new?

...

What we're seeing here is that there are multple concerns inside the little bit of code in this example. TableMetricProducer has an unwanted construction concern about which implementation of QueueWriter to instantiate (this shows up as two constructors, rather than just one). QueueWriterImpl has an additional lifecycle life cycle concern, in terms of managing the singleton.

...

The contributeMetricScheduler() method allows the module to contribute into the MetricProducer service's configuration. More testability: the MetricProducer isn't tied to a pre-set list of producers, instead it will have a Collection<MetricProducer> injected into its constructor. Thus, when we're coding the MetricProducerImpl class, we can test it against mock implementations of MetricProducer.

The QueueWriter service in is injected into the contributeMetricScheduler() method. Since there's only one QueueWriter service, Tapestry IoC is able to "find" the correct service based entirely on type. If, eventually, there's more than one QueueWriter service (perhaps pointing at different JMS queues), you would use an annotation on the parameter to help Tapestry connect the parameter to the appropriate service.

Presumably, there 'd would be a couple of other parameters to the contributeMetricScheduler() method, to inject in a database URL or connection pool (that would, in turn, be passed to TableMetricProducer).

...

Meanwhile, the QueueWriterImpl class no longer needs the instance variable or getInstance() method, and the TableMetricProducer only needs a single constructor.

Advantages of IoC: Summary

It would be ludicrous for us to claim that applications built without an IoC container are doomed to failure. There is overwhelming evidence that applications have been built without containers and have been perfectly successful.

...

Roll the clock forward a decade and the common consensus has shifted considerably. Objective-C 2.0 features true garbage collection and GC libraries are available for C and C++. All scripting languages, including Ruby and Python, feature garbage collection as well. A new language without garbage collection is now considered an anomaly.

The point is, the lifecycle life cycle of objects turns out to be far more complicated than it looks at first glance. We've come to accept that our own applications lack the ability to police their objects as they are no longer needed (they literally lack the ability to determine when an object is no longer needed) and the garbage collector, a kind of higher authority, takes over that job very effetivelyeffectively. The end result? Less code and fewer bugs. And a careful study shows that the Java memory allocator and garbage collector (the two are quite intimately tied together) is actually more efficient that malloc() and free().

So we've come to accept that the death concern is better handled outside of our own code. The use of Inversion of Control is simply the flip side of that: the lifecycle life cycle and construction concerns are also better handled by an outside authority as well: the IoC container. These concerns govern when a service is realized and how its dependencies and configuration are injected. As with the garbage collector, ceding these chores to the container results in less code and fewer bugs, and lets you concentrate on the things that should matter to you: your business logic, your application – and not a whole bunch of boilerplate plumbing!