DIMAJIX - software consulting (http://www.dimajix.de/)
 

Design Specific Topics

This section is about more general topics which are not bound to a single language like C++, but are of a broader nature and apply to most object oriented languages.


Rule 62. Prefer many small and simple classes over few complex classes.

A common mistake made by many beginners is to endow a single class with many different methods. This leads to very complex classes which are hard to maintain and to understand. You should prefer to limit each class to its very basic characteristics. Additional features can be implemented in other classes that work on the first one.

For example consider a class for strings. Such a class can be found in most programming environments and it simplifies the manipulation of text elements by a magnitude. But one should limit the operations of the string class to the essential ones. For example you should avoid to add conversion functions for other data types like integers or float values. These routines are best kept in different classes.

In many situations a filter pattern offers an appropriate solution. A filter is a class that accepts and object as input and transforms this object into a new one (think of graphics filters which sharpen or soften an image). You can even overload the << operator to enforce this character, like in the following example:

// Some image class
class Image {
    ...
};

// An abstract filter class
class Filter {
public:
    inline Image     operator<<(const Image& );

protected:
    virtual Image    onFilter(const Image& ) = 0;
};

// Specific filter implementations
class SharpenFilter {
protected:
    virtual Image    onFilter(const Image& );
};

class SoftenFilter {
protected:
    virtual Image    onFilter(const Image& );
};


...

Image original, filtered;
SoftenFilter softener;
SharpenFilter sharpener;

filtered = softener << original;

Note that this implementation of a filter is not perfect, because the following line will not compile:

filtered = softener << sharpener << original;

The reason for the compiler error this line will produce is the fact that there is not << operator that takes an additional filter as an argument. This problem can be solved by introducing an additional class representing a filter chain, like follows:

class Filter {
public:
    inline Image       operator<<(const Image& );
    inline FilterChain operator<<(Filter& );

protected:
    virtual Image    onFilter(const Image& ) = 0;
};

// The filter chain
class FilterChain : public Filter {
public:
    inline Image       operator<<(const Image& );
    inline FilterChain& operator<<(Filter& );

protected:
    virtual Image    onFilter(const Image& ) = 0;
};

The correct implementation is omitted here; it should work like this: A FilterChain object has to hold a list of all filters that have been added by the << operator and as soon as an Image object is fed to the chain, all filters have to be executed in the correct order. To give you an idea, lets analyse the following statement:

SomeFilter filter1, filter2, filter3;
Image filtered, original;
filtered = filter1 << filter2 << filter3 << original;

First filter1 << filter2 is executed and results in the creation of a temporary FilterChain object that should create a list containing references to filter1 and filter2. After that << filter3 is executed on the FilterChain object, this method should add filter3 as the first filter to apply. Finally << original is executed by the FilterChain object which should call all filters in the correct order.


Rule 63. Split up a complex problem into many simple problems.

If you want to solve a complex problem, it is always a good idea to split the problem into many small subproblems which often have an easy and well-known solution. The advantage of this approach is obvious: you reduce a complex problem to well-known smaller problems. This in turn is much easier to implement and to maintain, and moreover you raise the chances that you can reuse the code for the solution of the smaller problems.

This insight has a direct implication on the design of the classes of a program. You also should prefer many simple and small classes over big and complex classes. They are easier to implement and to maintain, and if written correctly, you can probably reuse these classes in different contexts, especially if you have implemented them as templates.

Complex behaviour then can be achieved by combining many small and simple objects into one big class by embedding them. Although the memory footprint of this class may be big, the class itself is not complex (in the sense of having many and big methods) even though it exposes complex behaviour.


Rule 64. Always try to keep dependencies small.

A project with many dependencies between classes is hard to understand and to maintain. Too many dependencies also prevent reuse of single classes or modules, and lead to ''all-or-nothing'' decisions. They also increase the general complexity of the project and thus decrease its usability by software developers. The next rule is a guidance how to avoid too many dependencies.


Rule 65. Strictly separate different modules.

It is a very good idea to identify and separate different modules of your program at an early stage. In a second phase you should define the only dependencies between the modules that are allowed, and this is best done with a layered design.

Consider for example a module containing input and output routines, classes for file access and file system traversals. Most programs do have some corresponding classes. It does not make any sense to make this module dependant on some higher layer like file loaders for specific file formats. But the other way round these loader classes will of course depend on the I/O module. This example should illustrate the idea of layers - the I/O module is part of a lower layer while the loaders are part of a higher layer. As a rule of the thumb a lower lower never should depend on a higher one. And within one layer you should try to eliminate all dependencies between the modules; a program module is always built on top of other modules, and not on the same level as its own dependencies.

Such a separation of different modules simplifies the reuse of code for completely different projects; to continue the example above the input and output routines could easily integrated in a completely different program. This saves valuable development time and helps to improve the overall quality of all products, as every bug that is fixed in one module will help all programs that use its classes and functions.


Recommendation 66. Consider abstract solutions by using templates.

In many cases when you implement an algorithm it is possible to build a generalised version using templates. Of course this only makes sense for real algorithms, like sorting, abstract containers etc. and it does make little or no sense at all for concrete objects specific to a certain usage.

Considering a template version of an algorithm not only gives you a generic solution which may be applied to other cases as well, it also helps you to transfer a given and concrete problem into a more general setting and it lets focus you on the algorithmic and abstract core of the problem. Often this approach will even produce better and cleaner solutions because a generic pattern is extracted and separated from a concrete task.

Kaya Memisoglu 2005-01-06