Design For Reuse

From eqqon

Revision as of 11:45, 7 December 2007 by Henon (Talk | contribs)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Jump to: navigation, search

Contents

The Value of Single-Purpose-Classes

Most of the classes that get written are used once and never again. The same special problem the class is intended to solve will never appear again. The effort of creating and testing them is practically wasted because it's value is gained only once for a specific part of a software. Often, similar problems arise but adopting the existing classes is too expensive so we need to write new classes again from scratch. If we were able to write classes that are highly reusable we had the opportunity to build a huge resource of diversely applicable modules which could be reused over and over again without any overhead. The commercial potential of a highly reusable class is enormous for a software company.

A Design Guide to Highly Reusable Classes

To rip off these benefits it is essential to understand what makes a class highly reusable and what design guidelines to follow. Here they are:

Single Purpose Classes

The most important thing is to keep a class small and simple. The rule of thumb here is to analytically dissect the functionality and divide everything into different classes what can be divided. A functionality immediately looses its reuse potential if it is mixed with other functionalities. A class which is made only for a single purpose and holds only a single simple functionality is what you should be going for. The class is easily reused because its implementation is slim and can be easily understood. The documentation of the class is slim because it has only one simple functionality. The simple class can be combined with other classes to solve complicated problems.

A badly factored class holding multiple functionalities when reused in a different context always bears the risk of unforeseeable bugs resulting from a functional part of the class that does not fit in the new context. When attempting to reuse a multi-purpose-class it might be necessary to work around some aspects which are not needed in the reuse context effectively reducing the value of reusing it. The risk that the reusing engineer is misunderstanding how the multiple functionalities work together in a fat class is high. The result are bugs which are hard to spot which cut the value gained from reusing, or even worse, force you to write it all new.

Side-Effect Free Methods

The methods of a class must not have any side-effects. A side-effect is something that might not be expected by the caller of the method. Side-effects inherently poison any attempts to reuse. Side-effect eliminating workarounds nullify any value gained from reusing. The resulting solution is prone to errors and hard to understand.

If there are any mandatory side-effects which cannot be avoided at all convert them into regular expected functionality by asserting on pre- and post-conditions that enforce the side-effect behavior.

Naming

Names are not just hollow words. It is extremely important to name classes and methods (and all other identifiers) such that the name describes the purpose of the class/method as exactly as possible. Put some effort in finding a good name and the users of your class (who might be you yourself after some time) will find it tremendously easier to understand and reuse it.

Badly chosen names lead to bugs or even prevent possible reuse which is, from our point of view, the worst thing that could ever happen.

Decoupling through Events

Putting different functionality in different classes is often not enough to provide a reusable solution. If those classes were to notify each other by maintaining hard coded references to each other they would be too highly coupled to be reused singularly. Events resolve such reuse-stopping dependencies and render related classes replaceable by any other class. The interchangeability of a piece of functionality is most essential for achieving reusable code.

Use Interfaces to Decouple Unrelated Functionalities

Classes which implement services often need to call methods on their clients or even store them in a data structure. Using events for decoupling sometimes is not possible or efficient enough. The service class could enforce all their clients to inherit from an abstract client class or something like that but these unnecessary restrictions would decrease the usability and prevent reuse. The solution is to declare a client interface which is required for the client classes to request the services of the class in question.

Use the Strategy Pattern to Abstract from Specific Needs

Quite often when analytically dissecting the functionality of a design I come to a point where there might be multiple ways of doing something. Those who reuse your class will surely need something different than you need right now. This is when you should think about applying the Strategy Pattern [Gamma et al 95]. The functionality or algorithm that might be replaced by a similar but different one needs to be factored out and placed in its own class. The user may supply its own algorithm and replace yours.

Use Composition in Favor of Dubious Inheritance

Quite often the same problem where one could apply Strategy is solved by using inheritance. The badly factored class where the strategy pattern should have been applied is sub-classed and the specific parts are overridden. Despite the fact, that it is a commonly accepted way of solving the reuse problem, I think most of the time it were better to use Strategy instead of dubious inheritance. Inheritance is most useful if you are able to make use of polymorphism and late binding. If inheritance is used just to reuse some functionality in another unrelated class and nothing else then it surely is better to use composition instead. With composition, the other class that has a functionality your class needs is just used as is. It is owned or referenced by the user class instead of inherited. This leaves your user class free to inherit from a class that makes more sense to inherit from and brings you the freedom of replacing the used functionality without changing class hierarchies.


A Practical Example

Once you get used to it, it is not very hard to follow the guide. Of course, this approach has a little "thinking and refactoring" overhead over a naive approach but in the end you quickly get a large set of small grained highly reusable classes which can be combined to flexible solutions of complicated problems with little effort.

I will try to explain the design guidelines on a small, easy to understand example where I started with a single not very reusable class and after some refactoring ended up with a quite flexible result consisting of six small highly reusable classes.

... to be continued ...