This article discusses reasons for existence of software flavor called “container” with emphasis on Java. For a long time I was wandering if technologies like Spring, Apache HiveMind, Apache Fortress, OSGi are really useful. And here is the story what I’ve learned. Please be warned that I never used any of those technologies myself! I’m not an expert in any sense, and all this stuff I write based only on reading documentation. This first part is theoretical, and second part contains real-life examples.
Software modularity is known for a long time. It was invented as approach to software analysis and design. Whole software is decomposed into pieces, and each piece implements certain functionality. The reason for modularity is to simplify analysis by controlling levels of detail. There are basically two means of expressing modularization: referring to something compound through a name (e.g. modules, packages, classes, procedures, functions), and preventing something from getting outside of context (e.g. local variables). Another term for levels of detail is called “levels of abstraction”, since detail is complementary term to abstraction. The main point I want to emphasize is that modularization is logical because it is meant to bring logical order by keeping many things local and dependencies explicit.
Next level of modularity is called “source modularity”, which means that source code is divided into parts representing logical modules. Java is remarkable in expressing logical modularization strictly through source. An accidental benefit of source modularity is a possibility of division of work, if analysis precedes implementation.
Another accidental side effect of modularity was the ability to re-use modules in several programs. Whole program is unlikely suitable for more than one purpose. However, a set of logical “blocks” which human’s mind uses is limited, so program analysts often use same concepts while designing different programs, leading to re-use of code which implements those concepts. Programmers started tearing programs apart. Some programming languages are proud of not just expressing modularization, but enforcing it. Some people started to write incomplete programs called “libraries” with idea that others will use them. Analysis of a program now includes a research of which parts could be re-used from other programs.
For me, most straightforward way to re-use existing module is by including its source code into program’s source code. However, if program is compiled, then source code of the module is compiled too. Because main program is changed much more frequently then module, and compilation takes time, so separate compilation was invented and new “linking” stage assembles modules together. An additional benefit of linking is that it very well expresses dependencies between coarse-grained modules.
It doesn’t matter if modules are assembled at source level or at linking level, the result will be a solid program. Compilation and linking are one-way processes, so it is not possible to remove a module from a program. In other words, module itself is a prototype which is “cloned” with no cost, and each clone is “fused” into a program instance forever. Java is slightly different in this way, because it doesn’t have linking stage, JAR files just look solid.
Software modularity as a development practice nowadays is used in any non-trivial software. Yes, it could be misused, but amount of experience in this area today allows developers to quickly learn how to use it in right way.
Struggle for de-coupling and implementation lookup.
Next thing which software developers have noticed is that modules could be replaceable. Several modules can have same interface and differ in implementation, performance, price and license, so module user has a choice. Module users were so excited about “flexibility” their programs suddenly achieved, that they agreed to abandon more straightforward interfaces in favor of “standard” ones. Biggest selling point of most Java technologies is that customer will not depend on single vendor.
Programmers started inventing interfaces and using modules only through interfaces. So, dependencies have not gone, they have just changed. First, instead of depending on particular module, program now depends on interface. And second, a complete program consists not of interfaces, but of particular modules. So dependency on implementation is still introduced, it just happens later. With languages like C a dependency on real module is introduced at linking stage. Unfortunatelly for Java, dependency is specified in the source code, at place where instance of implementation is created. So, for Java “late binding” always means “run-time binding”.
I had this in my Java programs. Whole program used module through interface, and the single place where my program depended on particular implementation was a place where I called “new InterfaceImpl();”. I had extracted this expression into factory method, and replaced “new” with creation through reflection, taking class name from system property. I was very proud of myself, because from this time this code was “flexible”. I have pulled out this dependency into script which launched the program. Such approach for resolving the dependency is called “implementation lookup”, because class name was obtained by searching in some single-instance location using specified key.
Obvious advantage of “late binding” is “deployment flexibility”, which means that you don’t need to re-compile your user module when you want to use another implementation. This could be useful, because some people just afraid of programming, other people don’t want to download source code. However, other people prefer pure programming solution. So, this concern is purely human one. Another advantage, which is more important to developers, is that there is no dependency on particular implementation at compile time, so compiler enforces de-coupling. There are also disadvantages: code became more complex and much less straightforward. If I have specified an incorrect implementation, I would get a runtime error. And instead of single direct dependency it now has two dependencies: on interface and on binding policy.
Implementation injection and Inversion of control
Programmers who value compiler-enforced interface-based programming and don’t value a flexibility of deployment have invented another programming technique called “implementation injection”. Factory method is moved in separate module (“factory module”), and user module receives an instance of interface from this factory module through setter method. Thus, by multiplying amount of modules and complicating a compilation process a programmer will have compiler-enforced flexibility and still pure programming solution.
Implementation injection is a particular case of more general programming practice called “inversion of control”. Inversion of control is a process of changing your module in the way so it is no longer executes some precise action to obtain a result (e.g. creating instance) , but instead it just exposes some means of obtaining a result (like setter method). In other words, Inversion of control is a process of turning an active component into passive one. Another colorful name for this approach is called “Hollywood principle (don’t call us, we’ll call you)”.
Inversion of control is quite general principle. Another particular implementation of it is called “event-driven programming”, which means that instead of having event-polling loop in each component there is a one global event-polling and dispatching loop. Applying inversion of control makes some code more flexible, but less capable (because now it does less), so it should be done only when extracting common behaviour from several places for sharing and unification.
Speaking of complexity, injection and lookup are similar. The only difference is that lookup is more imperative and injection is more structural.
Sometimes you need to execute several totally independent programs in the same runtime environment, for example, in the same JVM. This could be useful on equipment with limited memory, so common class files will be shared. Most straightforward way would be to write simple Launcher, which will create and start all co-programs. Programmers which prefer to stick with single language will write Launcher in the same language as co-programs (Java). But if a set of co-programs to be run changes, then updating such class means full circle of editing, compiling, deploying. There are approaches with better “deployment flexibility”, such as:
- launcher with shell for interactive specifications of co-programs
- launcher which reads configuration or script in some special language
- launcher which automatically discovers co-programs. For this case, modules should follow some common convention, so launcher could discover and use them. This is called “plug-in” concept: “if something exists, then it works, if you don’t want it working – remove it”.
Launcher is a simplest form of “management container”, because the only thing it controls is if a co-program will be started or not. Being able to manage co-programs from single point seems good to lots of people, so number of concerns to be managed have grown far beyond lifecycle.
Each co-program could be managed differently. To unify and automate management tasks most containers introduce “policies” which co-programs must follow. For example, they must implement certain interface used by container for lifecycle. So, making a co-program “manageable” often means inversion of control.
For example of lifecycle-management container, you can imagine window which shows several applets, with controls allowing to start/stop them independently.
Two benefits of container architecture (single management point for users, and having common code in single place for developers) really matter only if number of co-program is significant (for me, more then 10). For just a couple of co-programs, it is not worth to bother.
We are coming to most important point in whole story. Somehow developers decided that modules of which co-programs consist should be separate co-programs in container. Main reason for that is an ability of runtime sharing of code and associated resources (for example, one window frame, one HTTP stack, one event-polling thread). As a result, co-programs are no longer complete, they use other co-programs. Let’s call such co-programs a “components”, since they are parts from which a complete solution is assembled.
To implement sharing, a used component should be created by container (that’s why it is a separate component), and user modules should obtain a shared instance.
One approach for user modules to obtain shared instance is a “lookup”: a reference to shared instance is bound to some “name” (or “key”) in lookup facility, and user modules can obtain an instance using this “name” as a parameter. Since lookup could be done at any moment after user module creation, it is important to create and bind used module before creating user module.
Binding of shared module to name in lookup facility could be realized in different ways:
- “Imperative” way. Binding is done in the code of used module.
- “Declarative” way. Used module declares that it provides some service, and binding is done automatically by container after creation of used module, using service name as key.
- “Container-managed” way. Binding is done by container, either interactively, or through configuration.
Second and third approaches are cases of inversion of control, because common binding code is being moved from components into container. Applying inversion of control to user module turns “instance lookup” into “instance injection”: user module provides a setter method though which it will obtain a reference to used module. Like binding, injection could be realized in “declarative” per-component way and in “container-managed” way.
Thus we got a “component container”, which is a special flavor of management container capable of performing “wiring up” or “linking” components into complete application.
Dependency management in containers
You probably already noticed that the same “lookup” and “injection” approaches are used for two different purposes: for de-coupling of user code from implementation and for discovery of shared instance. Both tasks are cases of general task called “runtime dependency resolving” or “late binding”. Accordingly, “lookup” and “injection” are general solutions to this task. For component containers this means that existing linking facility could be also used for removing “hard” dependencies between components and replacing them with “managed” dependencies.
For simplicity, and because some components simultaneously play roles both of user of one interface and of implementation of another interface (so called “layers”), containers usually have uniform approach for dependency management. Here are typical cases.
“Imperative” container is not far beyond lifecycle manager plus bind/lookup facility. Both binding and lookup is hidden in code, so dependencies are not known to container. If “dependency could not be found” error occurs during startup, then container admin must check documentation and find out if he has missed some component or he should change startup order. In other words, management is simple yet not flexible, and troubleshooting is hard. However, this approach appeals to developers who prefer use single language for everything and do stuff programmatically. Components depend on container (lifecycle and bind/lookup facility) and on interface naming policy.
“Declarative de-centralized” container with dependency injection is a most widespread approach. Separate declaration is explicit, which requires special language for it (e.g. XML, annotations). This is a plug-in approach with container resolving dependencies automatically and printing helpful messages in case of errors. Start order is determined automatically by reversing dependency order. Components depend on container (lifecycle), interface naming policy (because interface must have same name declared in implementations and users) and dependency-declaration language. In other words, components are complex, container management is simple yet not flexible.
“Flexible centralized” container. Components are “plain old java objects” (“POJOs”), which themselves have no dependencies on container and on interface name. Container has an interpreter of some language (declarative, like XML or imperative, like BeanShell) for lifecycle management and wiring up components. This is a centralized and very flexible, but more complex management.
Flexible containers are transparent for components. However, they require some manual work for wiring up components, and in this sense they are just another way of programming.
Other approaches imply that components are designed for particular container. There were attempts to de-couple components from particular containers by designing a common standard for containers (“container framework”). Examples of such efforts are: EJB, Apache Avalon, OSGi. Neither framework prevailed, so developers have following options:
- Stick with single container
- Support several containers (usually not many) directly in cost of harder maintenance
- Use “wrapper” component for each container
There are still “container wars”. Some containers (like OSGi or JSLEE) have some distinguishing features which they use as selling points. Other containers try to be “generic” and attempting to attract users by supporting many dependency-management approaches and having more ready-to-use components packaged. More about that will be explained in second part.
We already mentioned that containers provide some benefits, like uniform management, runtime module sharing and implementation replaceability. However, it comes with cost of harder development and maintenance: architecture is more complex, code is less straightforward, “second” language and possibility of runtime errors.
My main criticism to containers comes from the fact that drawbacks are never mentioned by container vendors. They promote their products and push users to create over-engineered systems.
I believe that runtime errors are bad and should be avoided where possible. By “avoided” I mean “checked at compile stage instead”. In this sense, a claim that “containers help by enforcing modularity” is absolutely false.
To promote containers, their vendors claim that it will become possible to do “rapid application assembly from known components” (taken from azuki site). However, an amount of ready components is very small (in my opinion, only Spring has some). I believe that containers are appealing to non-programmers who think that they can avoid programming. But if your application is just an assembly of “known” components, what gives you advantage over competitors? I never believed in just “assembly”, something always should be written in code.
Then why containers are used ?
In my opinion, container approach is used mostly in server software. There are several reasons for that. Servers often host several independent “services” (co-programs). Server software clearly distinguishes between “service users” and “administrators” who manage from single point. Services often share common modules which consume system resources (threads, memory pools, database connections). For server-side software there is a market for “system-level” components (message queues, network stacks, object-relational mapping, tracing/logging) providing one of standard JEE APIs. So, you can see that all requirements which led to creation of containers are often applicable for server software.
To sum up all said, here are short advices.
- Avoid containers where possible.
- Don’t listen to their propaganda. They want to complicate your life, because they need to justify their worthless efforts to buil something from nothing.
- If forced by circumstances, evaluate something existing
- Be prepared to harder maintenance
A second part, which will try to map theory on real life by checking which of existing containers use which approach.
- Apache container 2.0 paper is a good overall presentation. However, I don’t agree with author about definition of “Inversion of control”.
- Great article of Martin Fowler supposes that you already believe in importance of using de-coupled components and discusses in depth topics of dependency injection and dependency lookup without investigating reasons of emergence of containers