Refactoring
What?
To change the code but keep the behaviour the same.
We can be certain behaviour is the same if we have test coverage of the behaviour
Test coverage can be confident over the unit to be refactored, when applying TDD (or its flavours outside in, or inside out)
Why?
Reduce techincal debt
Improve Non Functional aspects of the code ie maintainability, performance, extensibility etc
Code written may be improved upon despite it doing what it is meant to do
Reduce cost (time) to add features, reduce bug incidences, fix bugs, etc
Reduces code rot which makes the codebase hard to work with, and can lead to devs leaving
Does not mean complexity reduces completely, but can help
Longer an app is actively worked on the more features, bug fixes etc exist that increase complexity
Even non worked on apps can lead to complexity as it can be old, using old tech, no one knows the domain etc
When to do?
As part of TDD cycle
When technical debt is too big and affecting current development on code base
Refactor when changing parts for a current story, thus small refactors
When to avoid?
When time is tight, and the refactor is big and time consuming
To avoid getting to perfection, focus on big wins
When the app is beyond repair, and needs revamp
either due to new features or code rot
Types of refactoring groups
There are many refactoring methods (see Fowler) and these can be grouped (Fowler also groups them)
Groups - there can be overlap
Technical Refactoring
Code is changed to improve it's ability to be extended, modified, testable etc
Lead to applying common software principles (SOLID etc) and design patterns (Gang of four etc). Leading to more abstractness
This can be small, but can be quite big (time and changing lots of code)
Updating dependencies or adding security patches
Issues
These changes may be more maintainable/extendable but can end up being less readable or navigatable for the develop to do the maintaince or add a feature.
abstractness is always more difficult to follow rather than concretness
This can lead to an explosion of files/classes/methods etc. Although IDEs makes this easy to navigate, understanding what is happening can be harder as logic is all spread out. Needs to be a balance.
Cognitive Refactoring
Make code more readable
Reduces cognitive load
Helps new joiners understand the code faster
Helps current devs who have not worked on code base for sometime
This can be architecture (package structure), keep all the main business logic in one or as few classes for each flow, domain driven design, better naming etc
Technical refactorings can also lead code to be more readable
Issues
Keeping logic in one place
Can lead to a lot of duplication
Style Refactoring
Codebases are generally worked upon by several developers. They need a shared standard to make it easier to work on the code.
This allows the code to follow the conventions in the codebase if similar work has already be done
For new projects, allows for decision to be easily made in it;s creation
Promotes good practice, what should be used and not
Makes it easier for everyone to follow the code, even non team members if the guide is used across temas
Use some style guide documentation
Issues
Can styling documentation may change for new projects, so different projects can be confusing
Older projects may not have it applied, but attempting to change can only be done in small doses
Needs to be constantly enforced (pairing/reviews/static analysis tests) otherwise individual developers will use their own styling which leads to a mish mash of styles
Reverse Refactoring
Can be temporary to help the developer understand the code. The changes are reversed before committing
Inlining, reordering, remove language constructs or syntatic sugar (lambdas -> annoymous classes)
can bring in code to make more sense
Reduces cognitive load to improve comprehension
Performance Refactoring
This can be app specific and also platform specific
Improve the performance of the app
reducing calls over the network or bottlenecks (ie services which has limit of being called)
Caching, object/connection pooling, concurrency/parallism, batching,
reducing time or space complexity
Fully tested (code coverage) for reliability
platform level (where the app is deployed) to improve scalability, availability, fault tolerence etc. But this may require changes to app to be able to use the platform, if you want your app to run
Use of static analysis (find bugs, sonar cube or custom tests etc) use common patterns which detect insecure code, vulnerabilities, test coverage etc. This informs the developer where refactorings can happen
Issues
Adding performance can be pre emptive, which leads to a lot of work done for little improvement.
Should test this, and see if it meets requirements
Doing it everywhere can lead to reduced maintainability and readability, thus changes will take longer
should be done to the main culprits and encapsulated
Due to complexity, more comments and documentation will be needed, alot more complex testing too
Documentation Refactoring
Additons and/or changes to comments, documentation files, diagrams etc
When the above refactorings happens or bugs/new features documentation maybe out of date and need to be improved
Useful for new joiners
Issuse
Can go out of date very quickly
Can use html rendered acceptance test (yatpsec, cucumber etc) which turn tests into readable documents. Helps for business documentation, but not for technical side of the app
Time constraints leads to ignoring updating comments
Can be a duplication of effort
use of wrong comments (how rathe than why answered in comments)
Have code cognitively refactored should reduce the need for majority of comments, as code is the document
Adds too much noise
Time taken may not be worth it
ROI of refactoring
https://neilonsoftware.com/talks/the-roi-of-refactoring-lego-vs-play-doh/
Premature refactoring
Many articles about TDD emphasize the refactoring step - as in the green step is almost trivial/unimportant, and what really matters is the refactoring step. Developers misinterpret this as having to get to some over-engineered design at the start.
An example of a misconception: "if-else" isn't clean - so let's use polymorphism everywhere; we must remove all if-else statements during refactoring. The other reason (in the developer's mind) is an expectation of future requirements; the developer assumes certain requirements in the future and thus makes their CURRENT code MORE ABSTRACT to fit the predicted FUTURE needs.
The problem with premature refactoring:
The developer must now spend more time considering the more complex solution. This is actually wasted time.
The unnecessarily more complex solution means higher maintenance costs in the future. Again, wasted time.
When the future comes, and we get user feedback, it turns out that the real needs are different compared to what we had predicted now... So then the current abstraction has to be deleted (wasted effort) and implement the right abstraction.
So what's the solution? Deferred refactoring:
During the refactoring step, it's ok to do minor refactoring, such as variable renaming. But let's not get into the trap of trying to refactor the if-else into a polymorphic solution and trying to apply design patterns right now!
Since we have a simpler solution (the simplest solution that was enough to solve the problem), we don't have unnecessary maintenance overhead.
The future will come - when it comes. A month from now, it might turn out that the if-else statement is still working great. Three months pass, and the if-else statement is still working fine. After six months, due to some new requirements, and our better understanding of the business problem, we realize that we could replace the if-else statement with design pattern XYZ; at that point, "it feels right".
Last updated