What Every C++ Developer Needs to Know About Legacy Code

What does Legacy code mean?
The term was first used by the computer scientist George Olivetti to describe code maintained by an administrator that did not develop the code.

Other interpretations:

  • Legacy code is a source code that relates to a no-longer supported or manufactured operating system or other computer technology. By Wikipedia
  • Legacy code is referred to code that gets inherited by a team or a programmer from somewhere else (external or internal)
  • Code is legacy code as soon as it’s written
  • Legacy code is code without tests. By Michael Feathers

You may or may not agree with some of these interpretations, and that is ok. Michael’s Feathers definition is slightly different than the others. He explains it in his book:

“Code without tests is a bad code. It doesn’t matter how well written it is; how well structured it is, how well encapsulated it is. Without test there is no way to tell if your code is getting better or worse”.

Three approaches to working with legacy code:

  1. Most common approach- leave the old code alone and write more code that will become legacy code. This approach isn’t recommended because the code becomes harder to maintain in the future.
  2. Rewrite everything from scratch – This approach isn’t very practical in most cases.
  3. Approach the system pragmatically and slowly improve it – The best way to handle legacy code.

The rationale behind the most common approach (as stated in (1)) is usually as written below:

  • I don’t have time to refactor the code, I’d better make changes instead.
  • I am not sure about the benefit of refactoring legacy code.
  • Why would I risk changing the structure of the code when it has been working well for a long time so far.

Choosing the first approach is basically contributing to increasing Legacy code. We should not take shortcuts. In a programmer’s daily routine, shortcuts can lead to disasters especially with legacy code.

Uncle’s Bob sums it all up with “Boy scout Rule”:

“Leave the code cleaner than you found it! Whenever you touch an old code, you should clean it properly. Do not just apply a shortcut solution that will make the code more difficult to understand but instead treat it with care. It’s not enough to write code well, the code has to be kept clean over time.“

What Uncle Bob mean is that it is very important to leave a trace for the others to follow once you are done doing your part, especially if you did not finish changing and refactoring completely, you should make it more understandable hence, maintainable for the others to finish your part in the future.

What is the major dilemma when dealing with legacy code?
According to Michael Feather’s book, we need tests to change the code. On the contrary, we need to change the code to add tests.
The loop we are stuck in, according to Clare Macrae, is as the following:
We need to change the code -> There are no tests-> It was not designed for testing->It needs refactoring to add tests-> We can’t refactor without tests-> We are back to – we need to change the code.

So, how should one deal with it?
There are several approaches to it. However, one principle that is widely accepted is to use refactoring. How much refactoring and how is another question, that the answer for it is presented in Michael’s Feathers book “Working efficiently with legacy code”.

According to Michael’s Feathers approach, the algorithm should be:

  • Identify change points
  • Find test points
  • Break dependencies
  • Write tests
  • Make changes and refactor

It is vital to acknowledge that for most legacy code what the system does is more important than what it is supposed to do. By understanding this, it will be much easier for you to write the right unit tests for it and refactor the code when necessary.

After acknowledging that, make sure the system keeps on doing what it does! It might be easy to say and hard to implement, but it all starts from words.
Doing all of the above, we are ready to iterative improvements via refactoring.

Another useful tip is to start from the shortest branch to the deepest branch when writing the Unit Tests. We do not want to jump into deep water right away as it is better to start from the shallow in order to be ready: understand things better and in advance. Planning and understanding are key points!

Jim Carrey said, “My focus is to forget the pain of life. Forget the pain, mock the pain, reduce it, And laugh”.
He, of course, referred to mocking. Mocking is integral when working with legacy code.
Mock objects let you simulate real objects without running the real code in those objects. We will use mocking when we have dependencies like a database or network calls in our code and we have to test the logic of it somehow.

We can use Google mock within its framework Google test (and refactor when necessary). But what would we do in order to deal with:

  1. None Virtual Methods
  2. Classes that are hard to inherit from
  3. Singletons
  4. Static method calls
  5. Heavy classes

Some of the dependencies are hard/impossible to fake and requires a lot of refactoring, therefore, time-consuming. Not only that: It can also be dangerous sometimes. You can view it as taking several connections flights in order to get to your destination.

Why can refactoring be dangerous?
In short:

  • Lack of experience:
    If the code has been going around for a long time, technologies are more likely to change. If it’s an old technology, people might have a hard time understanding what it does (if at all), before even thinking of refactoring or it will be hard to find people that have expertise with that specific technology.
  • No tests:
    Without tests, refactoring can be described as the transformation of perfectly working production code into code that most likely will have bugs and will not work as expected. There is no automated way to verify that changing the code doesn’t break something.
  • It requires a lot of time and planning. The time available for refactoring is realistically limited and precious.

The good news is that the use of refactoring can be much reduced if you work with the right up-to-date tools.
Isolator++ allows you to fake concrete, virtual and no-virtual, static, singletons and heavy classes directly from its API in the safest way – without refactoring. In other words: Isolator ++ flies you directly to your destination 😊

We’ll examine the concept with a few samples using our Isolator++ API in order to show you how easy it can be.
Given these classes with the method headers:

With the following implementation:

We would like to assert a private method call without changing the code (or making the method public). This is how, for example, it is possible to do so with Isolator++:

Faking static method: we can fake all of the static methods using FAKE_STATICS API which causes the GetSingleton to always return a fake recursive pointer

Or even changing the behavior of a private overloaded method, using PRIVATE_WHEN_CALLED API:

We have a bunch of more examples available in our documentation.
These particular examples were taken from Isolator++ installation folder.
Please don’t hesitate to contact our support team for any question or assistance, at Support@typemock.com