Chapter 5. Class

The idea of classes goes back long before Plato. The platonic solids were classes, instances of which could be seen in the world. The platonic sphere was absolutely perfect but insubstantial. The spheres around us we could touch, but they were all imperfect in some way.

Object-oriented programming picked up on this idea by way of later western philosophers, dividing programs into classes, which are general descriptions of a whole set of similar things, and objects, which are the things themselves.

Classes are important for communication because they describe, potentially, many specific things. Class-level patterns have the largest span of any of the implementation patterns. Design patterns, by contrast, generally talk about the relationships between classes.

The following patterns appear in this chapter:

ClassUse a class to say, “This data goes together and this logic goes with it.”

Simple Superclass NameName the roots of class hierarchies with simple names drawn from the same metaphor.

Qualified Subclass NameName subclasses to communicate the similarities and differences with a superclass.

Abstract InterfaceSeparate the interface from the implementation.

InterfaceSpecify an abstract interface which doesn’t change often with a Java interface.

Versioned InterfaceExtend interfaces safely by introducing a new sub-interface.

Abstract ClassSpecify an abstract interface which will likely change with an abstract class.

Value ObjectWrite an object that acts like a mathematical value.

SpecializationClearly express the similarities and differences of related computations.

SubclassExpress one-dimensional variation with a subclass.

ImplementorOverride a method to express a variant of a computation.

Inner ClassBundle locally useful code in a private class.

Instance-specific BehaviorVary logic by instance.

ConditionalVary logic by explicit conditionals.

DelegationVary logic by delegating to one of several types of objects.

Pluggable SelectorVary logic by reflectively executing a method.

Anonymous Inner ClassVary logic by overriding one or two methods right in the method that is creating a new object.

Library ClassRepresent a bundle of functionality that doesn’t fit into any object as a set of static methods.

Class

Data changes more frequently than logic. That is the observation that makes classes work. Each class is a declaration, “This logic goes together and changes more slowly than the data values on which it operates. These data values also go together, changing at similar rates and being operated on by related logic.” This strict separation between data-that-changes and logic-that-does-not-change isn’t absolute. Sometimes the logic will be a little different based on data values; sometimes the logic varies considerably. Sometimes data doesn’t change in the course of a computation. Learning how to bundle logic in classes and express variations in that logic is part of programming effectively with objects.

Organizing classes into hierarchies is a form of compression, taking the superclass and including it textually in all the subclasses. As with all compression techniques, it leaves the code more difficult to read. You have to understand the context of the superclass to be able to understand the subclass.

Using inheritance judiciously is another aspect of programming effectively with objects. Creating a subclass says, “I’m like that superclass, only different.” (Isn’t it strange that we speak of overriding a method in a subclass? How much better programmers would we be if we had thoughtfully selected our metaphors?)

Classes are relatively expensive design elements in programs built from objects. A class should do something significant. Reducing the number of classes in a system is an improvement, as long as the remaining classes do not become bloated.

The patterns that follow explain how to communicate by declaring classes.

Simple Superclass Name

Finding just the right name is one of the most satisfying moments in programming. You’re struggling with an idea. Oftentimes, the code has gotten complicated and it doesn’t seem like it needs to be. Then, often in conversation, someone says, “Oh, I see! This is really a Scheduler.” Everyone sits back and lets out a breath. The right name results in a cascade of further simplifications and improvements.

Some of the most important names to choose well are those of classes. Classes are the central anchoring concept in the design. Once classes have been named, the names of operations follow. It is rare for the reverse to be true, except if the class was poorly named in the first place.

In naming classes there is a tension between brevity and expressiveness. You’ll be using class names in conversation: “Did you remember to rotate the Figure before translating it?” The names should be short and punchy. However, to make the names precise sometimes seems to require several words.

A way out of this dilemma is picking a strong metaphor for the computation. With a metaphor in mind, even single words bring with them a rich web of associations, connections, and implications. For example, in the HotDraw drawing framework, my first name for an object in a drawing was DrawingObject. Ward Cunningham came along with the typography metaphor: a drawing is like a printed, laid-out page. Graphical items on a page are figures, so the class became Figure. In the context of the metaphor, Figure is simultaneously shorter, richer, and more precise than DrawingObject.

Sometimes, good names take time to find. You may have code “done” and working for weeks, months, or (in one notable case for me) years when you discover a better name for a class. Sometimes you need to push a little harder to find a name: pull out a thesaurus, write down a list of the least-suitable names you can think of, take a walk. Sometimes you need to forge ahead with new functionality, trusting time, frustration, and your subconscious to supply a better name.

Conversation is a tool that consistently helps me find better names. Explaining the purpose of an object to another person leads me to look for rich and evocative images to describe it. These images can lead in turn to new names.

Look for one-word names for important classes.

Qualified Subclass Name

The names of subclasses have two jobs. They need to communicate what class they are like and how they are different. Again, the balance to be struck is between length and expressiveness. Unlike the names at the roots of hierarchies, subclass names aren’t used nearly as often in conversation, so they can be expressive at the cost of being concise. Prepend one or more modifiers to the superclass name to form a subclass name.

One exception to this rule is when subclassing is used strictly as an implementation sharing mechanism and the subclass is an important concept in its own right. Give subclasses that serve as the roots of hierarchies their own simple names. For example, HotDraw has a class Handle which presents figure-editing operations when a figure is selected. It is called, simply, Handle in spite of extending Figure. There is a whole family of handles and they most appropriately have names like StretchyHandle and TransparencyHandle. Because Handle is the root of its own hierarchy, it deserves a simple superclass name more than a qualified subclass name.

Another wrinkle in subclass naming is multiple-level hierarchies. Multi-level hierarchies are usually delegation waiting to happen, but while they exist they need good names. Rather than blindly prepend the modifiers to the immediate superclass, think about the name from the reader’s perspective. What class does he need to know this class is like? Use that superclass as the basis for the subclass name.

Communication with people is the purpose of class names. As far as the computer is concerned, classes could simply be numbered. Class names that are too long are hard to read and format. Class names that are too short tax the reader’s short-term memory. Clusters of classes whose names don’t relate to each other will be difficult to comprehend and recall. Use class names to tell the story of your code.

Abstract Interface

The old adage in software development is to code to interfaces, not implementations. This is another way of suggesting that a design decision should not be visible in more places than necessary. If most of my code only knows that I am dealing with a collection I am free to change the concrete class later. However, at some point you actually have to commit to a concrete class so the computer can perform a calculation.

By “interface” here I mean “a set of operations without implementations”. This can be represented in Java either as an interface or as a superclass. Patterns below will suggest when each is appropriate.

Every layer of interface has costs. It is one more thing to learn, understand, document, debug, organize, browse, and name. Maximizing the number of interfaces doesn’t minimize the cost of software. Pay for interfaces only where you will need the flexibility they create. Since you can’t often know in advance where you will need the flexibility of an interface, to minimize cost, combine speculating about where to introduce interfaces with adding them when flexibility is required.

Much as we complain about the inflexibility of software, there are a very large number of ways we don’t need any given system to flex. From fundamental changes like the number of bits in an integer to large-scale changes like new business models, most software doesn’t need to be flexible in most of the ways it could be.

Another economic factor in introducing interfaces is the unpredictability of software. Our industry seems addicted to the idea that if only we designed software right we would not have to change our systems. I recently read a list of reasons software changes. On the list were programmers doing a bad job of eliciting requirements, sponsors changing their mind, and on and on. The one factor that was missing from the list was legitimate change. The list assumed change was always a mistake. Why won’t one weather forecast do for all time? Because the weather changes in unpredictable ways. Why can’t we list once and for all the ways a system needs to be flexible? Because the requirements change in unpredictable ways and technology changes in unpredictable ways. This doesn’t relieve us from the responsibility to do our best to develop the system customers need right now, but it suggests that there are limits to the value of “future-proofing” software through speculation.

Putting all of these factors together—the need for flexibility, the cost of flexibility, the unpredictability of where flexibility is needed—leads me to the belief that the time to introduce flexibility is when it is definitely needed. Introducing flexibility costs because of the changes you need to make to existing software. If you can’t personally change all the software that needs to change, the costs rise further, a topic taken up in detail in the chapter on evolving frameworks.

Java’s two mechanisms for abstract interfaces, superclasses and interfaces, have different cost profiles for such changes.

Interface

One way of saying “Here’s what I want to accomplish and beyond that are details that shouldn’t concern me” is to declare a Java interface. Interfaces are one of the important innovations first put in a mass-market language in Java. Interfaces are a nice balance. They have some of the flexibility of multiple inheritance without the complexity and ambiguity. One class can declare itself as participating in multiple interfaces. Interfaces reveal only operations, not fields, so they can effectively protect users of an interface from changes in implementation.

If interfaces enable changes to their implementations, they discourage changes to the interface itself. Any addition or change to an interface requires modifying all implementors. If you can’t modify the implementations, widespread use of interfaces provides significant drag on further design evolution.

One quirk of interfaces that limits their value as a way to communicate is that all operations are required to be public. I have often wished for package-visible operations in interfaces. Making design elements a little too public isn’t a problem when they are for private use, but when publishing interfaces to a large audience it would be better to be able to be precise rather than build up inertia against future change.

Two styles of naming interfaces depend on how you are thinking of the interfaces. Interfaces as classes without implementations should be named as if they were classes (Simple Superclass Name, Qualified Subclass Name). One problem with this style of naming is that the good names are used up before you get to naming classes. An interface called File needs an implementation class called something like ActualFile, ConcreteFile, or (yuck!) FileImpl (both a suffix and an abbreviation). In general, communicating whether one is dealing with a concrete or abstract object is important, whether the abstract object is implemented as an interface or a superclass is less important. Deferring the distinction between interfaces and superclasses is well supported by this style of naming, leaving you free to change your mind later if that becomes necessary.

Sometimes, naming concrete classes simply is more important to communication than hiding the use of interfaces. In this case, prefix interface names with “I”. If the interface is called IFile, the class can be simply called File.

Abstract Class

The other way to express the distinction between abstract interface and concrete implementation in Java is to use a superclass. The superclass is abstract in the sense that it can be replaced at runtime with any subclass, whether it is abstract in the Java sense or not.

The trade-offs for when to use an abstract class versus an interface boil down to two issues: changes to the interface and the need for a single class to support multiple interfaces simultaneously. Abstract interfaces need to support two kinds of change: change in the implementation and change of the interface itself. Java interfaces do a poor job of supporting the latter. Every change to an interface requires changes to all implementations. With a widely implemented interface, this can easily lead to paralysis of existing designs, with further evolution only available through versioned interfaces.

Abstract classes do not suffer this limitation. As long as a default implementation can be specified, new operations can be added to an abstract class without disrupting existing implementors.

One limitation of abstract classes is that implementors can only declare their allegiance to one superclass. If other views of the same class are necessary, they must be implementated by Java interfaces.

Using the keyword abstract with a class tells readers that they will have to do some implementation work if they want to use the class. If there is any chance to make the root of a class hierarchy useful and instantiable on its own, do so. Once on the path of abstraction, it is easy to go too far and create abstractions that never pay off. Striving to make root classes instantiable encourages you to eliminate abstractions that are unlikely to pull their weight.

Interfaces and class hierarchies are not mutually exclusive. You can provide an interface which says, “Here’s how to access this kind of functionality,” and a superclass which says, “Here’s one way to implement this functionality.” In this case, variables should be declared with the interface as their type so future maintainers are free to substitute new implementations as necessary.

Versioned Interface

What do you do when you need to change an interface but you can’t? Typically this happens when you want to add operations. Since adding an operation will break all existing implementors, you can’t do that. However, you can declare a new interface that extends the original interface and add the operation there. Users who want the new functionality use the extended interface while existing users remain oblivious to the existence of the new interface. Anywhere you want to access the new operation you must explicitly check the type of your object and downcast to the new type.

For example, consider a simple command:

interface Command {
  void run();
}

Once this interface has been published and extended a thousand times, changing it becomes expensive. However, to support undoing of commands you need a new operation. The versioned interface style of solution is this:

interface ReversibleCommand extends Command {
  void undo();
}

Existing instances of Command work as before. Instances of ReversibleCommand work anywhere a Command works. To use the new operation, downcast:

image

Using instanceof generally reduces flexibility by tying code to certain classes. In this case, however, it may be justified because it enables the evolution of interfaces. If you begin to have several alternative interfaces, however, clients need to do a lot of work to deal with all the variations. These are signs that it is time to rethink the design.

Alternative interfaces are an ugly solution to an ugly problem. Interfaces don’t accommodate change to their structure as easily as they accommodate change to their implementations. Interfaces are likely to change, just like all design decisions. We all learn about design through implementation and maintenance. Alternative interfaces create a new programming language that is like Java but with new rules. Writing new languages is a different game with tougher rules than writing applications. However, if you are stuck in the situation of needing to extend an interface, it’s nice to know how.

Value Object

While objects-with-changing-state is one valuable way to think about computation, it is not the only way to think. Mathematics has developed over millennia as a way to think about situations that can be reduced to an abstract world of absolute truth and certainty, where statements can be made about eternal verities.

Our current programming languages are a mix of the two styles. The so-called primitive types in Java belong (mostly) to the world of mathematics. When I add 1 to a number in Java, I am making a statement of mathematics (all except that part where someone decided my computer only had to count up to 232 or 264 and then we should let it start over). I don’t change the value of a variable when I add 1: I create a new value. There is no way to change 0, as you can with most objects.

This functional style of computing never changes any state, it only creates new values. When you have a (perhaps momentarily) static situation about which you’d like to make statements or about which you’d like to ask questions, then the functional style is appropriate. When the situation is changing over time, then state is appropriate. Some situations could be thought of in either way. How can you tell which way is most helpful?

For example, you can represent drawing a picture as changes to the state of some graphics medium like a bitmap. Alternatively, you can describe the same picture with a static description (Figure 5.1).

Figure 5.1. Graphics represented as procedures and objects

image

Which of these representations is most useful depends to some extent on personal preference, but it also depends on the complexity of the pictures to be drawn and how often they change.

Procedural interfaces are more common than functional interfaces. One problem with procedural interfaces is that the sequence of procedure calls becomes an important (but often implicit) part of the meaning of the interface. Changing such a program is touchy and difficult because seemingly small changes have unintended consequences when the implicit meaning of the sequence is changed.

The beauty of mathematical representations is that sequence seldom matters. You are creating a world in which you can make absolute, timeless statements. Create micro-worlds of mathematics wherever possible. Manage them from an object with changing state.

For example, implement an accounting system by making the basic transactions unchanging mathematical values.

Figure 5.2. State-changing objects referring to immutable objects at the edges

image

image

There is no way to change any of the values of a Transaction once it has been created. What’s more, the constructor makes the statement that all transactions are posted to two accounts. When I read this code, I know that I don’t have to worry about transactions floating around loose or about transactions having their values changed after they have been posted.

To implement value-style objects (that is, objects that act like integers rather than like holders of changing state), first draw the boundary between the world of state and the world of value. In the example above, a Transaction is a value, an Account holds changing state. Set all state in a value-style object in the constructor, with no other field assignments elsewhere in the object. Operations on value-style objects always return new objects. These objects must be stored by the requestor of the operation.

bounds.translateBy(10, 20); // mutable Rectangle
bounds= bounds.translateBy(10, 20); // value-style Rectangle

The biggest argument against value-style objects has always been performance. The need to create all those intermediate objects can put a strain on the memory management system. In the overall cost of programming, this argument doesn’t often hold up because most parts of the program are not performance bottlenecks. Other reasons not to use value-style objects are unfamiliarity with the style and difficulty drawing boundaries between parts of the system where state changes and parts of the system where objects don’t change. Objects that are mostly value-style are the worst of both worlds, since the interfaces tend to be more complicated but you can’t safely make assumptions about state not changing.

Having gotten this far, I sense that there is much more to be written about programming with the three main styles—objects, functions, and procedures—and how to blend them effectively. For purposes of this book, I’ll close by reiterating that sometimes your programs will best be expressed as a combination of state-changing objects and objects representing mathematical values.

Specialization

Communicating the interplay between the similarities and differences of computations makes your programs easier to read, use, and modify. In practice, each program is not unique. Many express similar ideas, and, often, many parts of the same program express similar ideas. Expressing similarities and differences clearly allows readers to understand the existing code, discover if their current intentions are covered by one of the existing variations, or, if not, how to best either specialize the existing code to their needs or write entirely new code.

The simplest variations are those where state differs. The string “abc” is different from “def”. The algorithms that operate on the two strings are identical. For example, the length of all strings is calculated in the same way.

The most complex variations are total differences in logic. A symbolic integration routine has no logic in common with a mathematical typesetting routine, even though the two might share exactly the same input.

In between these two extremes—identical logic with different data and different logic with identical data—lies the huge common ground of programming. Data can be mostly the same but a little different. Logic can be mostly the same but a little different. (I would guess that the symbolic integration routine and the mathematical typesetting routine share little code.) Even the line between logic and data is blurry. A flag is boolean data but it affects the flow of control. A helper object can be stored in a field but used to affect a computation.

The patterns that follow are a variety of techniques to communicate similarity and difference, primarily in logic. Variations in data don’t seem as complicated or subtle. Effective expressions of similarity and difference in logic open up new opportunities for further expansion of the code.

Subclass

Declaring a subclass is a way of saying, “These objects are like those except...” If you have the right superclass, creating a subclass can be a powerful way to program. With the right method to override, you can introduce a variant of an existing calculation with a few lines of code.

When objects first became popular, subclassing seemed like a magic pill. First, subclasses were used for classification—a Train was a subclass of Vehicle regardless of whether they shared any implementation. In time, some people saw that since what inheritance did was share implementation, it could most effectively be used to factor out common bits of implementation. Quickly, though, the limitations of subclassing became apparent. First, it’s a card you can only play once. If you discover that some set of variations isn’t well expressed as subclasses, you have some work to do to disentangle the code before you can restructure it. Second, you have to understand the superclass before you can understand the subclass. As the superclasses become more complicated this becomes more of a limitation. Third, changes to a superclass are risky, since subclasses can rely on subtle properties of the superclass’s implementation. Finally, all of these problems are compounded by deep inheritance hierarchies.

A particularly pernicious use of inheritance is creating parallel hierarchies, where for each subclass in this hierarchy you need a subclass in that hierarchy. This is a form of duplication, creating implicit coupling between the class hierarchies. To successfully introduce a new variation you need to change both hierarchies. While I often see parallel hierarchies that I can’t immediately figure out how to eliminate, the effort to do so improves the design.

One example of this was an insurance system (Figure 5.3). Something is definitely wrong with this picture, because an InsuranceContract cannot refer to a PensionProduct nor is it attractive to move the product field down to the subclasses. The solution, which we never reached but took a year to come close to, was to move the variation around so Contract worked the same whether it was used for insurance or a pension. This required creating a new object to represent the prospective cash flows (Figure 5.4).

Figure 5.3. Parallel hierarchies

image

Figure 5.4. Hierarchy with duplication eliminated

image

With all these warnings in mind, subclassing can be a powerful tool for expressing theme-and-variations computations. The right subclass can help many people to express exactly the computation they want with a method or two. One key to achieving useful subclasses is to thoroughly factor the logic in the superclass into methods that do one job. When writing a subclass, you should be able to override exactly one method. If the superclass methods are too big, you’ll have to copy code and edit it (Figure 5.5).

Figure 5.5. Code copied and modified in a subclass

image

Copied code introduces an ugly implicit coupling between the two classes. You can’t safely change the code in the superclass without examining and potentially changing all the places to which it has been copied.

My goal in design is to be able to switch between strategies at will, depending on the needs of the code as it now exists. Visualize the code expressed with conditionals, with subclasses, with delegation. Does it seem like there are advantages to a different strategy than the one you are currently using? Take a few steps in that direction and see if you improve the code.

A final limitation of subclassing is that it cannot be used to express changing logic. The variation you want must be known when you create an object and can’t be changed thereafter. You’ll need to use conditionals or delegation to express logic that changes.

Implementor

The polymorphic message is the fundamental way to express a choice in a program built from objects. For the message to do its work of choosing, there needs to be more than one kind of object to potentially receive the message.

Implementing the same protocol multiple times, whether expressed with a Java interface and an implements declaration or as a subclass expressed with extends, is a way of saying, “From the point of view of one part of the calculation, as long as something happens matching the intention of the code, the details of what happens are irrelevant.”

The beauty of polymorphic messages is that they open a system to variation. If a part of the program writes some bytes to another system, the introduction of an abstract Socket enables the implementation of the socket to vary without affecting the calling code. Compared to the procedural expression of the same intention, with its explicit and closed conditional logic, the object/message version is clearer, separating the expression of the intention (write some bytes) from the implementation (call a TCP/IP stack with certain parameters). At the same time, expressing the computation as objects and messages opens the system to future variation undreamt of by the original programmers. This fortuitous combination of clarity of expression and flexibility is why object languages have become the dominant programming paradigm.

This supreme resource is easy to squander by writing procedural programs in Java. The patterns here are intended to help you express logic that is both clear and extendable.

Inner Class

Sometimes you need to package part of a computation but you don’t want to incur the cost of a whole new class with its own file. Declaring small, private classes (inner classes) gives you a low-cost way of having many of the benefits of a class without all of its costs.

Sometimes an inner class extends only Object. Some inner classes extend another superclass, which is useful for expressing refinements of other classes that are only locally interesting.

One of the features of inner classes is that when their instances are created, they are secretly passed a copy of the object that is creating them. This is handy when you want to access the enclosing instance’s data without making the relationship between the two classes explicit:

image

However, in the inner class above, there is not really a no-arg constructor, even if you declare one. This is a problem when creating instances of inner classes by reflection.

image

To get an inner class that is completely detached from its enclosing instances, declare it static.

Instance-Specific Behavior

In theory, all instances of a class share the same logic. Relaxing this constraint enables new styles of expression. All of these styles, though, come at a cost. When the logic of an object is completely determined by its class, readers can read the code in the class to see what is going to happen. Once you have instances with different behavior, you have to look at live examples or analyze the data flow to understand how a particular object is going to behave.

Another step up in the cost of instance-specific behavior is when the logic changes as the computation progresses. For ease of code reading, try to set instance-specific behavior when an object is created and don’t change it afterward.

Conditional

If/then and switch statements are the simplest form of instance-specific behavior. Using conditionals, different objects will execute different logic based on their data. Conditionals as a form of expression have the advantage that the logic is still all in one class. Readers don’t have to go navigating around to find the possible paths for a computation. However, conditionals have the disadvantage that they can’t be modified except by modifying the code of the object in question.

Each path of execution through a program has some probability of being correct. Assuming that the probabilities of correctness for the paths are independent, the more paths through a program the less likely the program is to be correct. The probabilities aren’t entirely independent, but they are independent enough that programs with many paths are more likely to have defects than those with few paths. The proliferation of conditionals reduces reliability.

This problem is compounded when conditionals are duplicated. Consider a simple graphic editor. The figures will need a display() method:

image

Figures will also need a method to determine whether a point is contained within them:

image

Suppose now you want to add a new kind of figure. First, you must add a clause to every switch statement. Second, to make this change you have to modify the Figure class, putting all of the existing functionality at risk. Lastly, everyone who wants to add new figures must coordinate their changes of a single class.

These problems can all be eliminated by converting the conditional logic to messages, either with subclasses or delegation (which technique serves best depends on the code). Duplicate conditional logic or logic where the processing is very different based on which branch of a conditional is taken is generally better expressed as messages instead of explicit logic. Also, conditional logic that changes frequently is better expressed as messages to simplify changing one branch while minimizing the effects on other branches.

Figure 5.6. Conditional logic represented by subclasses and delegation

image

In short, the strengths of conditionals—that they are simple and local—become liabilities when they are used too widely.

Delegation

Another way to execute different logic in different instances is to delegate work to one of several possible kinds of objects. The common logic is held in the referring class, the variations in the delegates.

An example of using a delegate to capture variation is handling user input in a graphical editor. Sometimes a button press means “create a rectangle”, sometimes it means “move a figure”, and so on.

One way to express the variation between the tools is with conditional logic:

image

This has all the problems of conditionals discussed above: adding a new tool requires modifying the code and the duplication of the conditional (in mouseUp(), mouseMove(), etc.) makes adding new tools complicated.

Subclassing is not an immediate answer either because the editor needs to change tools during its lifetime. Delegation allows that flexibility.

public void mouseDown() {
  getTool().mouseDown();
}

The code that used to live in the clauses of the switch statement is moved to the various tools. Now new tools can be introduced without modifying the code of the editor or the existing tools. Reading the code requires more navigation, however, because the mouse-down logic is spread over several classes. Understanding how the editor will behave in a given situation requires that you understand what kind of tool it is currently using.

Delegates can be stored in fields (a “pluggable object”), but they can also be computed on the fly. JUnit 4 dynamically computes the object that will run the tests in a given class. If a class contains old-style tests, one delegate is created, but if the class contains new-style tests, a different delegate is created. This is a mix of conditional logic (to create the delegates) and delegation.

Delegation can be used for code sharing as well as instance-specific behavior. An object that delegates to a Stream may be involved in instance-specific behavior, if the type of Stream can change at runtime, or it may be sharing the implementation of Stream with all the other users.

A common twist on delegation is to pass the delegator as a parameter to a delegated method.

image

If a delegate needs to send a message to itself, “itself” is ambiguous. Sometimes the message should be sent to the delegating object. Sometimes the message should be sent to the delegate. In the example below the RectangleTool adds a figure, but to the delegating GraphicsEditor, not to itself. The GraphicsEditor could have been passed as a parameter to the delegated mouseDown() method, but in this case it seemed simpler to store a permanent back-reference in the tool. Passing the GraphicsEditor as a parameter makes it possible to use the same tool in multiple editors, but if this isn’t important the code with the backpointer may be simpler.

image

Pluggable Selector

Let’s say you need instance-specific behavior, but only for one or two methods, and you don’t mind having all the variants of the code live in one class. In this case, store the name of the method to be invoked in a field and invoke the method by reflection.

Originally, each test in JUnit had to be stored in its own class (Figure 5.7). Each subclass only had one method. Classes seemed conceptually heavy as a way to represent a single class.

Figure 5.7. Trivial subclasses to represent different tests

image

By implementing a generic runTest(), ListTests with different names run different test methods. The name of the test is assumed to also be the name of a method which is retrieved and run when the test is run. Here is the simple version of the code to implement the pluggable selector version of running a test.

image

The simplified class hierarchy uses a single class (Figure 5.8). As with all code compression techniques, the modified code is only easy to read if you understand the “trick”.

Figure 5.8. Pluggable selector helps pack tests into a single class

image

When pluggable selectors first became widely known, people tended to overuse them. You would be looking at some code, decide it couldn’t possibly be called, delete, and have the system break because it was invoked by a pluggable selector somewhere. The costs of using pluggable selectors are considerable, but a limited use to solve a difficult problem may justify the cost.

Anonymous Inner Class

Java offers one more alternative for instance-specific behavior, anonymous inner classes. The idea is to create a class that is only used in one place, that can override one or more methods for strictly local purposes. Because it is only used in one place, the class can be referred to implicitly instead of by name.

Effective use of anonymous inner classes relies on having an extremely simple API—like implementing Runnable with its one method run()—or having a superclass that provides most of the needed implementation so the anonymous inner class can be implemented simply. The code for the anonymous inner class interrupts the presentation of the code in which it is embedded, so it needs to be short so it doesn’t distract the reader.

Anonymous inner classes have the limitations that the code to be set in the instance must be known when you write the class (unlike delegates, which can be added later) and it cannot be changed once an instance has been created. Anonymous inner classes are difficult to test directly and so shouldn’t contain complicated logic. Because they are un-named, you don’t have the opportunity to express your intention for an anonymous inner class with a well-chosen name.

Library Class

Where do you put functionality that doesn’t fit into any object? One solution is to create static methods on an otherwise-empty class. No one is expected to ever create instances of this class. It is just there as a holder for the functions in the library.

While library classes are fairly common, they don’t scale well. Putting all the logic into static methods forfeits the biggest advantage of programming with objects: a private namespace of shared data that can be used to help simplify logic. Try to turn library classes into objects whenever possible.

Sometimes this is as simple as finding a better home for a method. The Collections library class, for example, has a method sort(List). Such a specific parameter is a hint that this method probably belongs on List instead.

An incremental way to convert a library class to an object is to convert the static methods to instance methods. Maintain the same interface at first by having the static method delegate to an instance method. In a class called Library, for example,

public static void method(...params...) {
  ...some logic...
}

becomes:

image

Now, if several of the methods have similar parameter lists (and if they don’t then the methods probably belong in different classes), convert the method parameters to constructor parameters:

image

Next change the interface by moving instance creation to the clients and eliminating the static methods.

public void instanceMethod(...params...) {
  ...some logic...
}

This experience may give you ideas for how to rename the class and methods so client code reads clearly.

Conclusion

A class bundles together related state. The next chapter presents patterns that communicate decisions about state.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.144.254.138