I'm really pleased to announce the publication of my latest Pluralsight course, "Refactoring to SOLID in C# 14". It aims to provide C# developers with practical techniques and strategies to tackle the unique challenges of working in legacy codebases, such as dealing with technical debt, modernizing outdated dependencies, fixing code smells, and improving test coverage.
Legacy Code
Most professional software developers will spend a significant proportion of their careers working on "legacy codebases". By "legacy", I simply mean that the codebase is several years old, has had many different developers working on it, and continues to be actively maintained.
Legacy code isn't necessarily bad, but it's not uncommon for problems to gradually accumulate over time, making the codebase progressively harder to work with as time goes on.
Code Smells
Anyone who has worked on a legacy codebase will be all too familiar with the concept of "code smells", made popular by Martin Fowler. It's common to spend hours or even days navigating through various files, trying to understand how something works and wondering why on earth it was implemented this way. And while you maybe can't put your finger on exactly what's wrong with the code - it's clear that in its current state it's difficult to maintain, and difficult to understand.
Test Coverage
Of course, identifying problems in legacy code is easy enough, but fixing them is risky. And the main reason for this is a lack of confidence that the test coverage we have is sufficient. It's often the time required to thoroughly test our changes that proves the main blocker to addressing code smells.
One approach that's worth considering is Michael Feathers' concept of "Characterization Tests". These are tests designed to capture the current behaviour of the system, rather than the "correct" behaviour. The advantage of these tests is that they can alert you to any regressions introduced by refactoring.
Of course you can ask AI to generate test coverage for you, although it often doesn't have a great grasp of what the "correct" behaviour is - it simply infers what's supposed to happen from what the code already does. So almost by definition, the tests an AI will generate on your behalf are characterization tests.
One pitfall to be aware of with characterization tests though is that you can inadvertently "lock in" undesirable behaviour, as future developers (or agents) assume that the tests are protecting some important functionality.
Refactoring Strategies
Refactoring is safest when done in small, incremental steps, testing your work as you go along. Tools like Visual Studio include some built-in refactorings such as renaming variables, extracting classes etc, and these should be used wherever possible as they are deterministic.
If you are making widespread changes, it's worth familiarizing yourself with techniques such as "branch by abstraction", and the "Strangler fig" pattern, which are both designed to help you gradually replace legacy components without having to change everything in one go.
There's a very real danger though with any large refactoring initiative that it will stall mid-way through. This can result in an even more convoluted and confusing architecture. So be careful of starting what you can't finish.
SOLID Principles
In my new course I spend a couple of modules exploring how refactoring code to adhere to the SOLID principles can help a lot with software maintainability, testability and extensibility. The five "SOLID" principles have proved themselves to be very helpful guidelines over the years, but they aren't necessarily the whole picture.
It's also worth exploring complementary ideas such as DRY, YAGNI, KISS, Clean Architecture, CUPID, and STABLE.
A key benefit that all of these various "principles" provide, is that they give us lenses through which to evaluate our code, and vocabulary to talk about the problems we encounter. They help us move past the vague "code smell" sense that something is not quite right, to being able to articulate what the problem is and formulate a plan to remediate it.
App Modernization
If you have a large codebase that's more than about five years old, then it's highly likely that it's in need of some modernization. New versions of tools, frameworks and dependencies are constantly coming out, and the programming language itself moves on with new features. Unless you are very disciplined, it's easy to get left behind, and while most tech upgrades are relatively straightforward, every now and then you'll find the migration is non-trivial and you get stuck for some reason.
The further behind you get, the harder it becomes to upgrade and before you know it you find yourself in a situation where the libraries you depend on have critical security vulnerabilities but are no longer being maintained. You may even find that your hosting platform no longer supports running the framework you're using.
App modernization is an area that AI agents can be particularly helpful with, especially if you give them access to the official migration guides. That's essentially what the GitHub Copilot modernization agent is. If you've not tried asking an AI agent to help you modernize an app, it's something that's definitely worth experimenting with - you might be surprised at the results.
Summary
Legacy codebases can seem daunting to work on, but with the right tools and techniques at your disposal it can be a very rewarding experience to slowly and steadily improve a legacy codebase. If you have access to Pluralsight's excellent library of training courses, then do consider checking out my new Refactoring to SOLID in C# course in which I go into a lot more detail about all of these topics. And you don't need to wait until your codebase is a mess to start learning about these topics - refactoring should be an ongoing part of day-to-day development, even on a brand new application.