My Adventures in Coding

March 24, 2009

Working With Legacy Code – Lessons Learned

Filed under: Legacy Code — Brian @ 10:06 pm
Tags:

Over the last five years I have had the opportunity to work on a large legacy code application. In doing so I have found a common set of principles to working safely in this type of environment (Often discovered through mistakes!). Books such as Working Effectively with Legacy Code by Michael Feathers are excellent and will give you the techniques to refactor safely, but I have found keeping this list and constantly thinking “Am I violating one of these rules?” has helped me work on legacy code with more safety by showing me early on when I was getting off track. I know there are many tips to working with legacy code, I have just found these to be the most important for me. Good luck and happy legacy coding!

My cheat sheet:
1) Go slowly
2) Make small changes
3) Do one thing at a time

Small Changes
When refactoring, follow the steps one at a time. Nomatter how tempted you are or how easy it may seem to make two changes at once, don’t. This is often where bugs are created and time is wasted debugging. Even if the interim solution is not ideal, don’t worry, this is not the end product. We are working at cleaning up technical debt one step at a time, not all at once.

Slow Down
You should always read code slower then you would read anything else. We are so used to reading at a certain pace that we try to do the same when working on code, which is far worse when it is code you are seeing for the first time. This is typically where business logic gets overlooked and broken. You may feel like this is moving slower, but it is actually saving you time later and reducing bugs.

Duplicate Code
If you have duplicate code, focus on moving it all to one place before making changes. I know it is tempting to want to clean it up at the same time but don’t. This is where bugs get introduced and problems are created that take days to fix before the product is releasable again. Don’t combine duplicate code cleanup with another refactoring, remember one step at a time.

Stay on Task by Taking Notes
While working on code when you encounter a smell, it may be tempting to try and address it while in the middle of your task (or in the middle of another refactoring). “Oh this is easy, I will just make this change too”. Avoid this type of behaviour. Instead, keep a list of smells and take a note each time you encounter one. Focus on finishing your task (e.g., Adding business value). Once your task is complete, go back and prioritize your list and deal with the smells in order. Work on the the most important smells if you have time. If you don’t, raise these problems to the team and perhaps create an issue to have them dealt with. The important point is to not go down a rabbit hole of refactoring and cause your deadline to slip, but without completely ignoring the technical debt issue. It is so tempting to want to cleanup every smell you encounter, but this can quickly get out of control.

Hide the mess
If you have a chunk of messy code that is not under test, first try and hide that mess from the rest of the application with a clean, high level, easy to use interface and point the rest of the application to use this interface. Once this interface is in place, write tests. Now you are in a position to cleanup the mess, without having to worry about extracting the mess from the rest of the application at the same time. Also if the mess is hidden away from the application, the rest of the application can be clean, and if know one ever needs to make changes to that messy code, perhaps your time can be put to better use.

Don’t be afraid to take a step back
One of the big advantages in going in small steps and committing often is that when you run into a situation where you make a change and now suddenly the application is broken and you have no idea why, you can take a step back. Don’t be afraid to revert your recent change and start over. This can sometimes be much faster than spending time debugging to figure out why the code is broken. Now if you tried to make a number of changes at the same time and are rarely commiting, this mistake could be expensive. When refactoring, it is fine to go off on a tangent to try out a refactoring, but if the mess starts to unravel into a much bigger, more time consuming refactoring, don’t be afraid to back up and try a different approach. The point is to not create your own road blocks, which sometimes developers do to themselves.

Understand the code before you change it
Any time you are making changes to legacy code, the first step should be reading and understanding the business logic of the code you will be modifying before you make any changes. Often this code is the only documentation available and should be treated with care. If you don’t understand the business logic, why that code is there in the first place, you may end up introducing new bugs into the application, re-introduce bugs that had previously been fixed, and as a result create expensive production issues.

Advertisements

March 7, 2008

Testing Legacy Code

Filed under: Legacy Code,SD West 2008 — Brian @ 8:39 pm
Tags: ,

While at SD West I had the chance to attend a talk by Elliotte Rusty Harold who has written numerous books such as Refactoring HTML just to give an example. This talk was a tutorial on writing tests for existing production legacy code. Elliotte has a very practical approach to dealing with adding tests to existing legacy code.

The benefits of TDD are clear and 100% code coverage is always the goal. However, with existing legacy code you need to take a much different approach. When optimizing, or refactoring existing legacy code you can apply TDD and it works very well, however you must give up a few things:

  1. 100% Code Coverage
  2. Unit Tests

Focus on Broad Tests

Since you are writing tests for a system that has already been running on production for quite some time, you can assume for the most part it is offering what the user’s want, or there would have already been complaints and issues logged (there will be bugs, but overall it is functioning). So don’t focus on 100% code coverage or unit tests, instead focus on writing broad tests that confirm some functionality that the user relies on is working correctly.

Yes, when you make changes and a broad test does fail, it will not immediately show you the cause of the failure, like a typical unit test will. However, it will still show you that you have broken existing functionality and this is a big improvement over having no tests at all. In a small amount of tests we have been able to at the very least protect some existing functionality. The key is that since we usually have a very limited amount of time to devote to writing tests for legacy code, that it is more important to get broad coverage than targeted coverage.

Just like when using TDD with new functionality, when making any change at all in legacy code, ensure the tests are written BEFORE changing anything, but still in this case broad tests are the place to start. This is not advocating ignoring full coverage for your new code, only the legacy code. If you have time to write full unit tests for existing legacy code, great, but most developers do not. The key is to make sure your broad tests are in place FIRST.

When dealing with legacy code, the fastest gains in code coverage and also the biggest gains in reducing bugs come from starting your tests at the highest level possible. In writing these tests you do not think about the structure of the code, but rather just “What does this application do” and write tests to confirm that functionality and protect it.

Add a New Test and It Fails

Now in TDD when writing new code, the moment a test fails you fix the code until your test passes. However, when writing tests for existing legacy code, if you add a new test to an area that is not currently covered by tests and it fails, but you are confident the test should work, do not stop and fix the code to get the test to pass, this is a common mistake. You are far better off to log an issue for the bug and to continue to write more tests. The reason is because at this stage you do not have enough tests in place to ensure your bug fix will not break another area of the application. So write more tests, leave this test failing, and log an issue for fixing this bug.

Basic Method to Testing Legacy Code

So to get started with writing tests for legacy code you:

  1. Look at the existing application
  2. See what functionality it offers to the user
  3. Write tests to cover that high level functionality

Top Down Approach

When writing tests for legacy code, you want to go in the order of what will give you the most gain for the least amount of effort, so go in the following order:

  • Tests for each Package
  • Tests for each Class
  • Tests for each Method (with legacy code, will probably never get this far)
  • Tests for each Line (100% code coverage)

Once again the Michael Feathers book Working Effectively with Legacy Code was highly recommended. When I read this book I enjoyed it a great deal. At the conference this book was recommended at most talks I attended and is highly regarded as a reference for refactoring and getting code into a testable state.

Create a free website or blog at WordPress.com.