< Day Day Up > |
6.1 The Basics of ExtensionExtension is in many ways an awkward topic to write about because it encompasses so many different design principles. I've included it in this book because it's a fundamental capability for good applications. You need extensibility if you decide to apply the first four principles outlined in Chapters Chapter 2 through Chapter 5 because simplicity without flexibility is worthless. I'll define extensibility like this:
With this definition, I'm deliberately painting with a broad brush. It's important to do so, because you'll find value in many different types of extension. Some please your customers by letting you efficiently address short-term change. Others ease the burden of maintenance by allowing sweeping refactoring—even late in the application life cycle—with minimal effort. In this section, I briefly review several core concepts that lead to better flexibility. 6.1.1 Inheritance and InterfacesThe two most basic means of extension are inheritance and interfaces. If you were creating an interface for an electrical component, you could either attach your implementation to a ready-made plug or design your component to meet the specifications of the interface. With OOP, you have two similar alternatives (among others): first, you could think of a superclass as a working plug extended by a subclass; secondly, you could implement a Java interface. Each technique has strengths and weaknesses. Most beginning programmers automatically reach for inheritance, because it requires fewer files and less code, although most of the evil in this world happens because someone wanted to save a few keystrokes. In OOP's earliest days, inheritance was the tool of choice for reuse and extension. As you might expect, many programmers abused inheritance badly. Still, there's a place for inheritance and abstract classes, in particular. If you're trying to preserve a default behavior with a common API, an abstract class is often the best way to go. Keep in mind that you want to use inheritance to capture some kind of is-a relationship, and not extend some form of service or capability to all classes. You've probably run across limiting code that's been written in this way. For example, consider a Person class that inherits from PersistentThing. How might you extend Person to also be transactional? Figure 6-1 shows three options, although none of them are good. Option 1 doesn't work if you want to add an entity that's persistent but not transactional. Option 2 won't allow only persistence. Option 3 does not support existing subclasses of Person. Figure 6-1. Inheritance can be a dangerous tool for adding servicesA rough rule of thumb is to prefer interfaces to concrete inheritance, especially when you're trying to present a capability rather than an is-a relationship. Interfaces allow you to extend a class along more than one axis, as in Figure 6-2. You are not limited to a single service, and each service is independent and adaptable independently. Figure 6-2. Interfaces let you extend a class to implement additional servicesIt's only a rough rule of thumb, though, and it can be taken too far. Interfaces can be abused as well:
When you're deciding between interfaces and abstract classes, the acid tests are type of relationship and behavior. If you need to capture default behavior, lean toward the subclass. You must also consider the abstraction that you're trying to provide. Remember, inheritance is most appropriate for capturing an is-a relationship, while interfaces seek to expose a basic capability. Abusing these rules leads to complications down the road, such as the one in Figure 6-1. 6.1.2 Planned ExtensionIn some cases, you can explicitly allow customers to extend your framework in predetermined ways. For example, Ant allows plug-ins in the form of Ant tasks. Plug-ins anticipate your customer's need for extension. Planned extensions often take effort and foresight: don't invest in them lightly. Listen to your instincts and know your customer. Let's say that you've decided you need a specific extension. Further, it's a specific, difficult, tedious area to expose to your customer. It helps to look at the problem in a different way. You could break it down, which often allows two or more easier extensions. You could also try to generalize the problem. 6.1.2.1 The Inventor's ParadoxWhen you're solving a problem, you often decide to limit yourself to something that's as specific as possible. When you do so, you usually place awkward limits on developers who wish to use your framework and customers who would use it. General solutions often solve more problems than a specific solution does. There's an interesting side benefit. You can often solve the general problem with less effort, cleaner designs, and simpler algorithms. That's the Inventor's Paradox. In How to Solve It (Princeton University Press), a book that's quite famous in mathematics circles, George Polya introduces the Inventor's Paradox: "The more ambitious plan may have more chances of success." In other words, you can frequently solve a useful general problem more effectively than a highly specialized one. It's a principle that works often in math. For example, try totaling all of the numbers from 1-99, in sequence. Then, think of it in this way: (1 + 99) + (2 + 98) + ... + (49 + 51) + 50. You can probably solve the second equation in your head. Once you generalize the problem in this way, you can quickly sum any sequence of numbers. The general problem is easier to solve. It works in industry, too, as you saw in my duct tape example. There are many examples of the Inventor's Paradox in programming. Often, the most successful frameworks are simple generalizations of a complex problem. Apache web server plug-ins, Visual Basic custom controls, and the Internet are but a few examples of simple generalizations. Closer to home, Ant and Tomcat surpassed the wildest expectations of their author, James Duncan Davidson. Both of these frameworks allow exquisitely simple, elegant extensions. You don't have to have the luck of James Bond or the intellect of Albert Einstein to make the Inventor's Paradox work for you. Simply train yourself to look for opportunities to generalize. Start with this list of questions:
In the next couple of chapters, you'll see these principles in action. Spring generalizes a concept called inversion of control and uses a generalized architecture to assemble and configure entire applications—from the database to the user interface and everything in between. Rather than including a stored procedure framework, Hibernate exposes the JDBC connection, allowing users to extend Hibernate in ways that the inventors often never considered. The Inventor's Paradox represents all that's fun about programming: finding simple, elegant solutions to difficult problems. When you do, you'll find that patterns that seemed tedious in books emerge as new creations. But it's only the first step in allowing for extension. 6.1.3 Unplanned ExtensionNot all requirements can or should be anticipated. Building simple software often means waiting to incorporate future requirements until they're needed. You don't have to completely write off the future, though. By making good decisions, you can make it easy to extend your frameworks in ways you might not have originally intended. You do so by following good design principles:
The key to extensibility has always been the same: build an architecture that separates key concepts and couple them loosely, so any given concept can be replaced or extended. You can see that the earlier concepts in this book (like transparency, focus, and simplicity) all come into play, especially for unplanned extension. For the rest of the chapter, I focus on planned extension and the tools that achieve it. |
< Day Day Up > |