It is pretty well accepted now in development circles that systems usually can be built with components, especially pre-existing components, more efficiently than they can without. As a result, component development and the building of systems with those components has become the focus of more developers than ever before.
As more people work with components, examples of the common pitfalls and
problems start to come to light. In this article, you are presented with ten of
the most common mistakes made when developing component-based systems and the
components that make them up. Although the focus is on Java-based
components, most of these problems occur within other languages as well.
1. Fitting the Wrong Design into Components
The first, and perhaps most common, component mistake seen is when developers attempt to “force” a design into a component pattern when it was not originally thought out that way. Although it is quite possible to occasionally find a section of an existing system that can be “broken out” into a component or service, it is more common to find a retrofit involving components that attempts to preserve too much of the existing design at an implementation level. This is true even if you’re only starting from a design, rather than an existing system. This often happens when the specifications of the system are followed too literally, or followed from a user-interface point of view rather than a perspective of components and services interacting to provide functionality. When thinking components for services, the user interface is not normally that closely tied to the component structure behind the scenes, whereas some older systems might have a module for each user “screen” or “page.”
Sometimes, components are force-fit onto a back end in an attempt to gain the benefits of components, only resulting in another developer thinking components aren’t worth the trouble!
Design of component-based systems is covered extensively elsewhere, and I won’t try to describe it here—suffice it to say that if the design doesn’t take components into account, no amount of code can make up the difference.
The solution is not to “postpone” components in your system, but take them into account from the initial design. Then, the coding will flow more naturally from the design. When retrofitting an existing system, isolate the areas where component-based design can be applied, and consider creating an API to the remainder of the system, maintaining the distinctiveness of the new components.
2. Writing an “Almost Component”
The second common problem I see is the “near miss”—a chunk of code that started off right, and nearly became a functional component, but then somehow went astray. This often happens when the final stages of development are rushed for budget or time constraints, and developers need to “make it work” rather than “make it right.” A few sections of an otherwise good component are hard-wired to, say, an underlying database layer, or a last-minute interdependency is injected into an otherwise well-isolated design.
This is much more common than we’d like to believe, and again results in components in general getting a bad name when the project is finished. A “near miss” ends up being just as much (if not more) work than a “good” component, but with few of the benefits.
3. Tight Coupling
A feature of a well-written component is its independence—not only from the remainder of the system, but also from other components. Of course, it is often valuable to have components use other components, but an over-reliance on this capability can result in a tangled web of dependencies that reduces the re-usability and maintainability of many components—and conspires to defeat one of the major advantages of component-based development.
As a general rule, components should never depend on other components—although they might depend on the interface to other components (or services). The difference here is large: A direct dependency is very hard to change later, whereas a dependency on an API allows a component simply to be swapped out with another that implements the same API.
Now, dependencies at this level (designed component dependencies) are a different animal than library dependencies (for example, where your application relies on the presence of certain JAR files to compile/run). External library dependencies, however, should also be minimized wherever possible, and definitely recorded formally in conjunction with the components they support (for example, JAR file X is required by component Y). An excellent system for organizing such things is the Apache Maven project.
4. More Plumbing than Pipes
Another frequent mistake when working with components is spending too much time building the stove, and not enough time cooking the meal. It is easy to get caught up in the ongoing debate about what component framework to use, and to decide that the right decision is not to use any—but to build your own. Although there may be rare circumstances where this is appropriate, in the vast majority of projects there are simply too many excellent choices out there for component frameworks—available under a wide selection of licenses, from commercial to open source to free software—to make it worthwhile building your own.
Also, the apparent simplicity of some frameworks might fool you into thinking they’re quick and easy to build. Perhaps some are—but not the good ones. The small optimizations and discoveries that come about once a component framework has been in use for a while, and has been used for a number of different projects all contribute much to its utility. Re-discovering these subtleties is expensive and time-consuming work; be sure you have a good reason to do it before you decide to re-invent any wheels.
At the opposite extreme from trying to “roll your own” component framework is the mistake of trying to make use of components with essentially no framework at all—or with custom code trying to “glue” components together into a cohesive system. The advantages lost when making use of a non-standard mechanism for working with components are considerable.
5. The “One-Time” Component
There are many times you will encounter a situation where you think “this is a one-time situation—there’s no way that it makes sense to make this a re-usable component.” This will be followed by the temptation to short-circuit the design process, because what you are now designing is a “single purpose” component. Resist this urge whenever possible because the benefits of components are often not clear at this point of the process. Later, when you’re refining the system, you’ll regret having built any “one-offs.” Keep re-usability, external configurability, and all the other component design principals in mind, and most of the time you’ll discover that the overall effort to stick to good component design ends up worth it.
Good components are not “faster” to write than one-offs, although they are often easier. They’re always easier to test and debug, however, and coding is only one stage in the life-cycle of your component. Component-based systems are almost always faster to construct entirely (when you include time/cost of testing and so forth).
6. Subtle Dependencies
While we’ve discussed the common mistake of making coupling between components too “tight,” there is another, related mistake that is also very common. When attempting to reduce the dependencies of a component, to make it more portable and re-usable, it is often the “subtle” dependences that are overlooked. For example, the need for a component to have access to external files, or to a database with a certain name, might not be seen as “dependencies,” and yet they are. Fortunately, these kinds of dependencies are easily detected with a careful unit-test scenario, which can be performed on a separate machine. You do have extensive unit tests, don’t you?
7. Crossing Over
You have UI components getting mixed up in the service layer.
8. Everything Is a Nail
In exercising the entirely correct principals of good component design, developers look for re-use opportunities. This is a good thing, and good component design makes re-use easy and painless. A somewhat less common mistake is to take this approach too far: Developers who become familiar with the principles of component design can go on a bit of a “component rampage,” component-izing to a degree that is not necessary. If you’re trying to write a utility to clean up log files, you probably don’t need to go entirely overboard on component design, frameworks, independence, and so forth. It is important to use good judgment in how far to take the component process, so that the benefits are still outweighing the work required. Just because you can doesn’t necessarily mean you should!
Another manifestation of the “everything is a nail” approach:
9. Size Matters
In a sense, this problem is the opposite of the last mistake (Everything Is a Nail). If a single component grows beyond a certain size and complexity, it ceases to become a good component. Exactly what measure can be used to determine this limit is hard to say—it’s not necessarily lines of code. The number of methods exposed by the components API is a better indicator—too many can be a sure sign that your component should be broken down further.
It is helpful to think of the next step up from components as “services,” A group of related components that all together help perform a certain defined set of operations can be collected into a service—and a single API used to expose that service to the rest of the system. Internally, you then can break down into components further, and some of those components can even be used in other services. This reduces the problem of not wanting to componentize too far and thus hit the point where the exposed API is incomprehensible.
10. Not Declaring Your Independence
An essential attribute of a component is a clearly defined API by which other components and software can interact with it. This API needs to be—to mangle a famous saying—as simple as possible but no simpler. The more that complex data types are used as interface method arguments, and the more interface methods there are, the higher the chance of introducing unwanted dependencies into your component.
Despite all of these potential pitfalls, the benefits of a properly designed and well-implemented component-based system far outweigh the disadvantages. The greatest mistakes a developer can make with components is to fail to employ them at all!
About the Author
Michael Nash is the president of JGlobal Limited, a software development, consulting, training and support company specializing in open source Java technologies. He is also a core developer of the Keel meta-framework, the author of two books and a number of articles and papers about next-generation web-application development with Java, and a member of the JSR-127 (JavaServer Faces) Expert Group. He can be reached at [email protected]