The Java Framework Landscape
Java frameworks span everything from the simplistic to the entirely comprehensive. Some frameworks, for example frameworks concentrating on user interface development, web applications, or persistence, are tightly focused. Others take a more "holistic" approach, providing benefit to many different areas of an application's development efforts. It is these "whole application" frameworks I'll concentrate on here.
The best frameworks do exactly what a "framework" is defined to do: Provide a basic conceptional structure (as of ideas) or a skeletal, openwork, or structural frame on which you can construct your application.
This article will touch on a few frameworks available as open source, and observe some of the techniques and patterns used when choosing frameworks and putting frameworks to use.
An application framework is a tool that provides both structure and services to your application. Unlike an IDE or compiler, a framework usually will be deployed along with your application. Exactly what the structure looks like, or what the services are, depends on the particular framework. Some concentrate more in one area than in others; some provide a "soup to nuts" array of services ranging from user interface connectivity through to persistence and database access.
A good application framework provides proven and reliable design patterns and code architecture to an application, increasing development productivity manyfold. Frameworks grew out of a realization that many common design patterns could have a single implementation, and that implementation could be used on multiple projects.
As open source Java grew in popularity, many application problems turned out to have common solutions. These solutions began to be abstracted into design patterns—ways of doing certain things that had been tried and found to be successful. As implementations of these patterns were saved and used again, code re-use began to grow between projects, and the most common solutions were soon grouped together into the first frameworks.
Although an API library provides for a level of re-use, frameworks provide more structure than an API library does, and usually incorporate a number of different APIs, each providing a specific set of services to your application.
As component-based development began its rise in popularity, new frameworks were written (and existing frameworks were extended) to become the component execution environment and the inter-component communication mechanism—in a word, the component "container" of choice.
Many such frameworks were created; a few were released as open source, and gained popularity slowly. A number of reasons led to the slow initial adoption of frameworks. Many development shops had a bad case of "not invented here" syndrome. Many more were slow to understand the correct use of design patterns and components. Both attitudes impeded the early adoption of frameworks, often causing an organization to re-invent services and functionality already freely available.
Paradoxically, reductions in the budgets available for development has speeded up the growth of frameworks. As developers were forced to do more with less, they looked to re-use and application of standard components as solutions—and frameworks were there to provide them. No longer did developers have the luxury of re-inventing solutions to common problems many times—they were forced to consider re-use as a higher priority, and frameworks are ideal examples of re-use at its best.
Frameworks and Open Source
Frameworks are a good fit for the open source model in that the more they are used, the more flexible they tend to become, assuming the initial design was good.
Often, a framework is built as the result of an iterative process: As projects that have some common elements are developed, reusable components are created to reduce the need to continually re-create typical functionality. As these components are re-used and made more generic and flexible, they become the backbone of a framework (or become components to be run in an existing framework).
Many frameworks evolve to encompass services for the entire range of needs of an application. Open source means that a framework of this type will be extended and new services added by a number of different developers, creating services and capabilities that a single group might not ever have time to build for themselves, and increasing the utility of the framework for everyone.
Types of Frameworks
Frameworks designed for general application development often provide more capability in particular areas of the application development process. For example, the Apache Struts framework provides a lot of functionality in the area of user-interface building, whereas a framework like Spring doesn't touch the user-interface area much at all.
Some frameworks take a more pattern-oriented approach, supplying little of their own code; instead, they concentrate on the structure and overall architecture of an application, providing in essence a "backbone" into which other components can be connected. Often, a framework is broken into several sub-projects, each providing functionality for a particular area, such as user interface, persistence, scheduling, and so forth. Ideally, these sub-projects should be both optional and replaceable, giving the maximum flexibility to the developer.
The exact line between a framework and a project that provides functionality such as persistence or user-interface building is hard to draw sometimes. Generally, a framework will provide functionality able to build an entire application, even if it is on one area.
Some frameworks are created for industry-specific purposes, such as frameworks for building accounting solutions. These tend to be less widely re-usable, and are less often released in open source.
Another distinctive attribute of frameworks is the way they are incorporated into your own projects. Many other tools provide support to your project during development, but are generally not a part of your deployed application. Even the virtual machine, although of course essential to the deployed application, is not tied to it—your application should be able to be moved to any other compliant virtual machine and run just as well. This isolation is not necessarily the case with frameworks: A framework's code is usually deployed as an integral part of your finished application. This makes the choice of framework all the more important because your dependence on it does not end when the development is complete; you must be able to rely on its continued operation throughout the lifetime of your application. As with any relationship requiring a high level of commitment, it is best to proceed with your eyes open!
Some developers question the usefulness of frameworks in a J2EE environment, believing instead that J2EE itself provides all of the services required. Although it is true that the J2EE platform provides a highly valuable set of tools, services, and APIs, it is not a substitute for a well-crafted framework. Most modern frameworks work should allow the construction of applications with or without the full J2EE infrastructure, particularly the EJB portion of the specification. EJBs are not the only distributed component model, nor are they in all circumstances the best choice.
A framework, then, should augment and make use of J2EE, building on its strengths and addressing its weaknesses, thereby allowing the developer to make the appropriate choices of technologies for any given project.
A framework flows over into your deployment and maintenance phases of your project; it also is involved in design. Given the reliance of frameworks on design patterns, it makes sense that those same patterns can be used even in the design stage, while your application is still being worked out. Indeed, it makes little sense to settle on a framework and not use its design patterns in your project; it will make implementing in that framework more difficult when the time comes. A good framework can lend a certain amount of "paint-by-numbers" to your design process: You know what's available, and can work from there to decide how those capabilities are best applied to your specific needs. Of course, there's no reason you cannot also develop custom functionality, if there is something not supplied by the framework, but there should be an using existing functionality. If you find yourself having to code too much from scratch, you may have the wrong framework, or may be applying it incorrectly.
Many frameworks began by attempting to address one portion of application development—database access, for example—and then expanded to encompass other areas, such as presentation. Frameworks initially implied a certain amount of lock-in by providing such end-to-end services: Once your application was using the framework, it was difficult to change your mind and plug in services or elements from other projects. As component-based development became more popular and effective, however, the better frameworks reduced this tie in, as you will see.
Frameworks fit into virtually every phase of a development project's life cycle. Although they are not a design tool per se, their capabilities are definitely taken into account and can substantially simplify application design. They cut down on effort considerably in the development phase, and virtually every framework contains provisions for assisting in testing. The framework becomes an essential part of your application for production, and its facilities substantially reduce the maintenance burden.
Components and Services
Every framework provides services. These services are used either directly in the creation or management of your application code, or are used as part of your application to support its functionality, like Avalon. Either way, if you identify the services being provided, you have made a start towards isolating your application logic from those services, while still taking advantage of them.
The goal of component-based development is to assemble applications, not create them from scratch every time. By determining what services a project offers you, you then can determine the components you need to access from those services to use them. For example, if you look at the example of the Turbine framework, you see a caching service you want to take advantage of. In Turbine's case, it's easy to identify the services: The framework is built around services. For other projects, it will not be as simple, but the services are there nonetheless. Once you have identified a service you want to take advantage of, you need to identify and isolate the components you have to use to access that service. Again using Turbine's Cache as an example, the TurbineGlobalCacheService object is your top-level interface to the Turbine Cache.
At this point, you have identified an implementation of a service, and the primary component (or components) that you need to access this service. This is where you bring into play the separation of a service into a role and an implementation of that role. Just like in a play, a role is an abstract, not a concrete person. When you say "get Hamlet up here", you don't get out a shovel; you are referring to the person who is playing the role Hamlet—whose name actually might be Frank. You might have an understudy, Joe, who also is perfectly capable of playing Hamlet; in case your original Hamlet falls ill, this person is a second implementation of the role "Hamlet." In the script, however (the application logic in your analogy) you don't read "Joe says this," you read "Hamlet says this," and it is understood that you are referring to the current implementation of Hamlet.
Just so in your application, you create a role, say "Cache," that describes (via an interface) the function of a service that provides caching. In actual use, configuration associates the abstract "Cache" with the concrete "TurbineGlobalCacheService". It is the implementation that does all the work, but your code never accesses TurbineGlobalCacheService directly; in fact, it never even imports the Java package that contains TurbineGlobalCacheService. You deal only with "Cache", and if the actor playing "Cache" is changed for another capable actor, there is no difference to your "script." The play's the thing, after all!
By decomposing your design into services, and then defining the requirements of our services in terms of replaceable components, you remain independent of implementation. By keeping the individual elements of your application logic small and well-focused, you also maintain the ability to drive the application flow from an external source (such as workflow).