Flexibility of program code is resistance to its changes. This means that adding new or changing old functionality in a flexible system requires less effort than in an inflexible one.

The features of flexible code

In a broad sense, flexibility is achieved by satisfying two conditions: low cohesion and high meshing of the system’s modules. A module can be either a single class or an entire library of classes.

Low cohesion means weak dependency of modules on each other. Ideally, it should be possible to use each module independently, rather than drag the entire system along. In practice there are always and will always be dependencies, but reducing their number simultaneously reduces the complexity of the system and promotes reuse of the code base.

High meshing indicates the homogeneity of the abstraction used to design the module. Such a module is designed to solve a single specific task, as evidenced by its interface. For details, see the article on the single responsibility principle.

Tips for writing agile code

Now let’s look at some specific tips to help you create agile code. In fact, they all follow one simple thought: “don’t smear what you can keep in one place around the code.” Indeed, the less code you have to touch to make a change, the more flexible it is, and the less likely you are to make a mistake. After all, if you have to edit five files to make one change, you can forget about one of them (consider yourself lucky if the compiler notices it, but that’s not always the case). I recommend to pay additional attention to the article about the DRY (Dont’ Repeat Yourself) principle.

So, let’s go!

Tip 1. Wherever you can, use a base class, not its specific descendants

If it becomes necessary to switch to a different implementation (keeping the old one), using the base class will suffice to change the initialization point. Breaking the tip means that you will have to rework every reference to the class (don’t forget about input and return function parameters, which can add a lot of work).

As an example, refer to the article where we discussed the use of polymorphism in C++.

Tip 2. In C/C++, use typedef

Do not use primitive types directly if it is realistic to find some meaningful name. Your choice may eventually change when there is already a lot of code with an explicit type indication. For example, you may decide that int is fine as a user ID type. But at some point you realize that it was better to use long or unsigned int. And from that moment the difficulties begin. Most likely, the autoconversion will not help, because the int type is ubiquitous. The only way out is semi-manual processing of the whole application code. To complicate the situation especially, such problems are often detected at the late stages of development (or even operation!) of the system.

But you can always define an alias: typedef int UserID. Now you can change the used type in a single line for the whole application. Simple and fast.

Tip 3. Do not use “magic numbers”.

If the numbers define any static properties of the application, make a corresponding constant and apply it. Even if you think it will never change (for example, the number pi, which, however, is in most standard libraries). First, it will simplify your task if the constant does change. Unexpectedly, this happens a lot! For example, for the same pi number the required precision may change. And secondly, the code will become clearer. Compare yourself which expression is simpler: 3.14 * radius * radius or PI * radius * radius? On the other hand, there is no need to create a constant for two in the 2 * Pi * radius circumference formula. It only makes sense in this context.

There is another class of constants. They do not define any fundamental constants, like the number pi, but make sense only as a means of parameterizing the application. An example is the font size or the background color of the main window. More flexibility can be achieved if, instead of a constant in the code, you use a variable initialized by the default value of that constant. As a result, the behavior of the application doesn’t depend rigidly on this parameter. It becomes possible to use different values within one and the same program, what cannot be achieved in case of the constant. This approach allows you to go even further and read these parameters from configuration files, achieving a new level of flexibility. But be careful. Anyone can change the contents of a configuration file, so a lot of checks will need to be added to keep it working when the values are known to be wrong.

Tip 4. Don’t neglect encapsulation

The keyword private was invented for a reason (although it does not exist in Python, for example). Hide the inner workings of module implementations. Neglecting this rule greatly reduces the flexibility of the program. If the clients of a module start to make use of its internal features, dependency arises (increasing cohesion). Any dependency is an obstacle to making changes. You cannot just change the code on which its users directly depend. So the less the client of a module knows about it, the better.

In fact, encapsulation works everywhere. The rule is simple: give variables the lowest possible scope. If you can, let the variable be visible only within a function, otherwise define it as a private field of a class. Even the protected access level creates a dangerous dependency (especially in Java) even though it can ALWAYS be avoided. Public-access variables and global variables are against the principles (of course there are structures, but more on that later).

The implication of this advice: avoid assumptions when using any module, even (especially) if you are the developer. A function returns an ordered set of values today and not tomorrow (when it is not an explicit postcondition). If your code depends on this side effect, everything will break.

Tip 5. Plan function signatures carefully

The function signatures form the interface of the module. An interface is a contract between a module and its clients. It acts as a point of contact. You cannot just change the interface of a module without affecting the existing users (although expansion is usually allowed). Therefore, you need to approach planning of such an important element with special responsibility.

Not much can change in the function signature: the name, the input parameters and the return value (usually the return value type is not included in the signature, but that’s not important). There is not much to think of with the function name. If it turns out to be unsuccessful, the only way out is to create another function that calls the old one. Note that many popular libraries use this technique (the old name can be marked as obsolete and not recommended to be used deprecated).

As for the input parameters, it all depends on the situation. If you are lucky, you may be able to painlessly add a new parameter to the very end by specifying its default value. In other cases, overloading may help. Keep in mind that a large number of input function parameters (more than three) is a bad sign. If you notice this at the design stage, consider converting some of these parameters to the fields of the class to which the function belongs. You can also try creating a new class that includes related parameters. And if the parameters are loosely related to each other, this may indicate that the function has more than one purpose, which is contrary to the principle of single responsibility.

When working with a return value, tips 1 and 2 will help.

Tip 6. Avoid conscious duplication

Code duplication is evil. But when it happens consciously, it is a clear sign of design problems. Most often it does not happen within a single class, within which one can easily create a corresponding function. Deliberate duplication usually occurs when the same code (or very similar) is needed in different modules. As a rule, this module doesn’t fit into any of the existing interfaces, so you can’t take it somewhere without destroying the homogeneity of the abstraction. Conclusion: we need a new module.

Many developers don’t bother to design such an auxiliary module. Hence all sorts of Utils, Helpers and Tools. They are a dump of functionality, which is kind of common and nobody’s at the same time. Such modules can consist of just static functions, slipping into the procedural world. Of course, this is better than explicit duplication, but abandoning OOP is not the right solution either. The solution is to think carefully about the actual purpose of an auxiliary function. You can always build a specialized class around it. It may be very simple at first, but you may need to extend it in the future.

Tip 7. Prefer delegation to inheritance

Inheritance is a static link between classes at compile time. Any static relationship reduces the flexibility of the system. The problem is that if a subclass implements a certain behavior, it cannot be changed at runtime (or it requires the use of non-trivial syntactic structures of the programming language used). An alternative is to create classes whose behavior is formed by combining simpler components.

A typical example is that you already have a class from which you only want to borrow some properties when implementing your own class. This is often a deliberately bad idea. For example, it’s not uncommon to find a class like PersonList that inherits List. There are very many reasons why this is a bad idea. Let’s not even go into details, but remember that in such situations, it is better either to explicitly include an existing class as a field of your class or use private inheritance in C++.

As another example, let’s recall a test application we created when discussing threads in Qt. It was important for us to be able to choose the algorithm we wanted to use to build the fractal. We could implement a base class for the application that included a virtual function to draw the fractal. Then for different algorithms we would just have to inherit this base class, providing the appropriate implementations of the virtual function. Everything would have worked, but we wouldn’t have been able to easily switch from one algorithm to another while the program was running.

What was actually done: We turned the fractal algorithm into an independent abstract class. The main part of the application used an instance of that class as its field (don’t forget Tip 1), delegating to it the task of building the fractal. This allowed us to easily perform algorithm swapping on the fly. Static inheritance won’t give you that opportunity.

Yes, we couldn’t completely avoid inheritance. After all, we need polymorphism. But we added an additional level of indirectness. Any indirectness increases flexibility because it creates an extra clutch point which can be swapped out at any time (like a Lego constructor). The more coupling points, the greater the flexibility. But note that this has led to increased cohesion of the module. Now if we want to reuse any of its level in another project, we have to drag along with it and the layers below (or use their own stubs, which does not simplify the task).

It should be noted that the considered example of fractal plotting algorithm is based on the Strategy pattern. But more about that below.

Tip 8. Use design patterns

If someone has already solved your problem, why solve it again? Patterns are solutions to typical design problems. They are very powerful and easy to use tools for creating agile architecture.

Tip 9. Consider using plugins

Plugins are modules that can be dynamically plugged in while your application is running. The main advantage of using them is extensibility. In a well-designed plugin system, anyone can add their own functionality (even without having the source code for the system).

A plugin-enabled application uses a highly flexible model of operation: there is a kernel that provides minimal functionality and serves as the skeleton of the system, and plugins act as auxiliary modules with which the kernel interacts. At the same time, any of the plugins can easily be disabled or plugged in without changing the source codes of the kernel.

Examples of well-designed programs with plugin support include eclipse, QtCreator, vim, VisualStudio, Chromium, Firefox and many others.

Tip 10: A little of each is good

Now tone down your enthusiasm a bit. Flexibility is great, but as already mentioned, it is also an additional indirectness. Indirectness always increases complexity. It is impossible in principle to follow the listed tips everywhere (there are no infinite abstractions). The main purpose of these tips is to move the actual solution away from the central part (the core) of the application and thus make it easier to make changes.

On the one hand, there is nothing wrong with this, but the problem lies in the fact that high flexibility is a lot of abstractions, that is, commonalities. The general is always more complicated than the particular. In addition, these commonalities are also many, and in programming there is a very simple rule: the more code, the more places in it for errors. Try to use as many modules as possible, but not less.

It is much easier and faster to create an inflexible program than a flexible one, but it is almost impossible to expand it. You need to compromise. But the sense of balance is not developed immediately. It takes a lot of practice. Work and analyze your mistakes. Then, over time, your systems will be more and more reliable and flexible.

Leave a Reply

Your email address will not be published. Required fields are marked *