
Abstract
Two distinctly different widely used meanings of the term code refactoring are identified and named:
- Changing how code works, without changing the requirements that it fulfills (refactoring in the weak sense)
- Changing how code is expressed, without changing how it works (refactoring in the strong sense)
(Useful pre-reading: About these papers)
The common understanding
The term refactoring is commonly understood within the software engineering discipline to have the meaning documented by Martin Fowler in his Definition Of Refactoring post:
Refactoring (noun): a change made to the internal structure of software to make it easier to understand and cheaper to modify without changing its observable behavior.
In the case of a software application, the observable behavior is in essence the set of requirements that it fulfills. (And also its look and feel: if you change the font, this is not refactoring.) In the case of a module, or a class, or an individual method, the observable behavior is the mapping of parameter values to results, contracts fulfilled, and side-effects, if any.
Let us call these things requirements.
Note that according to this widely used sense of refactoring, we are allowed to take any piece of code, throw it away, and replace it with an entirely different piece of code, and to call what we just did refactoring, as long as requirements are still being fulfilled as before.
The trick is, of course, how can we tell, or who is to say, that requirements are still being fulfilled. It should come as no surprise that in many cases things do not go as intended:
- A change in the code might have some subtle consequences that we were not aware of, so we might be thinking that requirements are still fulfilled, while they are not.
- Requirements are never entirely unambiguous, so they might be fulfilled according to our interpretation, but not according to someone else's interpretation.
- Sometimes a combination of the above two mishaps may occur.
For example, the requirement to "create an empty file" might initially be fulfilled by two lines of code that create a binary file and immediately close it; then, one day, someone might decide to refactor those two lines of code by replacing them with a single invocation to a create-file-from-string function, passing it the empty string. One line of code is better than two lines of code, right? What could possibly go wrong? Well, if by "empty file" the requirements meant a file with no text in it, this refactoring was probably okay; however, if by "empty file" the requirements actually meant a zero-length file, then this refactoring may not have been okay, because the create-file-from-string function might, unbeknownst to us, create a file that contains a UTF8 BOM. This is an example of both things going wrong: the requirements were vague, and the "refactoring" had subtle unintended consequences.
Hopefully we have enough tests in place to catch such violations of the requirements, but this does not always work either, because the tests usually verify someone's interpretation of the requirements, and they usually do so only partially, since you cannot anticipate and test for every possible scenario.
The mathematical understanding
There is another sense of refactoring that we are also familiar with: the sense used by Integrated Development Environments (IDEs) that perform useful transformations on code, such as renaming a variable, re-ordering the parameters of a function, etc.
The transformations performed by IDEs tend to adhere to the mathematical sense of refactoring: the code is transformed in such a way that the new code is equivalent to the old code, just as in mathematics the refactoring of x = 2y + 2z yields x = 2(y + z), which is equivalent, and potentially more useful.
Note that when we perform refactoring operations using an IDE, we usually feel no need to re-run the tests, because the new code works exactly as the old code, barring any bugs in the IDE, or any foolish hacks from our side, such as weak typing or binding by name.
Summing it up
The bottom line of all this is that the term refactoring is being widely used within the software engineering discipline to mean two distinctly different things. These two things are so different from each other as to warrant taking notice of this fact, and making the distinction explicit:
- The weak (common) sense of refactoring:
Transformations that (hopefully) result in no change in how requirements are fulfilled.
They are usually performed manually by the programmer, they may involve extensive changes in the way the code works, and they require thorough testing to guarantee that nothing was inadvertently broken.
- The strong (mathematical) sense of refactoring:
Transformations that result in code that is functionally equivalent to what it was before.
They are usually performed automatically by the IDE at the programmer's request, they tend to be limited or superficial, they tend to change how code is expressed but not how it works, and they typically do not need to be followed by a round of testing, because the code is typically guaranteed to behave the same way as before.
Also of interest is Martin Fowler's post on the Etymology of Refactoring.
No comments:
Post a Comment