What should be done in the first turn: reengineering or migration?
Today there are a lot of projects that were written or the work on which started during the period of the Delphi 5/6/7 popularity or even earlier. Many of them have been already migrated to newer versions of the programming language. But some of them are still being developed in old IDEs. Usually, they use a huge range of external components. Some of them have been already abandoned as a modern version of the programming language supports the analogical “out-of-the-box” capacities. Some others are still being actively developing.
But here we have a question on the necessity to conduct app migration for ensuring modern capacities like cross-platforming or 64-bit app versions. And a huge number of external components can have a negative impact on the time and quality of migration.
What is migration?
If we take that migration is just code actualization for the requirements of the modern IDE, we can move to this stage without any doubt. But if it’s not enough for you just to update the language version, IDE and some components and you need to introduce some more serious changes to provide new capacities like cross-platforming or to fully update the existing UI, do not make a decision too quickly. In such a case, it’s worth considering such a migration stage as reengineering.
What is reengineering?
Very often, apps that were built in the early 2000s and that have been seriously expanded since then, do not have any particular architecture. Many of them were inspired by the desire to automate just one process. A developer just wrote an app without any serious preparations or vision. The logic was closely tied with the user interface.
Then there appeared a necessity to add some additional functionality. The same actions were repeated. Some code for similar functions was just copied from one module and pasted into another one. The app was growing. The maintenance of such apps is becoming more and more challenging.
Later, to change any similar functionality in different modules of the program, it was necessary to introduce changes to every unit. And if a developer didn’t have the possibility to break the vicious circle timely, it is required to re-process the existing code and conduct the re-engineering before migrating the app.
Engineering: what for?
- First of all, it will facilitate app support in the future.
- Secondly, it will facilitate migration.
Let’s consider an app from our practice as an example
The app was being developed for nearly 20 years and gradually it was being migrated to a newer Delphi version. But then a client decided to change the design. The task doesn’t seem to be too complicated, does it? We should have just taken the app and updated its design.
But within the process of app migration to a new GUI, the client decided to keep not the entire functionality but just a part of it as some features became outdated and some others had to be simplified. It means that there was a necessity not just to move the code to a newer GUI but to change the logic. And it still seems to be rather simple, doesn’t it? But here we face the first pitfalls. Some parts of the business logic were linked to the entire app. Deleting the obsolete logic became a little bit more difficult task.
But at the same time, the old app continued working and growing. It still had the same functionality. We had to track the changes in the old code and combine them with ours. Moreover, we couldn’t forget that a new app wouldn’t have some part of the logic and we didn’t have to migrate it. It was the first challenge.
The second issue was related to the fact that the legacy code had some errors. We had to correct them and provide the team behind the old app with our feedback and then move the code to our app again.
All this increased the complexity of the migration.
Another important thing was the time that was spent on migration. Due to requirement changes, it increased as well.
And of course, within this process of migration, it was impossible to avoid mistakes that were related to changes in functionality. And once again we needed additional time to correct them.
How could we avoid all these difficulties?
First of all, we should have worked with a legacy code and conducted its reengineering. The simplest thing that we could do was to separate the logic from the UI. It would have allowed us to work with a single code base for our old app and a new one but to have different designs.
Yes, in such a form, it won’t be fully correct to call it reengineering but we could have moved further. After separating logic we could have continued improving the existing code. Similar functionality that was contained in different forms of the app could have been combined in separate re-usable classes. The functionality spread across the app could have been united.
We could have not only avoided errors in a new app but also increased the code quality in the old one that is still in use. The introduction of new functionality in one of the apps would have allowed us to introduce the same functionality in the second app.
At the current moment, the functionality of the updated app has been realized, implemented and continues to be enriched with new features. The code r-engineering is being executed. And it already has very little in common with what we had initially. The old app is also being developed and used. But what if we need to introduce some functionality from the old app again? I think that it will definitely bring new challenges as both apps are following very different paths in their development.
There isn’t any universal solution for all problems. But in the case of dealing with legacy code, we’d like to recommend starting with analyzing the existing project and requirements. First of all, you should evaluate possible risks and requirement changes. And only after that, you can make a decision on how to work with the code.
In the considered example, the best decision should have been to conduct re-engineering before migrating the app.