Better Faster Lighter Java [Electronic resources] نسخه متنی

اینجــــا یک کتابخانه دیجیتالی است

با بیش از 100000 منبع الکترونیکی رایگان به زبان فارسی ، عربی و انگلیسی

Better Faster Lighter Java [Electronic resources] - نسخه متنی

Justin Gehtland; Bruce A. Tate

نمايش فراداده ، افزودن یک نقد و بررسی
افزودن به کتابخانه شخصی
ارسال به دوستان
جستجو در متن کتاب
بیشتر
تنظیمات قلم

فونت

اندازه قلم

+ - پیش فرض

حالت نمایش

روز نیمروز شب
جستجو در لغت نامه
بیشتر
لیست موضوعات
افزودن یادداشت
افزودن یادداشت جدید

6.1 The Basics of Extension

Extension 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:

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

A 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 services

It's only a rough rule of thumb, though, and it can 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.

6.1.2.1 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 messaging.

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 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:

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 separates the 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 extension and the tools that achieve it.

/ 111