Refactoring Code to Keep it Evergreen

by Ervin Turai
Ervin is a software engineer at Tecknoworks. Some of his specialties include C#, VB.NET, WPF and SQL Server.

Throughout the development phase of a software product, there comes a time when the source code needs maintaining. Think of it like a nice living fence that needs trimming every other week, to make sure it has just the right shape and aesthetics. That’s right—code can, and should be aesthetic, easy to read and comprehend. The process through which one achieves this is called refactoring. In a nutshell, refactoring code means the code is rearranged, rewritten, regrouped and reorganised in such a way that it fits the required specifications.

The need to for refactoring code might arise because of the following reasons:

  • The developer comes to realize that the original way of thinking may not have been the best.
  • A more suitable abstraction idea, or a better design pattern came to the mind of the programmer.
  • The customer changed their mind and wants to modify a few things.
  • A new feature comes along that relies on several existing ones.
  • A business rule needs extending in such a way that it covers a newly discovered edge case.
  • The development team consists of a dispersed group of programmers, from different cultures, having different habits or experience, resulting in an unwanted mixture of code styles.

And the list of reasons can go on. As it can be seen, pretty much all paths in the development cycle lead to the need for refactoring code. The extent of the action can, of course, vary.

Back in my days as a junior developer, I used to believe that thinking well ahead and writing good code from the very beginning exempts me from refactoring. While a good design of the architecture and backbone of the solution is a positive and desirable approach, it cannot prevent refactoring.

There are two main reasons I believe refactoring code cannot be prevented:

  • Internal: no matter how well one elaborates and thinks ahead, for medium-long projects, the planning horizon is simply not large enough. In other words, you cannot possibly think of every case from the beginning.
  • External: nearly no client is fully decided from the beginning on what they want. It is almost certain that, as the project starts working and things are shaping up, new ideas will come or existing ones will be modified. Alas, you are faced with changes that almost always lead to refactoring.

However, refactoring code has its disadvantages too. The risk of breaking things is the most imminent, and it usually happens. When a piece of code is designed to cover one or more features and the code is refactored, chances are that the developer overlooks one or two, or simply doesn’t understand why the code does that one, apparently useless thing. Then naturally, the features may start failing.

Not refactoring the code and instead just adding more and more lines to accommodate all changes and bugs, inevitably leads to technical debt. As all debts, if not paid, it’ll come chasing you. Needless to say, the later in the development cycle refactoring happens, the more risky, difficult, error prone and lengthy it is. So one good bit of advice is: don’t be lazy. Polish the code, especially if working in a team, and do not let code smells stay in there.

At the end of the day, the benefits of keeping the code clean greatly outweigh the risks. One simple yet very strong positive outcome is when thinking about the maintainability of the code. Either when new members join the dev team, for post-release maintenance, or in an unforeseen next phase of the project, having easily readable code is a huge advantage. Even if the original team is re-assigned to the project as little as 6 months later, they will have forgotten the subtle nuances and edge cases their code caters for. Then, having readability is their main advantage.

My top 3 tricks I recommend you to follow when refactoring code:

1. Keep it DRY or make sure no code is repeated.

It is quite amazing in how many places repeated code can be found and in how many shapes it can be.

Imagine the simplest example: let us have a team of several developers, working on a project. Two or more of them work on features that need displaying dates in a certain format. Depending on the order in which they’ll encounter the need to display the dates, they will start implementing in various ways: through .ToString() and a constant, through .ToString() and an inline string pattern, through an extension method or through a helper class. And the examples can continue.

The programmers may communicate and learn that there is a “standard” way in the application to display dates, or they may be thorough enough to start searching for a similar case and go by the approach they find. But odds are not always on the team’s side and they may end up in several implementations of the same problem. When it comes to refactoring, these things need to be consistent. And even that does not guarantee that, when new team members come along and need to display dates, they won’t reinvent the wheel and come up with their own way of doing it.

Example:

1. Dim targetPdf = String.Format(“{0}/{1}_{2}.pdf”, Server.MapPath(“~/Content/reports”), PostedReportName, timeTag)
2. Try
3. If IO.File.Exists(targetPdf) Then IO.File.Delete(targetPdf)
4. Catch ex As Exception
5. targetPdf = String.Format(“{0}/{1}_{2}_{3}.pdf”, Server.MapPath(“~/Content/reports”), PostedReportName, Guid.NewGuid().ToString(), timeTag)
6. End Try
7. repDoc.ExportToDisk(ExportFormatType.PortableDocFormat, targetPdf)
8. repDoc.Close()
9. repDoc.Dispose()
10.reportPdfViewer.Src = String.Format(“{0}/{1}_{2}.pdf”, “../content/reports”, PostedReportName, timeTag)

Basically, what it says is this: we want to export a report into the ~/Content/reports folder and then bind it to a report viewer’s source. For this, in line 1 a filename is set up. Line 3 makes sure that another file with the same name does not exist in the target location by attempting to delete it. However, if deletion fails, line 5 makes sure that the file name is chosen in such a way that it is certainly unique. But then look at line 10: it recomposes the file name from its various pieces, thus breaking DRY. What if line 5 actually got executed? We would bind a different file to the report viewer than the one we just created. And it’d be a problematic file, because line 3 could not delete it. And it’s all such an innocent looking, one line repetition. Imagine what effects a longer repetition could have!

2. The Boy Scout principle

As the scouts that keep the forest neat and clean as they stride through, the programmer should also leave the place in which he worked neater and cleaner than it was before.

The occasions when you’ll need to work on the code of others will be numerous and you might find that, besides implementing the change at hand, you can enhance or beautify the code too. For example:

  • Give better names to methods or variables.
  • Simplify that very long IF statement that doesn’t even fit inside a visible line.
  • Remove that bunch of commented code that stood there since ages and only bothers the eye.
  • Indent the code, such that the next person that comes along can read it well.
  • Split a very long method into several smaller and more significant ones.
  • Increase the test coverage on the newly obtained methods, or on the overall class.
  • Decrease the cyclomatic complexity of methods, when you see all those nested IFs, FORs, WHILEs, etc.
  • Get rid of the spaghetti code: if you took the time and went through the hassle of understanding the ‘pasta’, don’t let others go through the same; rather, apply the well-known, standard solutions, and leave behind code that can actually be maintained.

This is one of the most useful principles that can be applied when refactoring. It can be done in small, unrisky runs, let the new code settle in, let your teammates find and explore your solution. A simple example:

The code is full of statements like: if (element != null) list. Add(element);

It is not too pleasant to read all those repeated IF statements. Instead, why not create an extension method to your List <T> class, calling it AddIfNotNull, and just call that. It will even vertically arrange the text, rendering the inserted objects to start at the same position, thus making it easier to visually scan, top-to-bottom.

3. Mitigating Risks – Use unit tests or TDD.

The easiest way to cater for the risk of breaking things are the unit tests. TDD is the most important safety belt one can have against crashing features. During refactoring, the unit test can be run and they will immediately signal which feature needs more attention. When extending the team, unit tests are especially helpful—they cover features at a high level, endowing them with the power to run the tests and explore all paths in the code that the execution takes. Having this possibility is a time-saver for the existing team, because they won’t have to explain the solution in meetings. Rather, they’ll just focus on the really important key decisions, or mechanisms that are in place.

Final Tips

Another way to reduce the risks of regression is code reviews. The more eyes looking at the code, the greater the coverage will be. Moreover, pair programming can be a useful technique while refactoring, not just because two brains will yield greater coverage, but also because thinking aloud helps each of the programmers individually.

Do you have questions on best practices for refactoring code? Give us a call, and we’ll be happy to help!