“I’m implementing a user story and I see a way to improve/refactor the existing code while I am at it. Should I do that?”
“I’ve completed implementing a user story and want to clean up the code I’ve written. Should I do that?”
“I have an idea for a better way to solve this problem. Should I implement it?”
“This piece of legacy code is complex and painful. I want to rewrite it. Should I do that?”
Do any of the above questions sound familiar? I’ve heard them on many projects and on many teams. The questions reflect both a desire to “make things better”, as well as an uncertainty as to the boundary of “what’s ok to change”. I wish there was a simple answer. Sometimes there is, but often there is not. How do we decide if, when and how to implement improvements?
Uncertainty and Risk vs. Benefit
One of the underlying challenges posed by the opening questions is that there is a risk/benefit calculation that takes place. There are various risks and benefits, they can be hard to determine, and the answers depend very much on circumstances. Questions include:
What’s the risk of making this change?
- In the case of code that is well protected by automated unit and integration tests, the risks of change can be quite low. Legacy code (code with poor or no automated test coverage) is much riskier to change because unintended defects and side effects can go unnoticed. Higher risk changes may need to be judiciously scheduled (e.g. maybe not take them on right before a release). Working legacy code can unfortunately contain a treasure trove of lessons learned, bugs fixed, and other valuable experience; refactoring may preserve these, but rewriting may inadvertently lose important information.
- A refactoring effort against legacy code can and should consider adding those layers of protection. Good refactoring tool support (For C#, I’m a big fan of ReSharper) can also mitigate risks by maintaining functionality even through a series of refactoring steps.
- Risk mitigation strategies need to be considered. Complex refactoring is best done in small steps, adding tests along the way, and with pair programming (or I assume mob programming) to help detect errors and opportunities.
What’s the benefit of making the change?
- Agile processes promote sustainable development. The kinds of code improvements we are talking about are generally about making the code easier to enhance and maintain in the future, which helps teams maintain productivity over the long term.
- As an advocate for Clean Code, I want to see new code left in a state where downstream developers (which includes me, after I’ve worked on something else and then come back later!) don’t have to puzzle over misleading names, mixed abstraction levels, and other code smells. Clean Code makes it easier to not introduce new bugs when making subsequent changes, and eases troubleshooting and maintenance by making the authors’ mental models more explicit.
- In some cases, we have ugly code that simply doesn’t require much maintenance because it is in a low change area of the system. In such cases, this code might not be the highest priority thing to work on. On the other hand, classes and subsystems that change frequently or in which many bug fixes must be made are calling out for clean refactoring.
How much effort is involved? Do we have the time right now? Can we afford to not do it?
- For a team with good technical practices such as TDD and BDD, cleanup after implementation is simply part of the work: red (write a failing test), (implement until) green, refactor in the green (clean up with tests to tell you if you broke something). This is part of getting a story to “done”.
- A team that is following the Boy Scout Rule includes some amount of surrounding and supporting cleanup simply as part of story implementation.
- For larger cleanups and refactorings, where the cleanup effort becomes significant with respect to story size, there seem to be several possible approaches. It is possible to schedule chores to capture this work. It is also possible to look at a user story and ask “What cleanup will support the implementation of this story? Can we include that in the story estimate?” These conversations require a degree of trust between the development team and the Product Owner; the PO is responsible for balancing stakeholder concerns where both the product and the team (i.e. the ability to develop sustainably, with quality) are also stakeholders.
- The top two approaches (good technical practice, Boy Scout Rule) are preferred, because the team’s sustainable pace simply becomes part of the velocity: This is what it takes to add new functionality in a way that preserves quality and sustainable pace, without introducing technical debt or creating a bow wave of additional work.
Forces for Stability – Help or Hindrance?
Teams and organizations have their own personalities, and one way this shows up is in the bias for stability vs. tolerance for risk. When a team (perhaps encouraged by the environment and/or strong voices on the team) is risk-averse, important refactoring and innovations (e.g. incorporating a new test or build framework) may get stalled. Sometimes this helps a team achieve shorter-term goals, however a continued pattern of this kind imposes a long term productivity tax. This tax has several sources: not-so-clean code has more bugs, takes more team time to test, troubleshoot and fix, and takes longer to modify for new features; deferred innovations and enhancements rob the team of ongoing productivity boosters; and team members can have an ongoing sense of frustration at not being allowed to make things better.
Permission vs. Forgiveness – Is There Another Way?
A team that is good at the Scrum discipline of focus can achieve great things. When that focus is used on an ongoing basis to defer refactoring, enhancement and innovation, it can contribute to the challenges. Sometimes a team member may sneak in some of that extra work… going with “sometimes it is easier to ask for forgiveness than permission”. (On one project, I introduced SpecFlow so that I could take a BDD approach to a feature I was working on. Over time, other team members took on the capability and approach.)
The question that is presently in my mind is: what are other ways we can encourage these kinds of improvements? Here are a few paths that might be worthy of experiments…
Clear “Definition of Done” expectations for stories
- When “Definition of Done” makes explicit the expectation that “code is cleaned up” (“refactor in the green” + Boy Scout Rule), there is no question that this is part of the work of a story. This helps create momentum for supporting sustainable pace and limiting the creation of technical debt.
- On one project team, we specifically established a “story points” budget for chore items. This meant that we were explicitly allocating time to cleanup, improvement, and innovation, in the form of (in this case) 30% of the sprint’s target story points. The team, with the PO, prioritized the chores. We monitored the actual percentage over time and adjusted sprint-to-sprint to achieve an overall average.
- This was a useful approach in the context of having explicit support to allocate time to improvement, and the amount of time allocated was fully transparent.
- In Reinventing Organizations, one practice described by Frederick Laloux is “the advice process”… individuals are empowered to take action, and the one rule is that they must “ask for advice” from anyone who is affected. (They are not required to take the advice.) What if a developer can initiate changes but must follow the advice process first?
- In Turn the Ship Around, L. David Marquet describes a process of enabling responsibility. One practice described is, instead of asking for permission, is stating “I intend to <describe action>” and then proceeding, allowing others (in this case, the chain of command) to abort or question the action but allowing it to proceed otherwise. What if a developer posts in the shared Slack channel “I intend to refactor the XYZ class. Anyone have concerns I should know about? Anyone want to pair with me?” and then proceeds?
Gather data
- Sometimes improvements are deferred because the ongoing cost of not-so-clean code is not clearly visible. What if a team had a way to track (even roughly), the ongoing cost? e.g. for each story “I estimate that class XYZ cost me an extra 3 hours of this story”. Even a rough tally might reveal both an overall hours cost and a priority of hot points (e.g. “we are spending 20 hours per week working around class XYZ: this is a good candidate for cleaning up soon”).
- One practice I have seen is for teams to take on enough points in a sprint that they typically get done with a day of slack, and then use that day to take on chores and improvement experiments.
- A number of organizations stage a periodic “hackathon”. These events can be a good opportunity to innovate or introduce new capabilities.
- Practices that allocate time to tinker encourage the evolution of development practice.
- A good question to ask is often “what’s the smallest step we could take that would be valuable”? In the case of improvement refactoring, this could be as simple as introducing interfaces and adding tests, even without making significant structural changes.
“Do I make that improvement now?” At the end of the day, it’s a balance. We strive to satisfy the customer by delivering valuable software, and balance that with maintaining the ability to hold a sustainable pace and adapt to future change. Good technical practices and supporting Definition of Done can get us part of the way there, but we also need to build in a means to take on the larger improvements.
Originally published August 16th, 2018 on the Innovative Software Engineering blog. Republished with permission.