At last year's JavaOne conference, I attended a session in which the speaker talked about Sun's plan for the Java virtual machine (JVM). In this talk, the speaker stated that Sun planned, among other things, to clear up current performance bottlenecks in its virtual machine, such as the slowness of synchronized methods and the performance costs of garbage collection. The speaker stated Sun's goal: With the improvements to the JVM, programmers would not need to think about avoiding virtual machine bottlenecks when they designed their programs; they would only need to think of creating "good object-oriented, thread-safe designs."
The speaker did not, however, elaborate on what actually constitutes a good object-oriented, thread-safe design. That is the aim of this new column. Through the articles of the Design Techniques column, I hope to answer the question: What is a good Java program design, and how do you create one?
The column's focus
My focus in this column will be to provide practical design techniques that you can put to use in your everyday programming tasks. I'll assume you are familiar with the Java language and APIs. I plan to discuss techniques, ideas, and guidelines that will help you use the language and APIs in your real-world programs.
To give you an idea of what to expect in this column, here is a list of the kinds of topics I plan to write about:
Much of the material that has already been written about software design can be applied to Java. There are many full-featured design methodologies and thick textbooks that describe them. In this column I won't promote one methodology over another. Nor will I promote a new methodology of my own invention. Rather, I will draw on and combine insights that I've gained from several existing methodologies and found to be useful in my own programming practice.
The approach to design that I will recommend in these articles arises out of my experiences over the years in the cubicle: designing new software, enhancing old software, maintaining software written by others, maintaining software written by myself, working with various languages, tools, computers, and other programmable machines. My design philosophy will be very "cubicle-oriented": based on, and geared toward, real-world commercial programming.
This month: Process described, "design" defined
In this initial article of the Design Techniques column, I will provide a detailed account of the concept of software design based on my own experience as a developer. In the remainder of this article, I'll discuss the process of software development and explain what I mean by the term "design."
The software development process
In my experience, the process of software development tends to be rather chaotic. Team members come and go, requirements change, schedules change, entire projects get canceled, entire companies go out of business, and so on. The programmer's job is to successfully navigate this chaos and in the end produce a "quality" product in a "timely" manner.
Besides being chaotic, the software development process also tends to be rather iterative. As a software product is developed, it continuously evolves based on feedback from many parties. This iterative process works from release to release (each release is one iteration) and within the development cycle of a single release. From release to release, for example, the feedback of customers with the current version indicates which bug-fixes and enhancements are most important to make in the next version. Within the development cycle of a single release, the vision of the end target is continuously adjusted by forces within the company as development progresses.
Despite the chaos and iteration, however, I have found that most development teams try to enforce some structure on their development efforts. For the purposes of this column, I'll loosely divide the software development process of a single release cycle into these four phases:
With these four phases I intend to capture a structure that I have observed in most software development projects. Because each company is different, each team is different, and each project is different, these four phases form only a rough outline of a typical development cycle. In practice, some phases may be skipped or may happen in a different order. And because the iterative nature of software development tends to bubble up through any imposed structure, these phases may to some extent overlap or bleed into one another.
When I talk about design in the Design Techniques column, I am talking about the activities that take place during step two of the above list. To give you a better idea of what I mean by each phase, I describe each individually in the next four sections.
Phase 1: Specifying the problem domain
The specification phase of a software project involves bringing together all the concerned parties to discuss and define the end product of the software development process. During specification, you define "the vision" -- the target you will aim at for the remainder of the project. The deliverable that should come out of the specification phase is a written document that defines the requirements of the software system.
The requirements specification is much like a contract. It is a contract between all the concerned parties, but most importantly from the developer's perspective, it is a contract between the developer and whatever party desires the end product in the first place: perhaps a client, a customer, management, or the marketing department. When a specification is agreed to in spoken terms but is not written down, it is basically an oral contract. Although an oral contract is legally binding, in many cases not having something written down is a recipe for trouble. Different people tend to have different recollections of oral agreements, especially when it comes to details. A disagreement on details is even more likely if the details were never discussed as part of the oral agreement in the first place, which is a common feature of oral contracts.
When all the parties involved get together and try to write down the requirements of a software project, it forces an exploration of the problem domain. The problem domain is the end product described in a human (not computer programming) language. The same end product expressed in a computer language is the solution domain. In the course of exploring the problem domain, many ambiguous details can be identified and discussed, and disagreements can be resolved right from the beginning.
A good specification gives you a well-defined target to aim for as you develop. But it doesn't guarantee that the target won't move. Some adjustments in the vision of the end product are almost inevitable during the design and implementation phases; however, a good specification can help reduce the magnitude of such adjustments. Skipping the specification phase, or not covering the details sufficiently, can lead to the same kind of misunderstanding between parties that can occur with an oral contract. Thus, having a good specification first helps advance the subsequent design and implementation phases to a successful conclusion.
Phase 2: Designing the solution domain
Once you have a written specification that everyone involved agrees to, you are ready for what I call the design phase -- the process of planning, and in some way documenting, the architecture of your solution domain. I include many activities under the name "design," including:
Defining the system:
Building user interface prototypes:
Doing object-oriented design:
Defining the system
As a first step in the design phase, you must partition your system into its component parts. For example, you may require several processes at various places on a network. You may have some applets and some applications. Some components of the system may be destined to be written in Java and others not. If you want to use JDBC, you may need to select a third-party JDBC library that will enable you to access the database of your choice. All these decisions must be made before you can begin any object-oriented designs of the individual programs in the system.
As you define the system, you will likely want to document your work in one or more technical specifications. Documentation allows you to communicate the design to other interested parties in the organization and to get their feedback. You can pass out the specification, call a design review meeting, then present the system design at the meeting. The group can discuss your design and hopefully find any problems and make suggestions. Getting feedback -- and making adjustments to your system design as a result of the feedback -- is an example of iteration in the process of software development.
Building user interface prototypes
Building a user interface prototype is often a valuable activity during the design phase. Once the user interface prototype is completed, the parties that agreed to the specification can gather together again to review the preview version. Having a prototype gives the parties another chance to visualize and discuss the end target. By requiring everyone who agreed to the specification to review and sign off on a user interface prototype, you help ensure that all parties have compatible expectations for the end product. With the visual tools available today for the development of Java-based user interfaces, developing a user interface prototype can be very fast, and the end result is a framework of Java code that you can then endow with functionality during the implementation phase.
Note that the process of demonstrating a user interface prototype is a prime example of the iterative nature of the development process. When the interested parties (who have all agreed on a written specification) actually see user interface prototypes, they often have new ideas, or a better understanding, or a more detailed understanding -- in other words, a clearer vision -- of the end product. During the demonstration, some adjustments may be made to the specification. By this time, however, hopefully the adjustments will be minor.
Doing an object-oriented design
As you design a Java program, you must think in terms of all the programming technologies offered by the Java language, including multithreading, garbage collection, structured error-handling, and object orientation. Yet, because the dominant architectural characteristic of the Java programming language is object orientation, a Java program design phase is fundamentally a process of object-oriented design.
Doing an object-oriented design involves creating inheritance hierarchies and designing the fields and methods of individual classes and interfaces. Three basic categories of classes that you will come up with in a design are:
User interface classes are those that compose the program's user interface, such as classes that represent windows and dialogs. Problem domain classes are those that represent objects that you identified in the problem domain. For example, if your problem domain involved elevators, you might have an
Elevator class in your solution domain. Data management classes are those that you create to manage objects or data. Neither user interface classes nor data management classes have corresponding objects in the problem domain.
Phase 3: Implementation
Implementation is coding. Writing for loops, if statements, catch clauses, variables, and comments; compiling; unit testing; bug fixing -- that's implementation: the stark act of programming.
Phase 4: Integration and test
During the integration and test phase, the members of the project team, each tasked with building a specific part of the whole, meet and try to get all the pieces of the software system to work together. During this phase the team members find out how well the interfaces between the individual system components were defined and communicated during the system partitioning phase. The coding that takes place during this phase should primarily be bug fixing.
Documentation of software designs
There are many approaches to software design. Formal methodologies attempt to guide you through the process of transforming a problem domain into a solution domain. In designing Java programs, you may choose to use a formal methodology, to combine several formal methodologies, or to forgo formal methodology and design by the seat of your pants. But no matter how you attack the design phase of your software project, you should in some way document your design.
Documentation helps you keep track of your own design and helps you communicate it to others. Communicating your design to other developers before you implement can give you valuable feedback on your design early on, while it is still relatively easy to make changes. It is usually easier to correct design flaws during the design phase than it is later, during the implementation or the integration and test phases. (Note that this process of getting feedback on your design, and making the appropriate adjustments, is yet another example of iteration in software development.)
Many design methodologies include graphical conventions for describing a design. CASE (Computer Aided Software Engineering) tools often allow you to document your design as you create it. With Java, however, you have one other option. Java offers a simple way to express a software design not requiring anything that isn't a standard part of any Java development environment.
Documenting a design in code
A simple approach to design documentation is to express your design in Java code that has the structure and organization, but not the functionality, of your end product. You define and name classes and interfaces, and describe them in comments. You define the name and type of each field, and describe them in comments also. You define the name, return type, number of parameters, and parameter types of each method, but you don't define the method body. Instead of a body, you write a comment that describes what the body of the method, once implemented, will do. The comment can be pseudo-code, regular text description, or both. If you take this approach, your design will remain relatively easy to change throughout the design process.
If you do this, then at the end of the design phase you will already have a lot of Java code. For each program for which you did a user interface prototype, you'll have a framework of Java code for that program -- one that has look but no functionality. (These are the user interface classes.) You will also have a framework of Java code for each class that you designed with fields, comments, and methods with empty bodies. (These are the problem domain and data management classes.)
Presenting a design with
The Java programming environment includes a tool,
javadoc, that can help you document and communicate your design. In Java, there are three kinds of comments:
// indicates that the rest of the line is a comment.
*/ indicate that all characters between the initial
/* and the terminating
*/ should be ignored by the compiler.
*/ also comment out anything between them; however, the comments between
*/ are picked up by the
javadoc tool and placed into an HTML file it generates to document the code. Because of this, comments starting with
/** are called documentation comments, or simply doc comments.
javadoc parses .java files and generates several HTML files that describe classes, fields, methods, and that show doc comments.
Going through the entire system without implementing methods allows you to document your design in a way that helps you during implementation. At the start of implementation, you already have source code, and that source code already contains good comments. It is a good practice to make sure your classes compile as you design them even though they don't have method bodies.
Fluidity during design
An important goal to strive for during a software design phase is fluidity. What typically occurs is that you design one area, then as you go to design another area, you realize you need to go back and make adjustments in the first area. As a result, you will want to keep things fluid, or easily changeable. If you implement one section before continuing to the next, it is harder to go back and make changes to the areas already implemented. The more code you write, the greater your investment in a particular design, and the more financially and technically (and emotionally) difficult it is to go back and change it.
If there is a simple method and you implement it during the design phase, no alarms will sound. No one will expel you from the building (at least not for that reason). The main point of not implementing the methods during the design phase is to keep the design fluid until you've thought through the whole thing. To keep it easy to go back and make changes until you've gone through the entire solution domain.
Design monkeys on your back
In the real world, as you work to design software, you have several concerns to keep in mind -- several "monkeys on your back." Each monkey competes with the others for your attention, trying to convince you to take its particular concern to heart as you work. One large, heavy monkey hangs on your back with its arms around your neck and repeatedly yells, "You must meet the schedule!" Another monkey, this one perched on top of your head (as there is no more room on your back), beats its chest and cries, "You must accurately implement the specification!" Still another monkey jumps up and down on top of your monitor yelling, "Robustness, robustness, robustness!" Another keeps trying to scramble up your leg crying, "Don't forget about execution speed!" And every now and then, a small monkey peeks timidly at you from beneath the keyboard. When this happens, the other monkeys become silent. The little monkey slowly emerges from under the keyboard, stands up, looks you in the eye, and says, "You must make the code easy to read and easy to change." With this, all the other monkeys scream and jump onto the little monkey, forcing it back under the keyboard. With the little monkey out of sight, the other monkeys return to their positions and resume their activities.
As you sit there in your cubicle and work on your software, to which monkey should you listen? Alas, in most cases you must listen to all of them. To do a "good job," you will need to find a way to keep all these monkeys happy -- to strike a proper balance between these often conflicting concerns.
The first monkey: Meeting the schedule
In my experience, the most important way to keep management happy during a software development project is to meet the schedule. The schedule is critical, of course, because commercial software development is done for commercial reasons. It's a business.
Although there is often pressure to dive into implementation as soon as possible in an effort to finish a project faster, developing a well-thought-out specification and a thorough design from the start can help prevent unforeseen schedule catastrophes during the implementation phase. Having real specification and design phases before implementation gives you milestones that can help you manage your project schedule. In addition, the people for whom you are developing the end product (the people for whom schedule is the most important concern) will feel more comfortable if you give them intermediate milestones along the path to project completion. Early milestones can be the completion and sign-off of the specification, the completion and sign-off of the user interface prototype, and the completion and peer review of the design.
Once you have a design that includes all the classes populated with fields and methods without bodies, you have a good inventory of the work that remains. By making estimates for the time it will take to implement each method body, you should be able to come up with a reasonably accurate schedule for the implementation phase.
This is how a solid specification and design phase before implementation can help prevent major schedule slips during implementation: By the time you implement, you know what you are aiming for, and you know what you have to do to get there.
The second monkey: Correctly implementing the specification
To appease the second monkey, the most important thing is to do the work up front to figure out and communicate a clear, sufficiently detailed vision of the end target. If you don't know where you are going, you'll likely end up somewhere you never intended. To produce a product that matches and satisfies the customer's expectations, all those involved in developing the software must have a clear understanding of what they are supposed to produce.
The third monkey: Robustness
If you meet your milestones during the specification, design, and implementation phases but then require two more years to get the system to work reliably, you have a problem. Bugs are inevitable, but there is a threshold beyond which a product becomes unusable or at least less marketable. This threshold varies depending on the application. Software that helps commercial jetliners navigate likely has a higher robustness threshold than the browser software you are using to read this article. In all cases, however, there is some level of robustness that you must deliver.
In my experience, robustness has arisen out of good, solid designs and good coding practices. With the advent of Java, with its garbage collection and limitations on pointers, robustness became much easier to deliver. But even without memory problems, bad design and bad coding can still yield programs that lack robustness.
One coding technique I've found especially useful in achieving robustness is unit testing methods just after I implement them. After implementing a method, I walk through it with a debugger to make sure it is doing what I intended it to do.
The fourth monkey: Performance
Performance -- execution speed, resource usage, and so on -- is something you should usually keep in mind as you design and implement software. But often, programmers try to solve performance problems their programs don't actually have. The right approach typically is to keep performance in the back of your mind as you develop. In the front of your mind, keep "good object-oriented, thread safe" design. During integration and testing, if you discover you do indeed have a performance problem, that is the time to analyze and address it.
The fifth monkey: Flexibility
There is more than one path to correct implementation of a specification. Different teams of programmers can produce drastically different designs and implementations, all of which fulfill the requirements set forth by the specification. Some versions, however, may take longer to write and debug. As they execute, some versions may be slower than others. And over time, as the program evolves from release to release, some versions may turn out to be less flexible than others.
Flexibility, the ease with which a program can be changed, is important because source code usually evolves over time. As bugs are fixed and enhancements made for new releases, programmers must return to already-existing source code, understand it, and make changes to it.
When you write a program, you are communicating. Most obviously, you are communicating with a machine. You are telling a computer what you want it to do. But there is another, less obvious, form of communication that you perform when you write a program: communication with other programmers. Through your source code, you communicate your design to any programmers who, in the future, ever need to fix a bug, make an enhancement, or simply reuse your code. You are telling programmers how your code is supposed to work, how it should be used, and how best to go about changing it.
One of the most important aspects of program flexibility is source code readability. Readable source code is important because it helps future programmers (possibly including your future self) understand what you were trying to do. In many cases in the real world, a program's source code is the documentation. When others work on your source code, either to add new features or to fix bugs, they read your source code to understand what it does.
What constitutes a "good object-oriented, thread-safe" design? The answer depends on whether you are talking about object orientation or thread safety.
Thread safety primarily is about robustness (monkey number three). If you forget to synchronize methods that really need to be synchronized, you can end up with intermittent data corruption. Creating thread-safe designs involve understanding multithreading in general, understanding the multithreaded needs of your particular programs, and programming accordingly.
Good object-oriented design also plays a part in robustness. As I said earlier in this article, my experience has shown me that robustness arises out of good design and coding practices.
In addition, a good object-oriented design can play a role in keeping a project on schedule (monkey number one). As part of a solid overall software development process, a thorough design phase can help a team avoid unforeseen schedule slips. A thorough design gives the team a clear and detailed view of the work that will be required by the implementation phase.
The primary benefit, however, of a good object-oriented design is flexibility, the concern of monkey number five. A good object-oriented design can give you code that is easy to understand and easy to change. And the greater the flexibility of a body of code, the quicker (and cheaper) will be enhancements and bug fixes to that code.
The guidelines put forth in this column primarily will help you achieve program flexibility. When I talk about object design, building class hierarchies, interfaces, polymorphism, choosing composition vs. inheritance, and so on, my main focus will be to give insights that will help you make your programs easier to understand and easier to change.
With next month's Design Techniques I'll begin a mini-series of articles that focus on designing classes and objects. Next month's article, the first of this mini-series, will give design guidelines in the area of class initialization.
A request for reader participation
Software design is subjective. Your idea of a well-designed program may be your colleague's maintenance nightmare. In light of this fact, I hope to make this column as interactive as possible.
I encourage your comments, criticisms, suggestions, flames -- all kinds of feedback -- about the material presented in this column. If you disagree with something, or have something to add, please let me know by e-mailing me at email@example.com.
This article was first published under the name Introduction to Design Techniques in JavaWorld, a division of Web Publishing, Inc., January 1998.
Have an opinion? Be the first to post a comment about this article.
Bill Venners has been writing software professionally for 12 years. Based in Silicon Valley, he provides software consulting and training services under the name Artima Software Company. Over the years he has developed software for the consumer electronics, education, semiconductor, and life insurance industries. He has programmed in many languages on many platforms: assembly language on various microprocessors, C on Unix, C++ on Windows, Java on the Web. He is author of the book: Inside the Java Virtual Machine, published by McGraw-Hill.
Artima provides consulting and training services to help you make the most of Scala, reactive and functional programming, enterprise systems, big data, and testing.