DekGenius.com
[ Team LiB ] Previous Section Next Section

1.4 Core Development Concepts

When evaluating the end product of any enterprise development project, we can score it on four factors: Extensibility, Scalability, Reliability, and Timeliness. Different projects emphasize these factors to different degrees: NASA programmers will emphasize reliability above all else, giving appropriately short shrift to timeliness concerns. A startup may emphasize scalability and timeliness, with concerns over extensibility put off for the next release.[3]

[3] Of course, there are other considerations that come into play on individual projects, such as manageability and usability, but we've found this set to be appropriate for our purposes.

Obviously, each of the four issues affects the others at some level. A highly extensible system might be made more scalable by plugging in higher performance components, and time spent up front building support for scalability will pay off in timely deployment of later versions. The important thing to know is that design patterns can improve performance in all four areas. In this book, we focus on extensibility and scalability in particular.

1.4.1 Extensibility

The one constant in software development is that requirements always change. With each version of a product, there are bugs to fix and ideas to develop into new features. These days, particularly in business programming, requirements often change between the time a product is designed and the time it is released. When requirements do change, software divides into two categories: the kind that can be easily extended and the kind that can't. Unfortunately, determining in advance which category your program fits into is difficult. If you were trying to extend a toaster, it might be easier to add the ability to toast bagels than bake cakes.

In general, the extensibility of software determines how easily changes can be accommodated. It is easy to say whether the program was extensible or not in hindsight, but for the program to be really useful, we must have a sense beforehand. In a first version, it is sufficient to show that a program avoids the most common extensibility pitfalls. Once a few versions have been released, empirical evidence provides a much clearer picture.

There are a few common reasons that programs are hard to change. One that occurs most frequently is the fear of breaking supposedly unrelated code. We've all been in the situation where a seemingly innocuous change to one object has caused another, seemingly unrelated part of the program to suddenly stop working. This kind of problem is usually discovered weeks later, and you get stuck revisiting several weeks worth of changes to find the single, obscure dependency you broke.

As an application grows, the number of dependencies tends to go up, and consequently the amount of testing necessary after each change. Eventually, the fear of changing anything outweighs the benefits of new features, and development grinds to a halt. While this scenario sounds extreme, it is familiar to anyone who has worked on a large legacy application. In the enterprise environment the situation can get even worse, as changes to one application can affect entirely separate—but nonetheless related—systems. Imagine a financial system that starts requiring all transactions in Euros: if the purchasing computers aren't switched over at the same moment, chaos will ensue.

Even if you know all the dependencies, it can still be hard to change code. Changing one object's interface means not only changing that object, but also updating all the objects that rely on the interface. While providing backward-compatible interfaces is often a good compromise, this too becomes impossible after a few generations. In the end, someone gets stuck updating all the dependent classes—a tedious, thankless task, to say the least.

In short, be careful with dependencies. While objects must interact with each other in order for a program to do anything, the number of dependencies should be limited. In an extensible program, interfaces are kept as tight as possible to make sure objects only interact in well-known ways. Of course, there is a tradeoff here, as always. Components with rich interfaces can be more useful than components without them, but by their very nature, richer interfaces create tighter dependencies with other components.

Using clear interfaces separates an object's implementation from its interactions. This makes the specific implementation independent of the rest of the application, allowing implementations to be fixed, changed, or replaced at will. The application is no longer a single piece, but a collection of semi-independent components. Multiple developers can work on components without breaking, or even knowing about, the larger application. Components provide a certain functionality, which can be tested on its own and used in multiple applications. Multiple components can also be grouped into a larger entity that can itself vary and be reused. By using these larger components, an application can gain functionality without getting more complicated or sprouting more dependencies. In many ways, extensibility is simply a measure of how easy it is to understand the code. If the division of labor among components and the interfaces between them are clear, software is easy to extend. It can be worked on independently by a number of people without fear of interference, and new functionality does not further complicate the program.

Design patterns can help solve many extensibility problems. By documenting a particular aspect of architecture, a design pattern makes it easier to understand. Design patterns can be communicated to other developers, minimizing the risk that someone who does not fully understand the architecture will break other parts of the application. Most importantly, even the act of evaluating design patterns—whether they turn out to be applicable or not—forces developers to think out and articulate their design up front, often exposing flaws in the process.

1.4.1.1 Techniques for extensibility

Successful applications are constantly enhanced and extended, but these updates can come at substantial cost. Small design flaws in the original program are quickly magnified. All too often, the fix to one problem creates yet another problem that requires a new fix. These layered fixes make the code unwieldy and reduce opportunities for reuse. As more and more special cases are added, exhaustive testing becomes nearly impossible. Eventually, even adding a simple new feature becomes prohibitively expensive.

To make life even more complicated, these overgrown programs become increasingly difficult to understand. If learning one area of a program relies on learning five others first, it's unlikely that developers will be able to learn it fast. One person can reasonably build a text editor; however, he would have to be very dedicated to add a spellchecker, web browser, and other features. A clean, consistent base architecture allows many developers to contribute to the application.

The hallmark of extensible software is that it is designed to change. Whether you are working on an initial design or refactoring an existing one, there are several generic techniques that can make your designs more extensible:

Decoupling

We have already talked a little bit about loose and tight coupling. In a loosely coupled system, components can vary independently. They can be prototyped, updated and replaced without affecting other components. Because of this, loosely coupled systems will generally be more extensible.

Centralizing

When functionality is spread out over multiple components, making simple changes may require updating many parts of the code. It also makes the code harder to follow and therefore harder to understand and share. By gathering common functionality into central resources, the application becomes easier to understand, update, and extend.

Reusing

Adding too much functionality to a single component specializes it. A specialized component cannot easily be adapted to perform other functions, so code must be duplicated. This duplication makes the code harder to maintain and update. A design in which common functionality is encapsulated in reusable components is more extensible, because larger components can be composed effectively from specialized existing components. Design patterns may well have their most profound impact in the area of extensible systems. We'll use all three of these approaches in the chapters ahead.

1.4.2 Scalability

When building desktop applications, you generally have the ability to define your platform, at least in broad terms. A word processor may have to deal with documents from 1-1000 pages long, but it won't have to deal with 1-1000 users editing the document at the same time. Assuming your test lab is outfitted realistically, it is possible to determine whether an application will perform appropriately. With one user, a program only needs to perform one task, or at most two or three related tasks, at a time. The time it takes to perform each task is a measure of the application's performance. Performance depends on the task itself and the speed of the underlying hardware, but that's about all.

Enterprise applications aren't that simple. The time it takes for a web server to process a request certainly depends on the performance of the server application and hardware, but it also depends on how many other requests are being processed at the same time on that server. If the application involves computing by multiple servers, the web server's speed will also depend on the speed of those other servers, how busy they are, and the network delays between them. Worst of all, the speed of transmitting the request and response—which is based on the speed of all the networks in between the user and the server—factors into the user's perception of how fast a transaction was processed.

While developers generally can't control network speed, there are things they can control. They can modify how quickly an application responds to a single request, increasing its performance. They can also vary how many requests a server can handle at the same time—a measure of the application's scalability.

Scalability and performance are intimately related, but they are not the same thing. By increasing the performance of an application, each request takes less time to process. Making each transaction shorter would seem to imply that more transactions could be performed in the same fixed time, meaning scalability has increased. This isn't always the case. Increasing the amount of memory used to process each request can increase performance by allowing the server to cache frequently used information, but it will limit scalability, since each request uses a greater percentage of the server's total memory; above a certain point, requests have to be queued, or the server must resort to virtual memory, decreasing both scalability and performance.

Scalability can be broadly defined as the ability of an application to maintain performance as the number of requests increases. The best possible case is a constant response time, where the time it takes to process a request stays the same regardless of the load on the server. Ideally, an enterprise application will be able to maintain a more or less constant response time as the number of clients reaches the standard load for the application. If a web site needs to serve 200 requests a second, it should be able to serve any one of those 200 requests in the same amount of time as any other request. Furthermore, that amount of time needs to be reasonable, given the nature of the application. Keeping the user waiting more than a half second for a page is generally not acceptable.

Linear scalability is when the time it takes to process n requests is equal to n times the time to process one request. So if one user gets a response in 1 second, 10 simultaneous users will each have to wait 10 seconds, as each second of processing time is divided 10 ways. Enterprise applications may hit linear scalability when under a particularly heavy load (such as after they have been linked to by http://www.slashdot.org). If the load on the server goes up to 400 users a second, the time required for each response might double from the 200 user level.

At some point, a program reaches its scalability limit, the maximum number of clients it can support. An application's scalability is usually a combination of all three factors: constant response time up to a certain number of clients (ideally to the maximum number of users the application needs to serve), followed by linear scalability degrading until the scalability limit is reached. Figure 1-2 shows a graph of performance versus number of clients, which is how scalability is usually represented.

Figure 1-2. A graph of scalability
figs/j2ee_0102.gif

Building scalable systems almost inevitably involves a trade-off with extensibility. Sometimes breaking a larger component into smaller components, each of which can be replicated multiple times as needed, increases scalability. But more often, the overhead of communicating between components limits scalability, as does the increased number of objects floating around the system.

The design patterns in this book often focus on the interactions between application tiers. These interactions are where most scalability problems initially appear. Using effective practices to link these tiers can overcome many of the performance debts incurred by separating the tiers in the first place. It's not quite the best of both worlds, but it is usually a good start.

Of course, most systems do not need to be designed for unlimited scalability. In many cases—particularly when developing systems for use within a defined group of users (the case for most intranet applications)—only a certain number of clients need to be supported, and the trade-off between scalability and extensibility tilts toward the latter.

Design patterns support scalability in a number of ways, but primarily by providing a set of approaches to allow resources to be used efficiently, so that servicing n clients doesn't require n sets of resources. In addition, patterns can enable extensibility, and extensible systems can often use that extensibility to improve scalability by distributing operations across multiple servers, plugging in higher performance components, and even by making it easier to move an application to a more powerful server.

1.4.3 Reliability

Reliable software performs as expected, all the time. The user can access the software, the same inputs produce the same outputs, and the outputs are created in accordance with the software's stated purpose. Needless to say, complete requirements gathering is vital to ensuring software reliability, since without clear requirements there is no way to define what correct behavior actually involves. Requirements gathering is also important in figuring out what constitutes a reliable system: does the system need to stay up 99.999% of the time, or can it be taken down for maintenance for 2 hours a night?

Similar to scalability, a reliable system depends on the predictability of its underlying components. From a user's point of view, reliability is judged for the entire system, including hardware, software, and network elements. If a single component malfunctions and the user cannot access the application or it does not work correctly, the entire system is unreliable.

Corporate software projects are subject to specific quality requirements, and reliability is usually first among these. Most larger software teams have one or more people in a Quality Assurance role, or they make use of the services of a dedicated QA team or department. Larger projects, particularly in regulated industries such as health care, are subject to software validation processes and audits. A software audit can include every aspect of the development cycle, from initial requirements gathering through design, up to final testing and release procedures.

Design patterns can play a major role in ensuring reliability. Most of the patterns in this book are acknowledged, at least by some, as best practices within the industry. All of them have been applied countless times in enterprise application development projects. Design patterns can be validated at a high level and incorporated early on in the design process. This kind of planning makes the final validation process simpler, and generally produces code that is easier to audit in the first place.

1.4.4 Timeliness

The final goal of any software development project is a timely delivery of the finished software to the end users. At least, that's the way the end users generally see it! Design patterns might have less impact in this area than in the other three, although having a catalog of proven solutions to standard development issues can be a timesaver during the implementation phase.

The real time savings tend to come in subsequent release cycles and in projects that rely on an iterative development methodology. Since most patterns involve some sort of modular design, applications that use them will be easier to extend in the future, providing timeliness advantages to the next generation of software as well. Programmers can understand the structure of the application more easily, and the structure lends itself more readily to change: Version 2.0 can be made available much more readily than would otherwise be possible.

It is possible, of course, for patterns to negatively affect a project schedule. Solving a problem by writing code that conforms to a generic design pattern may take more time than solving the problem in a more direct fashion, although this investment is often recouped in later phases of the development process. But complex patterns can also introduce complexity where none is required. For enterprise applications, though, the balance tilts towards the patterns approach. Major systems are rarely, if ever, designed and implemented as one-offs: the ROI calculations at corporate headquarters assume that a project will be available through several years and likely through several new sets of features.

Later in this book, we discuss refactoring, the process of transforming existing software into better software by revisiting its various design assumptions and implementation strategies, and replacing them with more efficient versions. After learning an effective way to solve a problem, it is often tempting to race back to older code, rip half of it out, and replace it with a better implementation. Sometimes this philosophy leads to real benefits, and sometimes it leads to wasting time solving problems that aren't really problems.

    [ Team LiB ] Previous Section Next Section