In this post, I’d like to talk about some of the problems you may face, offer some suggestions, and share my experiences with refactoring code. Refactoring, the process of optimizing code, or as I tend to say, going through everything, breaking it apart, cleaning it up, moving it around, redesigning it, and ultimately cleaning it up…all while keeping the functionality the same and trying your best not to introduce new bugs (easy, right?!). This can greatly improve the code quality, readability, and ultimately lead to an easier-to-manage codebase. This sounds great in theory, but actually finding the time to do it, and do it correctly, can be extremely challenging.
I’ve always felt that refactoring is a pretty broad term, and depending upon who you ask, there could be a lot of different opinions on how to properly act on this. How far do you go? While you could start at just adding comments and changing variable names to improve readability, you could literally spend days restructuring something to be better. There are always ways to make something better, and as a software developer, you’re constantly caught in a balancing act of “get it done to meet your deadline” and “this code is awful, let’s make it better”. I’m rarely satisfied with what I write, and less so with what I review, but it’s important to understand the grim reality of the industry – delivering the best possible results in the shortest possible amount of time.
Tip # 1 – Get it Working!
I’ve seen a lot of developers that spend far too long trying to make something perfect. Whether it’s analysis paralysis or just never being satisfied with the code you write (constantly changing, premature optimization, second-guessing yourself, and thinking too far ahead for something you’re not going to need), these sorts of roadblocks can prevent you from getting your work done. On the flip side, when you make the lowest possible effort to get things operational, you may end up getting done earlier, but in the long term, the potential bugs you’ve introduced, the lack of interoperability with other areas of the domain, and the missing flexibility can cost more time and effort later in the project when new features come out.
I’ve been guilty of all of the above more times than I can recall; staring at whiteboards for hours as equally as looking at some hastily-written code and wondering what I was thinking. I found that as I became a more experienced developer, I made better decisions about how far to take something. No surprise there, right? Unfortunately, there is no magic pill you can swallow to become proficient at this overnight. It’s going to simply be a result of you working in the industry a long time. Sure, some people get it faster than others (and some never get it), but by and large you’ll find that the more time spent involved in projects, the better you’ll be at risk assessment. This is so crucial as the little warning bells will go off in your head that a business analyst doesn’t fully understand something, or alludes to some changing requirements that will give you the cue you need to make something more flexible. Contrasting that, there are times where you’ll just “feel” something will be so firm that you can keep it relatively simple and it won’t change. This is a skill you need to work on, and actively do a lot of thinking about as you work from project to project.
Steps to success:
- Before you begin development, do as much risk assessment as you can. Ask lots of questions. I make dozens of decisions (as educated as possible based on asking domain questions from the stakeholders, project managers, and business analysts) for how far I should take some code, and then I get it working. This will help you in planning out how flexible to write something and how much time you feel you can allocate to refactoring later. Make your analysts and managers your friends. They can be helpful, contrary to popular opinion!
- Spend some time (at least an hour or so) thinking about what you’re trying to do. I use a whiteboard, Gliffy, Visio, pieces of paper. Anything and everything. Now look through the code – are there existing services you can leverage? Has someone solved your problem in a similar way? When I do code review, there have been times where two developers, on independent stories, have created similar services that could have been combined. I would then flag this for some degree of a merge in another sprint (as a refactor, naturally).
- Begin your development. During this time, a lot of big ideas may start to come to you about how to optimize things. The more experienced of a developer/architect you are, the more this is going to become your anathema in the face of progress. I would suggest you try your best to write these thoughts down but not act on them right away. I often find that towards the end of the sprint some of the ideas I initially thought turned out to be bad ones, or just weren’t needed right away or ever. I also observe that when I change things drastically, I run the risk of introducing more bugs.
- Now that you’ve gotten your work done, it’s time to do a time check. How many days (or, uh-oh, was it minutes?) do you have left? At this point I put my code in a stabilized state and easily available. There are lots of ways you can do it, whether you stash, commit, check-in, or anything else depending upon the version control system you’re using. The reason for this is I want to be able to start refactoring if I have time, and then if I don’t finish the refactor by the time of a demo / sprint ending, I can put those changes aside, go back to what I had (or if I already checked-in or pushed, then I can just stash/shelf what I currently have and deal with it later).
So the important takeaway here is to get your work done and make sure it’s fully functional. After that happens, you can go back and start to improve upon it. You should also plan for doing this in iterations; if you see an opportunity to break up the refactor into three stages, or know that some future sprint work will include coming back to this area, then that’s even better for you, as you can find a sliver of time later to dedicate towards another round of improvement. You need to be a bit resourceful in this approach.
What am I Refactoring?
Now this is a hard question to answer, because my recommendations are typically based on some code context (i.e. I’m sitting down with you reviewing your work). In the absence of that, I’ll provide some general recommendations that have worked well for me in the past.
Tip # 2 – Improve Readability
Even if you’re the only person on the team, it’s important you write readable code. When you come back to your work, or another developer is trying to figure out what you did, the code should be as clear as possible. The problem is, deadlines (or lack of experience) can get in the way, and you need to work on your mental discipline to take the time to write clean code. There are many books on the subject (including a book by that very title!) to offer some advice in this respect. For me, my readability and commenting style tends to be something along these lines:
- I generally put some sort of description at the top of each class / file explaining the purpose of it. If it’s a controller, I’ll focus more on the purpose of it but in a more disconnected way from what the front end is intending to do with it (so I treat it as an API that can be consumed by anyone).
- All methods/functions are commented, and I put more information for parameters if it’s warranted (or less if it’s stupidly obvious).
- I group together blocks of code in terms of what they’re doing. So if the method contains 6 lines of code, and the first 3 lines deal with performing an update to a user, and the next 3 deal with leveraging a service to create a notification in the database, then I would split them apart into two blocks and put comments above both.
- I’ll go into whatever content system the team is using (Confluence, a Wiki, etc) and do a writeup of the purpose of the code (typically each file is small enough that I group the entire concept together and give a high level overview about how my code works). These “developer notes” end up being very helpful later, especially if you have a provision to connect your documentation with sprint and story numbers.
Tip # 3 – Leverage Services to Consolidate
The word “service” has become so prevalent in software development, and carries all sorts of meanings depending upon the architectural paradigm you’re following. I recall doing a lot of research on just what a ‘domain service’ meant and was surprised at the amount of conjecture on sites like Stack Overflow and Reddit and what is considered “Good Form”. It was during that time that I was becoming more interested in Domain Driven Design (DDD) that I found this site on domain services, which helped immensely. Rather than go into the specifics, let’s take a step back and talk about the purpose of a service. Let’s say you have a controller in an MVC-type project, and the action is set to update an user. So this controller might contain code that does the work. But now you have another controller that needs to do something similar. Instead of just littering all our controllers with this kind of user code, why don’t we move all user-related code to a specific class whose responsibility is to interact with a user? This may seem beyond logical to some, but in a lot of my code review, I’ve encountered countless times where a controller has had excessive complexity (50+ lines of code, let’s say) doing a variety of things. This sort of separation is important for the following reasons:
- You now have a service you can call, so if you ever do similar/duplicate work, you can invoke these calls throughout your program, rather than write duplicate code.
- If something changes, you only need to go to one place to make the change, rather than doing mass replaces.
- You could have this service then follow a code contract (interface) and take advantage of this more generic approach, improving code reuse and swapping out services for testing and other purposes.
When to Refactor?
While I’ve worked under many different project management scenarios, my more recent projects have always been in some form of an Agile Scrum setting. In a typical sprint of two weeks, finding the time to do any significant refactoring can be tough. My guidelines are that I allocate a decent chunk of time thinking about my stories (and the stories of the team) after working hours. I’m always brainstorming and testing out proof of concepts, even on the weekends, just to be as prepared as I can. A lot of this extracurricular time allows me to have a solid plan to start from. That start rarely becomes the final approach, but it’s at least better than nothing. Allocating time for a hardening sprint near an upcoming release can be helpful, however in my experience you usually end up playing “catch up” to get things done that were put off and when that’s done you don’t have much time left.
Tip # 4 – Know your timeline
First, make sure you think the story can even be completed within the timeline. If it can’t get that far, there’s not going to be time to refactor. Also, be careful on your estimate. Developers are almost always off by some margin. When they tell me x, I always do x * 30%. For me personally, a while back, the source of my low estimates was definitely a pride issue. “There’s no way that would take me longer than 2 days”. Look, we’re all smart people, but if you can’t deliver what you’re promising, then you’re going to be out. You can be the smartest person in the room, but if you’re not delivering, the client isn’t going to care.
To a client, people who get things done > people who don’t.
So if the functional requirements are going to be met, the next thing is to plan out how you’ll refactor. There are going to be plenty of times where you can tackle the lion’s share of the work in one go, but for complex stories, it’s likely that you’re going to want to push some things off for later. It’s this sort of stuff, coupled with realizations about what can be made generic, that will provide you with a list of what you’d like to refactor. Like I said earlier, as soon as your story appears complete, and you’ve done some testing (or even got it to your testers to begin their work finding bugs) start thinking about these items. In addition to the areas I want to redo, this is where I also start going back over my work and to see if anything appears messy. If you’re not sure, maybe you can ask another developer or the tech lead. Is there anything hardcoded, such as strings or variable values? Also, have you created any redundant code or polluted your controller (if you have one) with logic that could be broken out and moved to a service? I’m frequently trying to strike the right balance between flexibility / being generic with how likely it is that this area of code will change. Once you have a grasp on that, you will find it easier to make a decision if this part of your code is a good candidate for change.
Balancing your Deliverables
To date, I barely can recall a single time where I got all of my refactoring done in one sprint. It’s because of this that you need to understand the balance between the next round of work, bug fixes, and finding the time for your next wave of refactors. I had a developer who became so busy, once, that it took us 3 months of releases until we got time for her to move some of her repetitive logic into another class file. I consider this an absolute win, more because we actually had the perseverance to go back and find the code, discuss it again, and do what we promised we would.
When am I Done?
In some respects, I don’t think you’re ever done, but you need to accept the reality that as the project continues, you’ll have more things to do. More stories, more bugs, and more to refactor. So get it the best you can, and move on. Some people just take it too far, and are never satisfied. Don’t be one of those developers. Chances are, there is a lot more you can do to improve, but you don’t want to chance running out of time.