How To Refactor A Public Interface… 🔊
View original- current
Summary (TL;DR)
Refactoring means changing code structure without changing observable behavior. Changing a public API interface is not refactoring it is a redesign because it breaks consumers. Internal interface changes that don't affect external callers can be refactoring. The definition depends on perspective: a change is refactoring only if it doesn't alter behavior for its specific consumers.
How To Refactor A Public Interface… 🔊
When the team struggles to reuse a public function, it’s very common to hear this:
This public API is too hard to reuse, let's refactor it.
Before getting into why that statement is wrong, let's understand what "refactoring" means.
According to Martin Fowler, the act of refactoring is:
[…] to restructure software by applying a series of refactorings without changing its observable behavior
— Martin Fowler on the "Definition Of Refactoring" written in 2004.
Now, what's "observable behavior"? Fowler also has a definition of what that means:
[…] it means that the software still does what it did before
Well… still not very useful.
Is changing the interface considered refactoring? Yes and no, it depends on the interface. If you have control of the callers (not a published interface), then it’s refactoring.
Ok, let’s look at some examples.
Imagine an existing public library to validate an Australian address in NodeJS. It receives an Object Literal as an argument named input that contains the property value of type String representing the address to validate:
After some time, you realize the API is confusing. You decide to change it to be called with a simple String as the argument which represents the address to validate.
This is the first step to change the public interface:
This is the second step to rename the input argument to address for the purpose of code clarity:
Here's the final running code:
The first step is NOT refactoring. The API is from a public library, which means changing the interface introduces an incompatible breaking change. It will change the observable behavior for consumers of the public library.
The second step IS refactoring. You're not changing any observable behavior, just renaming the argument.
Ok, let’s look at one more example from another perspective.
After the changes from before, you notice the internal functions are still using the old contract of accepting an Object Literal and a value property. Now you decide to change the interface for the internal functions.
This is the third step applied to 1 of the 2 internal functions:
Here’s the running code with the third step applied:
From the point of view of the validate Australian Address function, the interface change for has Suburb is NOT refactoring. After all, the change requires the function validate Australian Address to modify how it calls the suburb validation.
From the point of view of the public consumers of the validate Australian Address function, the API change for has Suburb IS refactoring. After all, nothing has to change on their end.
Given that, you could also define refactoring like this:
Refactoring is to apply a series of small code change [1] steps [2] affecting the structure of the code without affecting the observable behavior [3] for consumers [4].
[1]: The checkpoint to define when something has "changed" is after any command that can be used to apply a change: a commit, file save, git add, etc.
[2]: A step is small as long as it doesn't leave the application on a broken state (syntax error, failing test).
[3]: The "no change in behavior" aspect of refactoring is very important to validate the use of TDD. If the code was created using TDD, changes should make a test break, unless they’re refactoring. If a test doesn’t break when changing the code that represents behavior, then the code was not created using TDD.
Speaking of which, can you look at this JSFiddle example? You might find something interesting :)
[4]: Consumers of the code can be internal or external.
So going back to the initial statement:
This public API is too hard to reuse, let’s refactor it.
If the interface of the API is public and hard to use, then when you change it, it’s not gonna be refactoring. It’s gonna be a redesign.
This public API is too hard to reuse, let’s redesign it.
As I've written in 2016, due to Wittgenstein’s Beetle, it is possible that in a real conversation two individuals believe they are talking about the same “thing”, but in reality, they are talking about things that are totally different.
Refactoring is one example of that.
Only when you make a code change that doesn't modify behavior or the public interface between APIs that you can call refactoring. However, even if you change the interface, it may still be considered refactoring if the change is invisible.
Is it a refactoring or not?
As almost everything in software, there’s no simple response.
The answer depends on who's asking.
It just… depends.
Edit September 28, 2018:
Here's a talk from that explains in details his understanding of refactoring in the context of a legacy application. He also talks about how to prove the correctness of code changes.