Refactoring Legacy Projects Using ASP.NET: Challenges and Solutions
Shahrzad Mamourian
Seasoned Full Stack developer | Proficient in Asp.Net, C#, SQL Server, Angular | +6 years of Software Development Experience
Abstract:
As a developer, you will almost certainly encounter legacy code at some point. When this goes on for too long and the developers cannot see an end in sight, it may be disheartening and make them dislike building software. Nevertheless, from what I have seen, the exit strategy appears very much the same in most situations [3]. In addition, you will find out what bloaters are and how to refactor bloated C# code by learning about the many anti-patterns that make up this class of code smells [2]. I will also show you how to construct ASP.NET Core Web APIs that require little work by introducing you to the wonders of super-DRY development and sharing some of the tips and tactics I have learned over the years. In fact, we solely employ industry-standard best practices that are based on generalizable solutions like DRY code. To begin, however, a little of theory [1]. ?Give me a chance to demonstrate.
Introduction:
Working with legacy code is something that no one enjoys doing since it is often difficult and, at best, time-consuming. What about the massive expenses and consequences of continuing to use legacy codes in their current form? [4]
Technical debt is inextricably linked to legacy code because it is the price paid for maximizing release frequency and time-to-market at the expense of producing code that is both high-quality and long-lasting but will eventually need to be updated. At least 90% of businesses, according to a study by Hitachi Consulting, were impeded by the use of legacy systems [4].?
Undeniably, the weight of legacy code is increasing significantly for companies. Based on the results of a survey conducted by the consortium for IT software quality, it is estimated that US businesses lost over $500 billion due to legacy systems in 2018 [4].
The thing that really matters is not whether we can quantify the technical debts and expenses involved with legacy code for specific firms, but rather what can be done to prevent them [4].
No issue can be solved instantly, however, legacy code refactoring is a powerful tool for improving the performance of older software systems [4].
One approach to fixing complex, tangled, or otherwise unwieldy legacy code is via a process called refactoring. The issue is that most organizations have a skewed understanding of refactoring, particularly the process of refactoring legacy code [4].
A Definition of Refactoring Legacy Code [4]:
Software refactoring, often known as "code refactoring," is a corrective method that comprises rewriting and reorganizing preexisting software applications in order to render them simpler, more maintainable, and more effective without compromising the performance of the original code [4].
The eventual aim of performing refactoring in a legacy code is to improve the code and boost its performance but not modify its operations. The final release is more comprehensible, manageable, and updatable once a refactoring process has been carried out [4].
Crucially, refactoring can only be applied if you have a firm grasp of the code's constraints, goals, and intended functionality. After that, individual sections of code are tested and revised as necessary. You cannot execute a thorough rework without first unit testing the different modules and components, therefore testing and refactoring are not mutually incompatible [4].
It is a common error for businesses and developers to use or run legacy code without first creating tests for it. Developers should run tests on the code after refactoring to make sure it is bug-free [4].
Putting legacy code into production without first refactoring or fixing it is asking for trouble, as stated at the outset [4].
A well-known example is the 2017 security breach at Equifax, in which hackers gained access to the personal data of over 150 million individuals via the company's database [4].
Professionals agree that this was one instance of a cyber breach that was never intended to occur. According to allegations by the US Government Accountability Office, the breach occurred mainly due to the use of legacy code on the Equifax server. Almost a year later, fixing the repercussions of the incident would cost the corporation over $1 billion [4].
Dirty codes, code rot, broken codes, or just outdated code are the main causes of problems with legacy code. Moreover, refactoring is not the only approach that may be used to fix this problem. While rewriting the code is the only option some experts would propose, some think that refactoring is unnecessary [4].
This discussion of rewriting vs refactoring will not be addressed. Both are undoubtedly routes to the same destination, although in different ways. Yet, it is essential to understand when refactoring and rewriting are most useful, as well as the advantages of each paradigm [4].
Advantages of Rewriting [4]:
1.??????There would be cutting-edge functionality in this revised application.
2.??????A fresh design and feel for the software would result from a rewrite.
3.??????Constant iteration.
4.??????An opportunity to fix past errors [4].
Disadvantages of Rewriting [4]:
1.??????The time needed for rewriting is usually more.
2.??????Spending more money is only one example of how much more time and effort this would need.
3.??????There is a chance that certain features from before will not be available.
4.??????It might need to acquire new skills or even a new language [4].
Advantages of Refactoring [4]:
1.??????Having the code more structured makes it easier to read and comprehend.
2.??????The program's functionality is preserved while its operation and performance are improved by careful refactoring of legacy codes.
3.??????It aids in the discovery of bugs
4.??????This is a time- and money-saver.
5.??????Scaling and maintaining the software becomes less of a hassle [4].
Disadvantages of Refactoring [4]:
1.??????You may affect the code's efficiency and functionality in some way.
2.??????More time than you anticipated might be required.
3.??????You might end up making things more difficult in the code instead of easier [4].
The code should be changed as little as possible until further notice. Please wait until we have completed our testing before making any adjustments. The purpose of our tests is to ensure that we haven't broken anything by making these modifications. Adding tests might be simple, difficult, or almost impossible, depending on the code you are dealing with. Coupling between components makes it difficult to test code independently. In this situation, you can do a few different things. One of them is the test pyramid [3].
This indicates that you need a large number of unit tests, a moderate number of integration tests, and a small number of UI or end-to-end tests. The reason for this is because UI tests and end-to-end tests are more prone to failure and need more effort to keep up-to-date. They are also more difficult to carry out [3].
In contrast, unit tests are brief, clear, and straightforward to modify and often execute quickly. I have been through extensive debates about the nature of unit tests and how they contrast with integration tests. Yet the truth is that we do not know. The test pyramid is best described in this fashion [3].
The higher you get on the pyramid, the more parts you are checking in one go. To that end, you should prioritize unit tests above integration tests. Even fewer should be used to test more than 10 parts of the program, and even fewer yet should be used to test the whole application [3].
Theoretically, yes. My experience has taught me that things are not always what they seem to be while working with legacy code. I have found that beginning with end-to-end tests is a good way to introduce the application to testing. The team and I began refactoring more, which made it possible to create more unit tests, while also increasing the number of end-to-end tests. Instead of starting at the top and working our way down, we reversed the order of the pyramid [3].
领英推荐
How can we define bloaters? [2]
Excessive growth of the code base without good reason is a kind of code smell. This is often due to the evolution of preexisting codes. As we continue to add more features, we see that classes are becoming less coherent. Keep in mind that the fewer lines of code there are, the simpler they will be to update and add to in the future. But, this does not give us a license to forego readability in favor of the shortest feasible code [2].
We can differentiate the following bloaters [2]:
A.????Obsession with the primitive
B.?????A large class size
C.?????Long procedure
D.????A lengthy list of parameters
E.?????Data clumps [2].
A. Obsession with the primitive [2]:
Using basic types instead of custom-created kinds is the essence of this code smell. The term "primitive obsession" may be used to describe either a single variable or a set of variables that need to be standardized. It causes problems with discovering duplication or with the code's readability and structure [2].
Improving performance by refactoring huge classes [2]:
A class that has become too huge and is doing too numerous tasks is a common source of code smells. This may complicate the class's readability, maintainability, and testability. It may also make it more difficult to reapply to the class in other contexts [2].
B. A big class often exhibits the following characteristics [2]:
1.??????There is a very extensive list of instance variables in the class.
2.??????There are a large number of methods in the class, and many of them are complicated and involved.
3.??????The class is accountable for a variety of activities and bears significant burdens.
4.??????The class is attempting to accomplish too much at once, making it hard to follow.
Refactoring techniques like "extract class" and "extract subclass" may help repair a huge class [2].
C. Refactoring lengthy procedures [2]:
This code smell is quite blatant and straightforward to correct. Having to read, change, and manage lengthy procedures is a nuisance. The method ought to be not over 10 lines in length to ensure it is seen without having to scroll. Just the specified tasks in the method's name should be included in its body. As a corollary, a shorter function name is more convenient to name [2].
D. Refactoring a lengthy list of parameters [2]:
As soon as we see a method with a big number of parameters, a warning signal should go out. No more than three or four parameters should be used. The only time this is not the case is when doing so would cause unintended class dependencies to disappear. Turning a blind eye is the best course of action in this kind of situation [2].
To what extent can we improve on the present code? [2]
It is important to examine the parameters first. When sending several values of the same class, it is more efficient to provide the full object. In such a case, maybe we can design a logical class in which we account for all the parameters. Introducing new classes may result in unwelcome project expansion, but it is usually worth it if we can put those classes to use in more than one location. Our efforts to make the code more understandable and lessen the number of redundant lines of code are greatly aided by this [2].
There is always the method's body and the steps leading up to it to examine. Maybe we can reorganize things in such a way that not all parameters need to be given [2].
E. Data clumps refactoring [2]:
Data clumps relate to situations in which the same group of data is repeatedly sent together in different parts of the code. As it is not evident why or how the data is being utilized, it might be difficult to understand and maintain the code [2].
What can be done to correct the situation? [2]
The extract class refactoring technique may be used once more. The goal, as discussed in [2] regarding the Person class, is to create a new class to enclose the data and its associated operations.
?The code's readability and ease of maintenance are enhanced by commenting regarding what the data means and the manner in which it is used [2].
ASP.NET core Super-DRY development [1]:
The abbreviation "DRY" refers to a principle of good software architecture. Its literal translation is "Don't Repeat Yourself," and it is a fundamental idea for everyone who has ever worked on a source code project that has to deal with legacy code. In other words, if you make the same changes in code over and over again, fixing bugs and adding new features would become tedious [1].
The maintainability of your project will suffer, and it will be harder to implement improvements if you have too much repetition in your code. Also, the more similar statements there are, the more spaghetti-like the code will be. On the other side, if you can keep your projects from having too much repetition, you will be a more fulfilled and effective software developer and have a much simpler time maintaining and fixing bugs. The short version is that adhering to DRY principles may aid in the development of high-quality code [1].
You can take this crucial architectural idea to a whole new level if you adopt a DRY mindset, to the point where it seems as though your project is materializing out of thin air, with no work on your part required to bring about the desired effects. The processes of "super-DRY" might make it seem to the inexperienced that code appears out of thin air. Brilliant code, however, is even smaller than great code [1].
The bareness of the ASP.NET Core Web project is the initial thing you will realize when you begin to browse the code. You may do this by making use of the capability to dynamically incorporate controllers in ASP.NET Core. The Startup.cs file contains the code that makes this work. This essentially amounts to putting every controller across all the assemblies in your folder dynamically into your AppDomain. With this simple concept, you may create systems in a modular fashion, reusing controllers and increasing their efficiency. To become a super-DRY practitioner, you must master the art of controller reuse between projects [1].
Because of our skillful use of object-oriented programming (OOP), generics, and DRY principles, we have evolved into super-DRY magicians. The obvious follow-up question, therefore, is how you may put this strategy to work to optimize your code [1].
To get started, create a new project within your model folder, or add a new class to the current magic.todo.model project, that corresponds to the structure of your database. The contracts folder is where you may go to construct your service interface. Next, create a view model and controller, and then implement your service under the services folder. Connect your service's interface with its implementation in tight binding. Next, if you want to start fresh with a new project, you will need to include a reference to your assembly in the magic.backend project so that ASP.NET Core knows to load it. The only two projects your back end must refer to are the service project and the controller [1].
The final bit is superfluous if you are just going to utilize the already-established projects. You may develop a complete ASP.NET Core Web API solution with only one line of code that binds your service implementation to your service interface. I would say that is a really powerful piece of code [1].
Conclusion:
Regular updates are necessary for both legacy software and effective applications. Increased performance, simpler advancement, ongoing development, and a better user experience all benefit from updating legacy systems, but neglecting to do so leaves them vulnerable to security breaches [4]. Many strategies have been presented for refactoring bloated C# code as well as how to detect and resolve bloats. Understanding these approaches will drastically impact the overall excellence of our code. These methods have the potential to not only resolve code smells but also make the code itself more readable and less problematic to update [2]. To be told, it is not easy to steer clear of reiteration. One must be prepared to refactor, refactor, and refactor some more. When you think you are finished refactoring, perform some more refactoring. Yet, the benefits far outweigh the drawbacks. By adhering to DRY guidelines, you may compose modules out of preexisting elements and generate code with a wave of your scaffolding wand, making it seem as if you were writing code by magic [1].
References:
[1] https://learn.microsoft.com/en-us/archive/msdn-magazine/2019/june/patterns-and-practices-super-dry-development-for-asp-net-core
[2] https://code-maze.com/csharp-refactoring-bloated-code/
[3] https://www.dotnetcurry.com/patterns-practices/tackling-legacy-code-tips
[4] https://dzone.com/articles/legacy-code-refactoring-tips-steps-and-best-practices