📜 ⬆️ ⬇️

Code reuse - as it happens in practice

Talk about “reuse of code” is very popular among programmers - and mostly they talk about it in a positive way. We like to say that the designs we have designed are “universal” and “suitable for use in other projects”. Why this is considered a good thing is easy to understand - everyone wants to implement the next project twice as fast as the previous one by using existing work.

But when it comes to this in practice, most often something goes wrong. There is one very clever idea about this: “Do not try to make the code reusable until you see at least three different places where you can apply it”. I consider this advice very good - I have seen quite a few situations where it helped (or would help) avoid obsession with trying to write reusable code where the problem could be solved for one particular case “here and now”.

This shows us the flaws in the theory that reuse is always a desirable and noble goal.

Why not reuse?


It is easy to argue the writing of a reusable code: if we write and debug the code once, and get benefit from it in several places, this will immediately increase the business value of our product / product, right?
')
Yes and no. Premature code compilation is a very real problem (as well as premature optimization). People most often are not able to see the real potential of reusing pieces of code until practical tasks force them to write the same code (or very similar variations of it) several times. On the other hand, sometimes programmers in their fantasies build so abstract castles in the air that they do not solve their original task, what to speak of repeated use.

How can you not remember the fashionable cultural phenomenon now called "Patterns". Patterns were originally purely descriptive. Programmers found here and there some general ideas, approaches - and gave them names. Having accumulated a fair amount of such named entities, people suddenly put the cart before the horse. It turns out that now the programming patterns have become mandatory, and each of them must be implemented strictly in accordance with clearly defined rules. If you build some system of type X, and there are already three such systems on the market that use pattern Y, then it should be in your implementation.

We need some kind of balance. Obviously, the idea of ​​total copy-paste is flawed. But the idea that all code should be written with the idea of ​​its potential reuse is also flawed.

There is another interesting factor. Most of the time when developing software, we will not reuse the code we have already written (even if it was written as reusable), but we write something new. This is not logical and an understanding of why this happens is very important so that we can stop rewriting the same things over and over again, in new languages ​​and paradigms, but without adding something conceptual new.

Why we do not reuse the code?


Here is a practical example from life. I want to design a callback processing system in the game's video engine. But I already have several similar systems designed by other developers of my company in the process of working on previous similar projects. Most of them are built on the same principles: we have “sources of events”, there is a mechanism for subscribing to events, when an event occurs, you need to pull each signer and notify him about the event. Simply.

That's just the game Guild Wars 2 had in its source code about six different implementations of this simple architectural idea. Some were in the client, others were in the server, and still others used messages between the client and the server, but in general, they all did the same thing, and their implementation was also essentially the same.

This is a classic example of when it may seem a good idea to apply refactoring, unify the code and reduce the number of duplicate components. Here only Guild Wars 2 is a huge behemoth from several million lines of code and I definitely don’t want to be the one who takes and redoes one of the fundamental mechanisms in it.

Well, let's not redo existing code. He, after all, and so works. But let's think about the future. People will not stop playing games, which means programmers will not stop making games. So they will have engines, and these engines will need a good standardized library of callbacks, which everyone will love at first sight. Let's write it? And let's

We want to write open source so that other people can use it. On the one hand, it must be powerful enough for monsters like Guild Wars 2 to find everything necessary in it, but on the other hand, it should not include something purely specific to one game (or even a platform), since we we want to write a re-usable code.

But in practice, there is a whole bunch of reasons for not using a similar foreign (even if open) library. First of all, in such a library there will definitely not be any functionality you need and it will have to be added. Secondly, the dependencies of this library will be a huge obstacle.

Some of the dependencies are simple and obvious. The class Foo is inherited from the class Bar, which means they are dependent - this is understandable. But there are more interesting forms of dependencies. Suppose we still write and publish our library of callbacks. Somewhere inside it, the library will need to have a container for storing information about subscribers. Well, the very ones who will need to be notified of events. No matter how you look, whatever you think up, you need a container. How do we implement the container? Well, we're not in the stone age. Anyway, this is an article about reusing code. The obvious answer (outside the game dev world) will be to take a container from the standard C ++ library. This can be std :: vector , or std :: map , or both.

In games for some reason, the use of the standard library is often prohibited. I will not explain here why, read about it somewhere. Just accept as a fact that sometimes you cannot choose the libraries used in the project.

So, we have several options. I can implement my library with a dependency on the standard C ++ library, which immediately makes it useless for half of the potential users. They will have to rewrite the code of my library to get rid of everything that is not available on their platform. The volumes of potentially rewritten code will be such that it will be embarrassing to talk about some kind of “reuse” of my library code.

The second option is to implement the container yourself, inside the library. In fact, simple containers like a linked list or vector are not so difficult to write. But this is even worse from the point of view of reuse of the code - such containers are in the standard library, they probably are in the libraries of those users who want to use our library. And here we add another set of container types! What kind of code reuse is here - we, on the contrary, produced extra entities above the roof.

Contract Programming


The idea of ​​contract programming is not new at all, but it is not so often used in practice. So let's start with a simple dependency in the form of the container described above:

class ThingWhatDoesCoolStuff { std::vector<int> Stuff; }; 

This code obviously makes the ThingWhatDoesCoolStuff class dependent on std :: vector , which is not convenient for those who cannot use std :: vector from the standard library. Let's make the code a little friendlier to them:

 template <typename ContainerType> class ThingWhatDoesCoolStuff { ContainerType Stuff; }; //     : ThingWhatDoesCoolStuff<std::vector<int>> Thing; 

It became better, although the customers had to write a rather long and strange type name (which, of course, can be visually simplified using typedef or using).

In addition, everything will break as soon as we start using the container in the code:

 template <typename ContainerType> class ThingWhatDoesCoolStuff { public: void AddStuff (int stuff) { Stuff.push_back(stuff); } private: ContainerType Stuff; }; 

Access to the container requires the push_back method to add items. All, of course, will be fine, as long as our container is a standard vector. And if not? If in the container type that the user gives us, the method for adding an element will be called Add ? We will get a compilation error. And the user will have to either rewrite the code of our library for compatibility with his container (for the time being, reuse of the code), or rewrite his container and the code using it (no one will ever go for it).

But, as they say, any problem can be solved by adding a sufficient number of layers and indirection! Let's do that:

 //      template <typename Policy> class ThingWhatDoesCoolStuff { private: //   ,    typedef typename Policy::template ContainerType<int> Container; //        Container Stuff; public: void AddStuff (int stuff) { using Adapter = Policy::ContainerAdapter<int>; Adapter::PushBack(&Stuff, stuff); } }; //         : struct MyPolicy { //        template <typename T> using ContainerType = std::vector<T>; template <typename T> struct ContainerAdapter { static inline void PushBack (MyPolicy::ContainerType * container, T && element) { //          container->push_back(element); } }; }; 

Let's see how it will work. First, we define the Template template class, which allows us to separate business logic from its dependencies (such as containers). Any code that claims to be reused must be distinctly separated from its dependencies. The above-described method with a template is not the only implementation, but one of the good ones.

The syntax in the above implementation really doesn’t shine with brevity. All we want to say with this code is: “Hey, I need a container and here is the container API, which I understand, give me a suitable implementation and I will do my job.”

The template here is used to avoid the overhead of calling virtual functions. Theoretically, I could just make the base class “Container”, define virtual methods in it, blah blah blah, God, I hate myself just for trying to think about such a terrible option. Let's just forget about it forever.

What is good about this code is that I can use it without changes, both in projects with the standard C ++ library, and without it. By publishing my kolbek system only once can I save users from having to edit its code depending on their platform, environment, and other restrictions.

There are also disadvantages to think about: everyone who wants to use my library will be forced to think about its dependencies and writing suitable adapters for the same containers. But this needs to be done only once (very few people switch to their project from one set of container types to something completely different).

For other entities (those that are more complicated than containers), writing adapters can be more difficult. But reuse of such code is only necessary very carefully, ideally as described above - only after you have written several similar components and understand well which parts of them can be distinguished into a general abstraction, and what should remain a specific implementation of each of them.

Conclusion


Finally, you can look at the performance of the given example. In debug builds, it may limp, but release builds by using templates will get well-optimized efficient code. With runtime performance everything will be fine. And what about the build time? Templates increase compile time. But in our example, a template will only be instantiated by a certain type once, which roughly compares the compile time with a version without a template. However, with the multiple use of templates, it is easy to arrive at a situation of a catastrophic increase in compile time — you need to follow this. And even so, I consider this approach a better option than defining a heap of related abstract interfaces.

That's all that I wanted to talk about this example of decomposition. Hope this was helpful.

And remember: before allocating something to a component of a reusable code, you must come to the need to use it in at least three different places .

Source: https://habr.com/ru/post/343404/


All Articles