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

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

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

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

Justin Gehtland; Bruce A. Tate

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

فونت

اندازه قلم

+ - پیش فرض

حالت نمایش

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








3.4 Refactoring to Reduce Coupling


You may start with a



cleanly defined design and
you may layer your design, as we've discussed, so
that each layer does one autonomous job. You may have coupling only
at the appropriate places. But if you don't try to
maintain that design, it won't last. Your code will
naturally move toward tighter coupling unless you fight that
tendency. In the last part of this chapter, we review some of the
types of coupling, and how to avoid them. The benefits of looser
coupling include:

Decoupling protects major subsystems of your architecture from each
other. If the coupling between your model and view are low, then
changes in one will not severely affect the other.

Loosely coupled code is easier to read. If you couple your business
domain model to your persistence framework, then you need to
understand both to read your domain model. Loosen the coupling and
you can read your domain model unencumbered.

Decoupling can improve reuse. It's harder to reuse
big blobs of software, just like it's harder to
reuse a full engine than a spark plug.


Keep in mind that some coupling is natural. You've
got to have some degree of coupling to do anything at all. Coupling
gets out of hand when things that don't belong
together are bound together. Your goal should be to avoid accidental
couplingyou want any coupling in your application to be
intentional and useful.

Also, keep in mind that decoupling often comes at a price. You can
add JMS queues and XML messages between every class, but
you'll work harder and your application will be dog
slow. Decoupling becomes much more important between major subsystems
and layers of your architecture.


3.4.1 Microcoupling


Most of the code written today



has
many relationships, and most of those relationships are tightly
coupled. Anything from a method call to the use of a common variable
increases your coupling. Like I said earlier, that's
not inherently bad. You just want to keep it intentional, and keep
your coupling confined to an area of the architecture.

Your software design strongly suggests where your coupling should be.
Excessive coupling across packages, and across layersin
general, excessive coupling across a focused ideabreaks down
your ability to do one thing and do it well. When you find it,
refactor it.


3.4.1.1 Direct access


The easiest type of coupling


to find is direct access. When you
directly call a method on another class or access its member
functions, you're coupled to it. You can break
coupling in a number of ways. When you're trying to
loosen the coupling from two classes, often the easiest way to is to
insert some kind of intermediary. The Java programming language
includes interfaces for this purpose. Bear in mind that interfaces
are useless for decoupling unless they are paired with a factory.
This line of code:

MyInterface myinterface = new MyObject( );

is no less tightly coupled than this one:

MyObject myobject = new MyObject( );

Whereas this one accomplishes the task and is completely decoupled:

MyInterface myinterface = MyFactory.getObject( );

Think about cars. They work because they have combustion engines that
drive axles, which spin wheels. In order to drive a car, you
don't manipulate the engine and axles directly; you
turn a steering wheel, press gas and brake pedals, and maneuver a
shift. The steering wheel, pedals, and shift make up the interface to
a car. There is a big difference between a '72
Beetle and a '04 Ferrari under the hood, but anybody
can drive either because they share an interface.

An interface lets you couple to a capability rather than an
implementation. Let's say that
you're building classes that you'd
like to fire when an event occurs. You could have your code
explicitly call the fire method on all of the classes that you want
to notify. This approach is limited to behavior that you can
anticipate at compile time.

A slightly better approach is to build a class that supports the
method fire. Then, everything that needs to be triggered can inherit
from that class. That's the solution many novice
Java developers use. It's limiting, because you may
want to trigger other types of classes too. Instead, use an interface
called Firable:

interface Firable {
public void fire( );
}

Notice that you don't see an implementation. Now,
whenever you want a Firable class, you simply
implement the interface:

public class AlarmClock implements Firable {
public void fire( ) {
System.out.println("Ring!");
}
}

Now, other classes can use your
"fire" method without coupling
directly to yours:

public void wakeUp(Firable clock) {
clock.fire( );
}

The idea is to couple to an idea rather than an implementation. You
don't want to build an interface that repeats every
method of a class. Instead, break out the concepts that you want to
expose in the interface. If you find yourself addicted to JUnit as I
have, you'll use this trick with some frequency. The
nice thing about this approach is that you don't
have to have any special behavior to test the alarm clock. You can
also quickly mock a Firable class to help test
code that fires your interface.

Interfaces serve as intermediaries, and you can decouple with other
kinds of intermediaries as well. A façade is an
intermediary that is nothing more than a thin wrapper. At first
glance, you might think that you're trading coupling
from one area of the application to the other, so you gain nothing at
all. That premise is not entirely true. You'll see a
few direct benefits:

Your thin façade hides the details of the existing
outbound interface. If the code it is wrapping ever changes, you can
react to that change in the façade, leaving the other code
intact.

The façade is thin, and cheap. The code it is wrapping
probably isn't. If you need to throw away the
façade, you have not lost much.


You've probably seen other kinds of intermediaries
as well. Rather than initialize a class with a new one, followed
immediately by many sets, you can insert a constructor as an
intermediary to enforce a policy for construction and consolidate
several method calls. If you need to consistently call five methods
to do a job, such as to establish a JDBC connection, you can wrap
that code into a single method, or a class, like a connection
manager.


3.4.1.2 Inheritance


Inheritance is one of


the least understood mechanisms in modern
programming. It is tempting to use the casual
"inheritance is for is-a
relationships," but this is just semantic
handwaving. Everything "is-a"
something else. Conceptually, there are two kinds of inheritance:
implementation and
interface. When a class inherits from another
class and by doing so inherits actual implementation details (field
values or code blocks), that is implementation inheritance. When a
class implements an interface, thus promising to provide the services
described there but without inheriting any specific values or code,
that is interface inheritance.

In languages like C++, where multiple implementation inheritance is
allowed, the problem can be quite severe. Classes that inherit from
multiple direct parents can become logical
Frankenstein's monsters, half-living beasts that
don't quite look normal and never behave. Newer
languages like Java solve part of the problem by eliminating multiple
implementation inheritance. A Java class can have only one direct
parent class (which in turn can have one direct parent, and so on).
The chain is easier to follow and the results more predictable.
However, classes can implement as many interfaces as they desire.
Since interfaces do not impart specific implementation details to the
implementer, just a public contract for services provided, the
results are again easier to predict. Since any implementation code
sitting behind an interface is living in the class itself, there is
never the question of hidden conflicts and accidental overrides
creating random runtime behavior.

In order to decide which kinds of ideas require which kind of
inheritance, it requires a little common sense and a little Zen
meditation. When two or more classes represent specific categories of
a single idea (Employees and Customers are both a kind of Person),
then implementation inheritance makes sense. Person is a good
candidate for a superclass. All logical children of that idea share
the data and methods abstracted back to the Person object.

Interfaces, on the other hand, are useful for identifying services
that cross-cut the logical model of the application. Imagine you are
writing an application for a veterinary clinic. You might have two
classes, Employee and Customer, which both inherit from Person. You
might also have three other classes, Cat, Dog, and Bird, all
inheriting from Animal. If they should be persistent, you can
implement the PersistentObject interface as needed. The key is that
each kind of person must be a Person; they need not necessarily be
persistent. Each kind of animal must be an Animal, but they only
may be persistent.


3.4.1.3 Transitive coupling


Keep in mind that



coupling is transitive. If A is
coupled to B, and B is coupled to C, then A is coupled to C. This
type of coupling often seems innocuous, but it can get out of control
in a hurry. It's especially painful when
you're dealing with nested properties. Whether you
have something like this:

store.getAddress( ).getCountry( ).getState( ).getCity( )

or something like this:

address.country.state.city

you're building a whole lot of assumptions into a
very small place. Dave Thomas, founder of the Pragmatic Programmer
practice, calls this programming style the "Java
train wreck." The worst form of the train wreck
reaches into many different packages. The problem is that
you're coupling all four classes together. Think of
the things that might some day change. You might need to support
multiple addresses, or international provinces instead of states.

Decouple this kind of code. You might decide to add some convenience
methods for your customers or you might need to build a flatter
structure, or even determine why you need access to the city in the
first place. If it's to compute a tax, you might
have a getTax method that isolates this coupling
to one place. If it's because stores in certain
cities have special attributes, you may add the attributes or methods
to Store to loosen the overall coupling.


3.4.1.4 The role of transparency


Sometimes, you want to apply



a little extra energy and completely focus
certain pieces of code. For example, recall that we wanted to add
security to our Account class without adding
special security methods. We would do so with another layer. You
would say that the Account class is transparent
with respect to security.

Business rules often need special treatment, because they tend to be
complex and tend to change with great frequency. Increasingly,
leading edge Java developers look to find ways to isolate the
business domain model from other concerns. Right now, the Java
community is struggling to find the right way to package service
layers, in order to keep business domain models fully transparent.
Component architectures like EJB say that you should build your
application as components and snap them into containers that provide
the services. This architecture has tended to be too invasive and
cumbersome. Instead, others say that services should be packaged as
aspects, using a new development model called

Aspect-Oriented
Programming (see Chapter 11). As a compromise,
many people are working to develop lighter containers that allow
plain Java objects rather than special components. Pico and Spring
(covered in Chapter 8) are two lightweight
containers that are growing in popularity.


3.4.1.5 Testing and coupling


As you've already seen, your first defense against
tight coupling is good, solid unit testing of bite-sized building
blocks. As you code, you'll likely build an
implementation, use that implementation in your code, and then reuse
it again within your unit tests, as in Figure 3-5.
Since you've built at least two clients into your
development model and intend to test bite-sized pieces,
you're much more likely to keep your coupling down
to a level that's easy to manage. Further, your test
cases will use each new class outside of its original context. With
test-first development, you'll quickly understand
where your coupling and reuse problems lie.



Figure 3-5. Testing offers the chance to have multiple clients for your new classes


3.4.2 Macrocoupling


Coupling at a higher


level,
or macrocoupling, is usually a much more serious
problem than microcoupling because you want to keep each software
layer as autonomous as possible. For years, distributed technologies
forced significant coupling, making many kinds of refactoring nearly
impossible. Communication protocols forced clients and servers to
manage intricate handshakes even to make a connection. Later, remote
procedure call technologies forced clients to bind directly to a
named procedure with a fixed set of parameters and fixed orders and
types. CORBA took things a step further, and forced clients to bind
to a whole specific object.

Today, you don't usually have to couple as tightly.
A variety of technologies help build and connect independent systems.
You can fight macrocoupling on several different levels.


3.4.2.1 Communication model


Your communication model


can have a dramatic impact on the degree
of coupling between your systems. Early in your design process, make
some painless decisions that reduce the coupling between your
systems:

Prefer asynchronous messages where coupling is a factor. A one-time
message using something like JMS generally requires less coupling
than synchronous technologies like session beans or RMI.

Prefer established standards to reduce dependencies. If you build a
standards-based communication interface, you're
placing fewer restrictions on either end of the wire.

To reduce coupling, use a flexible payload. If you fix the number,
order, or type of parameters, you're going to
increase your coupling. Instead, try to use a more flexible payload,
like a system using name-value pairs (such as a JMS mapped message)
or an XML document.


Each of these techniques can reduce coupling, but remember that
sometimes coupling is good. If you've got strict
control of both ends of an interface, and if you
don't expect the interface to change, then a tighter
coupling can possibly buy you better performance. For the most part,
however, it's usually worth it to pay a small
performance penalty to reduce coupling from the beginning.


3.4.2.2 Façades


Façade layers don't



really
reduce your coupling. Instead, they let you couple to something
that's a little less permanent. In addition,
façades have some other important benefits:

You can change a lightweight façade much more easily than
you can change your business domain model.

A façade lets you adapt your interface. You may want to
translate value objects to XML documents to have a looser coupling
over a distributed interface. A façade is a logical place
for that conversion.

A façade can let you build a coarse-grained interface to a
fine-grained model. For example, instead of forcing your user
interface to read each element of your invoice, you could return an
entire invoice, such as the one in Figure 3-6.

A façade provides a convenient attachment point for
services. Transactions and security work well within a
façade layer.




Figure 3-6. Client 1 must make four round-trip communications to the server; Client 2 reduces the total number of communications to one


3.4.2.3 Shared data


Applications that
share

interfaces usually need to share data as
well. Whether you're using a buffer or a parameter
list, the problem is the same. If you fix the number, type, order, or
size of parameters, you're asking for trouble,
because changes in the payload invariably force both sides of the
interface to change. These strategies can help you to reduce coupling
between subsystems that share data:

Allow optional parameters


When you don't control both ends of an interface,
you need to have features that allow both sides to advance at their
own pace. Optional parameters let you support a new feature without
mandating immediate support on both ends of the interface.


Use name-value pairs


For interfaces that have a list of parameters, it's
often better to name each parameter and pass them as a series of
name-value pairs. Hash tables, maps, and XML documents all let you
build name-value pairs as input parameters. If you can support
arbitrary text, you can handle XML. If XML seems to be overkill, JMS
supports mapped messages, and you can use collections to handle this
message type if you share memory addresses. The advantage of this
approach is that applications need not depend on order, and optional
parameters can be safely ignored.



Flexible data interchange formats are both a blessing and a curse.
Your endpoints are more flexible in the face of changes to the
payload, but it is more difficult to know exactly what is being
shared. The more loosely typed your data interchange format, the more
self-describing it must be. This is vital. If you pass name-value
pairs, make sure that the consumer of the data can enumerate over
both the values and the names. XML is a perfect format, since it is
inherently self-describing.


3.4.2.4 Databases


The data access layer is




one
of the most problematic for Java developers to isolate. It
doesn't need to be. Many good frameworks and
solutions let you build an independent, transparent business model
that knows nothing about persistence. Many persistence frameworks
(such as Hibernate, JDO, and OJB) handle this well.

You must also ask whether you need a full relational database
management system (RDBMS). Relational databases are large, expensive
(in both resources and dollars) and complex. Sometimes, flat files
are all that is needed. Make sure that you need what it is you are
trying to wrap.

Regardless, you need not bite off a full persistence framework to
solve a good chunk of this problem. You can build a lightweight DAO
layer (like the one that we started for this
chapter's example) to manage all data access for
your application. There are a variety of IDEs and standalone tools
that generate DAO layers automatically.


3.4.2.5 Configuration


Many times, you might want



to
avoid a particular standardized service to use a lighter, faster
proprietary service. If you did so, you would have better performance
and an easier interface, but you could be boxing your users into a
corner. The makers of Kodo JDO faced that problem, and decided to
make the service configurable. Increasingly, frameworks use
configuration to decouple systems. Better configuration options
invariably reduce coupling.

This list is far from exhaustive. If you want to excel at finding
coupling problems, you've got to sharpen your
observation skills. There's simply no substitute for
reading code and watching the usage patterns, especially around the
perimeter of a layer.


/ 111