![]() |
|
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
|
![]() |
![]() |
Tips 'N Tricks
SummaryBy Jeff Mather
Don't give up on your efforts to write reusable code! This tip outlines three ways you can modify your existing code to increase its reusability. (1,500 words)
hat
reuse is a myth seems to be an increasingly common sentiment among programmers.
Perhaps, however, reuse is difficult to achieve because deficiencies exist in
the traditional object-oriented programming approach to reuse. This tip
describes three steps that form a different approach to enabling reuse.
Step 1: Move functionality out of class instance
methods
Class inheritance is a suboptimal mechanism for code
reuse due to its lack of precision. Namely, you cannot reuse a single method of
a class without inheriting that class's other methods as well as its data
members. That excess baggage needlessly complicates the code wishing to reuse
the method. The dependency of an inheriting class on its parent introduces
additional complexity: changes made to the parent class can break the subclass;
when modifying either class, it can be difficult to remember which methods are
or are not overridden; and, it can be unclear whether or not an overridden
method should call the corresponding parent method.
Any method that performs a single conceptual task should be able to stand on its own as a first-class candidate for reuse. To achieve that, we must revert back to procedural programming by moving code out of class instance methods and into globally visible procedures. To promote the reuse of such procedures, you should code them just like static utility methods: each procedure should use only its input parameters and/or calls to other globally visible procedures to do its job, and should make no use of any nonlocal variables. That reduction in external dependencies decreases the complexity of using the procedure, thereby increasing the motivation for reusing it elsewhere. Of course, even code that is not intended for reuse benefits from that organization, as its structure invariably becomes far cleaner.
In Java, methods cannot stand on their own outside of a class. Instead, you can take related procedures and make them publicly visible static methods of a single class. As an example, you could take a class that looks something like this:
class Polygon
{
.
.
public
int getPerimeter() {...}
public boolean
isConvex() {...}
public boolean
containsPoint(Point p)
{...}
.
.
}
and change it to look something like this:
class Polygon
{
.
.
public
int getPerimeter() {return
pPolygon.computePerimeter(this);}
public
boolean isConvex() {return
pPolygon.isConvex(this);}
public boolean
containsPoint(Point p) {return pPolygon.containsPoint(this,
p);}
.
.
}
Here, pPolygon
would be this:
class pPolygon {
static public int
computePerimeter(Polygon polygon) {...}
static public
boolean isConvex(Polygon polygon) {...}
static public
boolean containsPoint(Polygon polygon, Point p) {...}
}
The class name pPolygon
reflects that the procedures enclosed by
the class are most concerned with objects of type Polygon
. The
p
in front of the name denotes that the class's only purpose is to
group publicly visible static procedures. While it is nonstandard in Java to
have a class name start with a lowercase letter, a class such as
pPolygon
does not perform the normal class function. That is, it
does not represent a class of objects; it is rather just an organizational
entity required by the language.
The overall effect of the changes made in the above example is that client
code no longer has to inherit from Polygon
to reuse its
functionality. That functionality is now available in the pPolygon
class on a procedure-by-procedure basis. Client code uses just the functionality
it needs, without having to concern itself with the functionality it doesn't
need.
That is not meant to imply that classes don't serve a useful purpose in that neoprocedural programming style. Quite to the contrary, classes perform the necessary task of grouping and encapsulating the data members of the objects they represent. Moreover, their ability to become polymorphic by implementing multiple interfaces is the preeminent reuse enabler, as explained in the next step. However, you should relegate reuse and polymorphism through class inheritance to less-favored status in your arsenal of techniques, since keeping functionality tangled up within instance methods is less than optimal for achieving reuse.
A slight variant of that technique is briefly mentioned in the Gang of Four's widely read book Design Patterns. Their Strategy pattern advocates encapsulating each family member of related algorithms behind a common interface so that client code may use those algorithms interchangeably. Since an algorithm is usually coded as either one or a few isolated procedures, that encapsulation emphasizes reuse of procedures that perform a single task (i.e., an algorithm), over reuse of objects containing code and data, which may perform multiple tasks. That step promotes the same basic idea.
However, encapsulating an algorithm behind an interface implies coding the algorithm as an object that implements that interface. That means we are still tied to a procedure that is coupled to the data and other methods of its enclosing object, therefore complicating its reuse. There is also the matter of having to instantiate those objects every time the algorithm needs to be used, which can slow program performance. Thankfully, Design Patterns offers a solution that addresses both of those issues. You can employ the Flyweight pattern when coding Strategy objects so that there is only one well-known, shared instance of each (which addresses the performance issue), and so that each shared object maintains no state between accesses (so the object will have no member data, which addresses much of the coupling issue). The resulting Flyweight-Strategy pattern highly resembles this step's technique of encapsulating functionality within globally available, stateless procedures.
Step 2: Change nonprimitive input parameter types to
interface types
Taking advantage of polymorphism through
interface parameter types, rather than through class inheritance, is the true
basis of reuse in object-oriented programming, as stated by Allen Holub in "Build
User Interfaces for Object-Oriented Systems, Part 2".
"...you get reuse by programming to interfaces rather than to classes. If all the arguments to a method are references to some known interface, implemented by classes you've never heard of, then that method can operate on objects whose classes didn't even exist when the code was written. Technically, it's the method that's reusable, not the objects that are passed to the method."
Applying Holub's statement to Step 1's results, once a block of functionality can stand on its own as a globally visible procedure, you can further increase its reuse potential by changing each of its class-type input parameters to an interface type. Then, objects of any class that implements the interface type may be used to satisfy the parameter, rather than just those of the original class. Thus, the procedure becomes usable with a potentially larger set of object types.
For example, say you have a globally visible static method:
static public boolean contains(Rectangle rect, int x, int y)
{...}
That method is meant to answer whether the given rectangle contains the given
location. Here you would change the type of the rect
parameter from
the class type Rectangle
to an interface type, shown here:
static public boolean contains(Rectangular rect, int x, int y)
{...}
Rectangular
could be the following interface:
public interface Rectangular
{
Rectangle getBounds();
}
Now, objects of a class that can be described as rectangular (meaning can
implement the Rectangular
interface) can be supplied as the
rect
parameter to pRectangular.contains()
. We have
made that method more reusable by loosening the restrictions on what may be
passed to it.
For the above example, however, you might be wondering whether there is any
real benefit to using the Rectangular
interface when its
getBounds
method returns a Rectangle
; that is to say,
if we know the object we want to pass in can produce such a
Rectangle
when asked, why not just pass in the
Rectangle
instead of the interface type? The most important reason
not to do that deals with collections. Let's say you have a method:
static public boolean areAnyOverlapping(Collection rects)
{...}
that is meant to answer whether any of the rectangular objects in the given
collection are overlapping. Then, in the body of that method, as you iterate
through each object in the collection, how do you access that object's rectangle
if you can't cast the object to an interface type such as
Rectangular
? The only option would be to cast the object to its
specific class type (which we know has a method that can provide the rectangle),
meaning the method would have to know ahead of time on which class types it will
operate, limiting its reuse to those types. That's just what that step tries to
avoid in the first place!
Step 3: Choose less-coupling input parameter interface
types
When performing Step 2, which interface type should be
chosen to replace a given class type? The answer is whichever interface fully
represents what the procedure needs from that parameter with the least amount of
excess baggage. The smaller the interface the parameter object has to implement,
the better the chances for any particular class to be able to implement that
interface -- and therefore, the larger the number of classes whose objects can
be used as that parameter. It is easy to see that if you have a method such as:
static public boolean areOverlapping(Window window1, Window window2)
{...}
which is meant to answer whether two (assumed to be rectangular) windows overlap, and if that method only requires from its two parameters their rectangular coordinates, then it would be better to reduce the types of the parameters to reflect that fact:
static public boolean areOverlapping(Rectangular rect1, Rectangular
rect2) {...}
The above code assumes that the objects of the previous Window
type can also implement Rectangular
. Now you can reuse the
functionality contained in the first method for all rectangular objects.
You may experience times when the available interfaces that sufficiently specify what is needed from a parameter have too many unnecessary methods. In that case, you should define a new interface publicly in the global namespace for reuse by other methods that might face the same dilemma.
You may also find times when it is best to create a unique interface to specify what is needed from just one parameter to a single procedure. You would use that interface for that parameter only. That usually occurs in situations where you want to treat the parameter as if it's a function pointer in C. For example, if you have a procedure:
static public void sort(List list, SortComparison comp) {...}
that sorts the given list by comparing all of its objects, using the provided
comparison object comp
, then all sort
wants from
comp
is to call a single method on it that makes the comparison.
SortComparison
should therefore be an interface with just one
method:
public interface SortComparison
{
boolean comesBefore(Object a, Object
b);
}
The only purpose of that interface is to provide sort
with a
hook to the functionality it needs to do its job, so SortComparison
should not be reused elsewhere.
Conclusion
Those three steps are
meant to be performed on existing code that was written using more traditional
object-oriented methodologies. Together, those steps combined with OO
programming can constitute a new methodology that you can employ when writing
future code, one that increases the reusability and cohesion of methods while
reducing their coupling and complexity.
Obviously, you should not perform those steps on code that is inherently
ill-suited for reuse. Such code is usually found in a program's presentation
layer. The code that creates a program's user interface and the control code
that ties input events to the procedures that do the actual work are both
examples of functionality that change so much from program to program that their
reuse becomes infeasible.
About the author
Jeff Mather works for Tucson, Ariz.-based eBlox.com, where he creates applets for
companies in the promotional materials and biotechnology industries. He also
writes shareware games in his
spare time.
![]() |
![]() |
|
![]() |
Copyright © 2002 JavaWorld.com, an IDG Communications company |
![]() |
![]() |
![]() |