Maximize your code reusability

Overcome deficiencies in the traditional OOP approach to reuse

Maximize your code reusability Overcome deficiencies in the traditional OOP approach to reuse


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

classPolygon{
      .
      .
      publicintgetPerimeter(){...}
      publicbooleanisConvex(){...}
      publicbooleancontainsPoint(Point p){...}
      .
      .
}


and change it to look something like this:

classPolygon{
      .
      .
      publicintgetPerimeter(){return pPolygon.computePerimeter(this);}
      publicbooleanisConvex(){return pPolygon.isConvex(this);}
      publicbooleancontainsPoint(Point p){return pPolygon.containsPoint(this, p);}
      .
      .
}

Here, pPolygon would be this:

classpPolygon{
    staticpublicintcomputePerimeter(Polygon polygon){...}
    staticpublicbooleanisConvex(Polygon polygon){...}
    staticpublicbooleancontainsPoint(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.

 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:

staticpublicbooleancontains(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 typeRectangle to an interface type, shown here:

staticpublicbooleancontains(Rectangular rect, int x, int y){...}

Rectangular could be the following interface:


publicinterfaceRectangular {
      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 itsgetBounds 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 theRectangle instead of the interface type? The most important reason not to do that deals with collections. Let's say you have a method:

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


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

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

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


publicinterface 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, soSortComparison 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.





To view or add a comment, sign in

Insights from the community

Others also viewed

Explore topics