Chapter 17

Architectural Ideas

The first support section is quite short. It has only three rules, with a few sentences of content for each one. Their focus is programming-language agnostic, and they remind me of the philosophical chapter.

A.1

Separate stable code from less stable code

Here is the sentence from the C++ Core Guidelines: “Isolating less stable code facilitates its unit testing, interface improvement, refactoring, and eventual deprecation.” What does that mean?

Putting an interface between stable and less stable code is a way to separate it. Due to the interface, your less stable code becomes a kind of subsystem, which you can test or refactor in isolation. You can now test not only the subsystem but also the integration of the subsystem into the system. The first kind of test is typically called the subsystem test, and the second is called the subsystem integration test. The subsystem has two channels into the system: the functional and the nonfunctional channels. Both have to be tested. The functional channel provides the functionality of the subsystem, and the nonfunctional channel propagates the exceptions that can happen and to which the system may react. Thanks to the interface, the concrete subsystem is an implementation of the interface and can, therefore, quite quickly be replaced by another, maybe more stable, implementation.

A.2

Express potentially reusable parts as a library

This is a straightforward idea. Immediately, a few questions arise:

  1. When is a part of software potentially reusable?

  2. When do the costs of implementing the library pay off?

  3. What is the right kind of abstraction?

The three questions are quite blurry and are, therefore, difficult to answer in the general case. This is particularly true for the last question. Let me explain it.

First of all, don’t put too much effort up front into your code to make it reusable as a library because “you aren’t gonna need it” (YAGNI), but write your code so that it could be reusable. This means follow simple guidelines such as writing your code for understandability, maintainability, testability, and other quality attributes. It is highly probable that you or another programmer will have to work with your code in the future. Or to say it with the words of Philip Wadler: “Make your code readable. Pretend the next person who looks at your code is a psychopath, and he knows where you live.”

The second principle that comes into play is “don’t repeat yourself” (DRY) when you need the same or similar functionality more than once. Now you should think about abstraction. When I have two similar functions, I write a third function that provides the implementation, and the similar functions just become wrappers for using the implementation function. Here are my ideas put into code to make my point.

std::vector<void*> myAlloc;
 
void* newImpl(std::size_t sz, char const* file, int line){ // (3)
    static int counter{};
    void* ptr = std::malloc(sz);
    std::cerr << file << ": " << line << " " << ptr << '\n';
    myAlloc.push_back(ptr);
    return ptr;
}
 
                                                       // (1)
void* operator new(std::size_t sz, char const* file, int line){ 

    return newImpl(sz, file, line);
}
 
                                                       // (2)
void* operator new[](std::size_t sz,char const* file, int line){
    return newImpl(sz, file, line);
}

The overloaded new operators in the simple form (1) and for arrays (2) invoke the common implementation in (3).

Third, I don’t want to answer question 3 because it is very subjective and may be affected by many factors. The answer may depend on the domain of the software. Does the software, for example, run on a desktop, embedded device, or high-frequency server? It depends on factors such as maintainability, testability, scalability, to name a few traits, but also on performance. It may depend on the skill level of the users. Maybe your library is an infrastructure library or a library for your customers.

Writing reusable software in the form of a library is about three to four times more effort than doing a one-off implementation. Here’s my rule of thumb: You should think about a library when you know you reuse the functionality. You should write a library only when you will reuse the functionality at least twice.

A.4

There should be no cycles among libraries

Cycles among libraries c1 and c2 make your software system more complicated. First, they make your libraries challenging to test and impossible to reuse independently. Second, your libraries become more difficult to understand, maintain, and extend. When you find such a dependency, you should break it. There are a few options, thanks to John Lakos (Large Scale C++ Software Design, p. 185):

  1. Repackage c1 and c2 so they are no longer mutually dependent.

  2. Physically combine c1 and c2 into a single component, c12.

  3. Think of c1 and c2 as if they were a single component, c12.