6.1 The Basics of Extension
Extension is in many ways an
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:
Extensibility is the ability to quickly adapt code to do things that
it was not built to do, in ways both planned and unplanned.
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 refactoringeven late in the application life
cyclewith minimal effort. In this section, I briefly review
several core concepts that lead to better flexibility.
6.1.1 Inheritance and Interfaces
The 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 services
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 services
be taken too far. Interfaces can be abused as well:You don't need an interface for every
classonly those that implement special abstract concepts.
After having success with interfaces, some rigid managers go too far
and demand all classes have an interface. This process leads to
poorly designed interfaces and frustrated developers.An interface should not expose every method in a classonly
those that relate to the concept that you're
presenting. Beginning programmers who've just
discovered interfaces make this mistake often, either because they
can cut and paste or because they don't understand
fundamentally what interfaces are trying to do.
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 Extension
In 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.
220.127.116.11 The Inventor's Paradox
When 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:What's likely to change?
You can't spend all of your time generalizing every
block of code, but you can identify areas that you may need to
future-proof. MVC is a famous design pattern because views and models
change. It's important to generalize the way you
deal with your views so you can change at need. If you intentionally
identify and generalize these interfaces, you'll
often be much better off. I'm not arguing for more
complexity. You are looking for ways to generalize and simplify at
the same time.
Is there a different way to solve this cumbersome problem?
When I get in trouble, I usually step back and ask myself,
"Why this way?" For example, many
open source projects read configuration files as XML DOM (Domain
Object Model) trees. Many developers begin to look at configuration
as a Java representation of an XML file. It's not.
Instead of looking for ways to efficiently lob DOM trees across your
application, look for the reason that you're doing
so. Maybe it's better to read that configuration
file once, and translate it to a smaller set of concrete objects
representing your configuration. You can share those at will.
Have I seen this problem before in another context?
Simple generalizations often show up in dramatically different
contexts. For example, it took me a while to see that the
model-view-controller concepts are not limited to views. You can
generalize a view as just another interface. You can apply MVC-like
ideas to many different types of services, including persistence and
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 applicationsfrom
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 Extension
Not all requirements can or should
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:
Expose the right methods, with the right granularity
Methods should be fine-grained and
handle a single concept. If your methods
bundle up too many concepts, you won't be able to
extend the class by overriding the method.
Use Java interfaces
Providing general Java interfaces
interface from implementation details. If you see a service or
capability that's buried in a class definition,
break it out into a separate interface.
Loosen coupling between key concepts
This concept always comes up in good Java programming books for a
reason. It works.
Keep designs clear and simple
Code that's hard to read and
understand will be hard to extend.
Publish the code under an open source license
An application with source that can be examined and modified is much
easier to extend then a closed-source application.
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
the tools that achieve it.