This appendix contains suggestions to help guide you in performing low-level program design and in writing code.Naturally, these are guidelines and not rules. The idea is to use them as inspirations and to remember that there are occasional situations where they should be bent or broken.
Design with Java
- Elegance always pays off ( Good OOD succeed ) .
- In the short term it might seem like it takes much longer to come up with a truly graceful solution to a problem, but when it works the first time and easily adapts to new situations instead of requiring hours, days, or months of struggle, you’ll see the rewards (even if no one can measure them). Not only does it give you a program that’s easier to build and debug, but it’s also easier to understand and maintain, and that’s where the financial value lies. This point can take some experience to understand, because it can appear that you’re not being productive while you’re making a piece of code elegant. Resist the urge to hurry; it will only slow you down.
- First make it work, then make it fast ( refactoring).
- This is true even if you are certain that a piece of code is really important and that it will be a principal bottleneck in your system. Don’t do it. Get the system going first with as simple a design as possible. Then if it isn’t going fast enough, profile it. You’ll almost always discover that “your” bottleneck isn’t the problem. Save your time for the really important stuff.
- Remember the “divide and conquer” principle.
- If the problem you’re looking at is too confusing, try to imagine what the basic operation of the program would be, given the existence of a magic “piece” that handles the hard parts. That “piece” is an object—write the code that uses the object, then look at the object and encapsulate its hard parts into other objects, etc.
- Separate the class creator from the class user (client programmer).
- Extract interface or class.The class user is the “customer” and doesn’t need or want to know what’s going on behind the scenes of the class. The class creator must be the expert in class design and write the class so that it can be used by the most novice programmer possible, yet still work robustly in the application. Think of the class as a service provider for other classes. Library use will be easy only if it’s transparent.
- When you create a class, attempt to make your names so clear that comments are unnecessary.
- Your goal should be to make the client programmer’s interface conceptually simple. To this end, use method overloading when appropriate to create an intuitive, easy-to-use interface.
- Your analysis and design must produce, at minimum. the classes in your system, their public interfaces, and their relationships to other classes, especially base classes. If your design methodology produces more than that, ask yourself if all the pieces produced by that methodology have value over the lifetime of the program. If they do not, maintaining them will cost you. Members of development teams tend not to maintain anything that does not contribute to their productivity; this is a fact of life that many design methods don’t account for.
- Automate everything. Write the test code first (before you write the class), and keep it with the class.
- Automate the running of your tests through a build tool—you’ll probably want to use Ant, the defacto standard Java build tool. This way, any changes can be automatically verified by running the test code, and you’ll immediately discover errors. Because you know that you have the safety net of your test framework, you will be bolder about making sweeping changes when you discover the need. Remember that the greatest improvements in languages come from the built-in testing provided by type checking, exception handling, etc., but those features take you only so far. You must go the rest of the way in creating a robust system by filling in the tests that verify features that are specific to your class or program.
- Write the test code first (before you write the class) in order to verify that your class design is complete.
- If you can’t write test code, you don’t know what your class looks like. In addition, the act of writing the test code will often flush out additional features or constraints that you need in the class—these features or constraints don’t always appear during analysis and design. Tests also provide example code showing how your class can be used.
- All software design problems can be simplified by introducing an extra level of conceptual indirection.
- This fundamental rule of software engineering[122] is the basis of abstraction, the primary feature of object-oriented programming. In OOP, we could also say this as: “If your code is too complicated, make more objects.”
- An indirection should have a meaning (in concert with guideline 9).
- This meaning can be something as simple as “putting commonly used code in a single method.” If you add levels of indirection (abstraction, encapsulation, etc.) that don’t have meaning, it can be as bad as not having adequate indirection.
- Make classes as atomic as possible.
- Give each class a single, clear purpose—a cohesive service that it provides to other classes. If your classes or your system design grows too complicated, break complex classes into simpler ones. The most obvious indicator of this is sheer size; if a class is big, chances are it’s doing too much and should be broken up.Clues to suggest redesign of a class are:1) A complicated switch statement: consider using polymorphism. 2) A large number of methods that cover broadly different types of operations: consider using several classes.3) A large number of member variables that concern broadly different characteristics: consider using several classes. 4) Other suggestions can be found in Refactoring: Improving the Design of Existing Code by Martin Fowler (Addison-Wesley 1999).
- Watch for long argument lists.
- Method calls then become difficult to write, read, and maintain. Instead, try to move the method to a class where it is (more) appropriate, and/or pass objects in as arguments.
- Don’t repeat yourself.
- If a piece of code is recurring in many methods in derived classes, put that code into a single method in the base class and call it from the derived-class methods. Not only do you save code space, but you provide for easy propagation of changes. Sometimes the discovery of this common code will add valuable functionality to your interface. A simpler version of this guideline also occurs without inheritance: If a class has methods that repeat code, factor that code into a common method and call it from the other methods.
- Watch for switch statements or chained if-else clauses.
- This is typically an indicator of type-check coding, which means that you are choosing what code to execute based on some kind of type information (the exact type may not be obvious at first). You can usually replace this kind of code with inheritance and polymorphism; a polymorphic method call will perform the type checking for you and allow for more reliable and easier extensibility.
- From a design standpoint, look for and separate things that change from things that stay the same.
- That is, search for the elements in a system that you might want to change without forcing a redesign, then encapsulate those elements in classes. You can learn much more about this concept in Thinking in Patterns (with Java) at www.BruceEckel.com.
- Don’t extend fundamental functionality by subclassing.
- If an interface element is essential to a class it should be in the base class, not added during derivation. If you’re adding methods by inheriting, perhaps you should rethink the design.
- Less is more.
- Start with a minimal interface to a class, as small and simple as you need to solve the problem at hand, but don’t try to anticipate all the ways that your class might be used. As the class is used, you’ll discover ways you must expand the interface. However, once a class is in use, you cannot shrink the interface without breaking client code. If you need to add more methods, that’s fine; it won’t break code. But even if new methods replace the functionality of old ones, leave the existing interface alone (you can combine the functionality in the underlying implementation if you want). If you need to expand the interface of an existing method by adding more arguments, create an overloaded method with the new arguments; this way, you won’t disturb any calls to the existing method.
- Read your classes aloud to make sure they’re logical.
- Refer to the relationship between a base class and derived class as “is-a” and member objects as “has-a.”
- When deciding between inheritance and composition, ask if you need to upcast to the base type. If not, prefer composition (member objects) to inheritance. This can eliminate the perceived need for multiple base types. If you inherit, users will think they are supposed to upcast.
- Use fields for variation in value, and method overriding for variation in behavior. That is, if you find a class that uses state variables along with methods that switch behavior based on those variables, you should probably redesign it to express the differences in behavior within subclasses and overridden methods.
- Watch for overloading. A method should not conditionally execute code based on the value of an argument. In this case, you should create two or more overloaded methods instead.
- Use exception hierarchies—preferably derived from specific appropriate classes in the standard Java exception hierarchy. The person catching the exceptions can then write handlers for the specific types of exceptions, followed by handlers for the base type. If you add new derived exceptions, existing client code will still catch the exception through the base type.
- Sometimes simple aggregation does the job.A “passenger comfort system” on an airline consists of disconnected elements: seat, air conditioning, video, etc., and yet you need to create many of these in a plane. Do you make private members and build a whole new interface? No—in this case, the components are also part of the public interface, so you should create public member objects. Those objects have their own private implementations, which are still safe. Be aware that simple aggregation is not a solution to be used often, but it does happen.
- Consider the perspective of the client programmer and the person maintaining the code. Design your class to be as obvious as possible to use. Anticipate the kind of changes that will be made, and design your class so that those changes will be easy.
- Watch out for “giant object syndrome.” This is often an affliction of procedural programmers who are new to OOP and who end up writing a procedural program and sticking it inside one or two giant objects. With the exception of application frameworks, objects represent concepts in your application, not the application itself.
- If you must do something ugly, at least localize the ugliness inside a class.
- If you must do something nonportable, make an abstraction for that service and localize it within a class. This extra level of indirection prevents the nonportability from being distributed throughout your program. (This idiom is embodied in the Bridge Pattern, among others).
- Objects should not simply hold some data. They should also have well-defined behaviors. (Occasionally, “data objects” are appropriate, but only when used expressly to package and transport a group of items when a generalized container is innappropriate.)
- Choose composition first when creating new classes from existing classes.You should only use inheritance if it is required by your design. If you use inheritance where composition will work, your designs will become needlessly complicated.
- Use inheritance and method overriding to express differences in behavior, and fields to express variations in state. An extreme example of what not to do is to inherit different classes to represent colors instead of using a “color” field.
- Watch out for variance. Two semantically different objects may have identical actions, or responsibilities, and there is a natural temptation to try to make one a subclass of the other just to benefit from inheritance. This is called variance, but there’s no real justification to force a superclass/subclass relationship where it doesn’t exist. A better solution is to create a general base class that produces an interface for both as derived classes; it requires a bit more space, but you still benefit from inheritance and will probably make an important discovery about the design.
- Watch out for limitation during inheritance. The clearest designs add new capabilities to inherited ones. A suspicious design removes old capabilities during inheritance without adding new ones. But rules are made to be broken, and if you are working from an old class library, it may be more efficient to restrict an existing class in its subclass than it would be to restructure the hierarchy so your new class fits in where it should, above the old class.
- Use design patterns to eliminate “naked functionality.” That is, if only one object of your class should be created, don’t bolt ahead to the application and write a comment “Make only one of these.” Wrap it in a singleton. If you have a lot of messy code in your main program that creates your objects, look for a creational pattern like a factory method in which you can encapsulate that creation. Eliminating “naked functionality” will not only make your code much easier to understand and maintain, but it will also make it more bulletproof against the well-intentioned maintainers that come after you.
- Watch out for “analysis paralysis.”Remember that you must usually move forward in a project before you know everything, and that often the best and fastest way to learn about some of your unknown factors is to go to the next step rather than trying to figure it out in your head. You can’t know the solution until you have the solution. Java has built-in firewalls; let them work for you. Your mistakes in a class or set of classes won’t destroy the integrity of the whole system.
- When you think you’ve got a good analysis, design, or implementation, do a walkthrough. Bring someone in from outside your group—this doesn’t have to be a consultant, but can be someone from another group within your company. Reviewing your work with a fresh pair of eyes can reveal problems at a stage when it’s much easier to fix them, and more than pays for the time and money “lost” to the walkthrough process.