In order to truly understand Seedling, one must understand the problems that it is attempting to address. These problems involve many aspects of component-based application development.
This chapter begins by discussing the general nature of a component, the fundamental unit of composition within a Seedling application. We then examine issues that arise when one attempts to build an application from components. Finally, we derive some specific design goals that would solve (or at least reduce) the most common problems.
The term component has been given many different definitions by many authors, but Seedling uses the word in a fairly specific manner. We define a component as a class or object that is designed for independent reuse and composition. Let's look at that definition in parts.
class or object: We will at times refer to both classes and objects (instances) as components. Most of the time, a “component class” will be specifically designed for use within a component framework; the specific benefits of components don't occur by accident. An object becomes a component when it is composed with other components within the framework. What makes a class a component is its design intent; what makes an object a component is the way it is used in a specific application.
independent reuse: The primary goal of component-based design is to enable reuse, and thus streamline application development. Component classes should be flexible and configurable, so that they can be applied to many different contexts. A class that cannot be used in multiple contexts is probably not useful as a component.
composition: A component is only part of an application, so that any useful program will involve connecting (“composing”) multiple components. A small application may only require a handful of components, but larger systems will make use of hundreds or even thousands of components. Although it is possible to write code to handle this composition process, the problems with doing so are numerous, so it is advisable to use a component framework to manage the complexity.
Note | |
---|---|
Components are closely related to design patterns, but are generally at a higher level. A Visitor won't be a component, but Singletons, Facades, and Factories typically are. In fact, a good component framework solves many of the same problems addressed by such design patterns. |
It's generally accepted that hard-coded values are to be avoided in source code. We should not assume that our HTTP server component must listen on port 80. We must not assume that our database connection factory utilizes the “production” catalog.
Components support good practice by providing properties to hold such values. A property is a storage location encapsulated within an object, along with programmatic read- and/or write-access via methods.
Note | |
---|---|
Since the Java language does not directly support properties, Seedling uses the idioms established by the JavaBeans specification. The simplest properties involve pairs of “getter” and “setter” methods that meet certain naming conventions. |
A well-designed reusable component is likely to have several properties that must be configured (assigned values) to make the component functional. This idiom allows us to avoid hard-coding values.
A well-designed component will have low coupling, so that its dependencies are limited. This increases the reusability of the component, because it has less “overhead”, fewer requirements, and can therefore be used in a broader range of applications.
An object that does too many things can be hard to reuse, because in many cases the user will only want some subset of the features. Objects with a broad scope will usually require lots of configuration, again making it harder to reuse. A good component follows the maxim to “do one thing, and do it well.”
Many developers new to component-based development find themselves confronted with a steep learning curve. Although individual components are relatively easy to design and use, there is often the sense that it’s a great deal of work for little benefit. Part of the problem lies in the fact that components themselves are not terribly powerful. After all, they are just specialized objects.
This real impact of components isn’t understood until you assemble many components, putting the pieces together into a complete, working application. Since components tend to have fine-grained scope of responsibilities, any realistic application will involve a large number of components. These objects are composed together strategically to express the broader application architecture. At this level you can begin to sense how the component mindset enables architectures that are both flexible and comprehensible.
This leads to an interesting set of challenges. Your architecture requires a number of components that interlock in particular ways. Each component must be instantiated and configured to refer to other components, as well as any lower-level support objects. How does your application determine what to construct, and when? How does it determine the configuration of an individual component?
It is the job of the component framework to provide pragmatic solutions to these and many other challenges. Now we'll consider the problems in detail, and then derive design goals that should help to solve the problems.
As discussed above, components are property-driven so we can avoid using hard-coded values. If we shouldn't put configuration values into our Java code, where does it go? There must necessarily be resources external to the code that provide these values. Given that, the next question is: how much of the configuration should be specified externally?
We can identify three general areas or dimensions of configuration. First, when is the component created, and when is it destroyed? Second, what is the type of the component? Third, how are the component's properties configured, and how is the component composed with others? As we'll now see, a component framework should support each of these dimensions of configuration in order to meet the needs of a real-life application development and deployment.
A component's first dimension of configuration is its lifetime. It is generally beneficial to avoid instantiating components unless they are actually needed by the application. For example, a persistence component that opens a pool of database connections should not be instantiated unless the persistence layer is actually used. However, dynamic configuration implies that we may not know until runtime whether this is the case. This means that the framework must support lazy instantiation of components (that is, waiting to create a component until it is first accessed). A more complex example is that of HTTP sessions: a Web application server may need to construct several components for each new browser session, making use of lazy instantiation.
On the other hand, it is also desirable to destroy components when they are no longer needed. For example, when a user's session expires on a Web server, the server should release all of the components and other resources that were allocated to that session. Thus, the framework needs mechanisms to track components throughout the runtime, and ways to add and remove components as necessary.
It is also extremely useful to be able to change the concrete class of a component at different stages of development. For example, it's a common testing technique to replace a database-backed storage layer with an in-memory version that trades long-term capacity and reliability for high speed. If both versions implement a common persistence interface, then a simple configuration change (perhaps as trivial as simply changing a single property defining the name of the concrete class) can switch between the deployable application and one streamlined for testing.
Many well-known design patterns exist to support such late-binding of type, particularly Abstract Factory, Singleton, Facade, and Strategy. However, flexible configuration of types within the component framework greatly reduces the need for these patterns.
Many components will have properties that require different values at each deployment, and at different stages of the build-test-deploy cycle. For example, a server component that responds to requests from the network should have a property that defines the TCP port on which to listen. This port number will probably be different on a developer's machine, during integration testing, and at deployment. This implies that the value must not be hard-coded in the Java, because we must be able to change it at deployment time without recompiling. This in turn implies the need for configuration resources outside the application proper.
The situation becomes even more interesting when we recognize that no component exists in a vacuum. In order to construct a running application from components, they need to be linked together. The outputs of one component become inputs of another. One component provides a service used by many components throughout the system. The components form an arbitrary web of objects that becomes increasingly complex as an application grows in scope and scale. Beyond the problem of just keeping track of all these components, we must be able to specify what connections are made. For example, if our networking framework provides several TCP connection factories (secure vs. insecure, or maybe a factory for each of several servers), each component making use of the framework must be able to specify which factory it uses. Thus we need a way to identify individual component instances at runtime. Furthermore, we may want to reconfigure any connection (which is really just another property) at deployment time, so the identities must be readable and manageable to humans.
Assuming that we've solved the problems involved with configuring individual components, now we discover another challenge: just getting all of the components constructed in an appropriate order can be extraordinarily daunting task. Writing custom code to handle this process is cumbersome and fragile, especially when the other dimensions are taken into account. For example, if we attempt to integrate a significant new subsystem, there may be a large number of new components to create, and many connections to and from existing components. Changing the type of a component, or changing a connection or two to refer to a different component, can require changing the order in which the components are constructed (and can even affect whether or not a particular component needs to be constructed at all). Our budding component framework must respond to this challenge by automatically determining the order in which components are to be created.
install many components together as a complete subsystem
allows integration of components from multiple sources
natural unit of distribution and deployment
The challenges presented above lead to the following design goals:
The framework must be able to dynamically control when components are installed and removed. There should be a facility to automate this when many components have similar lifetime.
The framework must be able to delay binding values of component properties, including the concrete type of each component and links between components, at least until deployment time, and potentially until application launch.
The framework must allow the developer to configure components through resources outside of Java code. We call this capability external configuration.
The external configuration resources must be able to identify individual components via some kind of namespace. Since a large number of components may be involved, there must be some mechanism for organizing the namespace.
The framework must automatically construct, configure, and compose the necessary components as declared by the external configuration. This process must be sensitive to appropriate order of construction, to ensure that components are brought into a well-defined state.
Seedling is a platform designed to provide a powerful way to develop and deploy component-based applications. It is a foundation upon which a developer can express the architecture of her program in terms of components. Seedling can be seen as a “component container” that provides a role similar to that of a J2EE Web container or JNDI, albiet in a much more light-weight fashion so that it can be used across a broader range of applications.
Seedling provides a structural model of component composition, but it does not require or enforce component specifications or contracts. That is, Seedling says nothing about how the components work and interoperate. Your components can be as tightly- or loosely-specified as you want.
In order to be a generic as possible, Seedling does not require the use of particular coding idioms, design patterns, or architectural styles. It is entirely possible to construct a Seedling application from custom components that have no coupling whatsoever to any Seedling classes.
Seedling provides advanced facilities for composition and deployment. In Seedling, components are deployed as nodes in a hierarchical namespace. Each node has a unique address within the tree, which eases documentation, and provides a mechanism for composing objects: instead of writing code to connect actual object instances together, we write configuration files that connect nodes by name.