Let's continue our series of short posts about code refactoring! In it, we discuss technics and tools that can help you improve your code and projects.
Today we will talk more about moving in small steps and what can help us with that.
Transformation Priority Premise
As we discussed earlier, during refactoring, we want to move in small steps and split change sets into smaller ones.
One of the technics that can help us with it is the Transformation Priority Premise, TPP.
TPP is a set of recommendations on evolving the code in a controlled manner without adding too much complexity at a time.
It offers us a list of transformations:
- ({} → nil) no code at all → code that employs nil
- (nil → constant)
- (constant → constant+) a simple constant to a more complex constant
- (constant → scalar) replacing a constant with a variable or an argument
- (statement → statements) adding more unconditional statements.
- (unconditional → if) splitting the execution path
- (scalar → array)
- (array → container)
- (statement → tail-recursion)
- (if → while)
- (statement → non-tail-recursion)
- (expression → function) replacing an expression with a function or algorithm
- (variable → assignment) replacing the value of a variable.
- (case) adding a case (or else) to an existing switch or if
The goal of this list is to offer us the simplest possible change to the code that we can do.
Simplest Possible Change
When developing and refactoring code, we have to cope with its internal complexity. Sometimes, the complexity comes from the domain itself; other times—from the infrastructure constraints of the system.
In both cases, we usually need to evolve the code from a naive, most straightforward implementation to a more complex one that meets all the project requirements.
The complex implementation might contain many details we need to keep in mind simultaneously. The more details there are, the more cognitive resources we'll spend trying to read and change the code.
TPP helps avoid doing everything at once. It encourages us to gradually update a piece of code, changing small bits, recording each step in the version control system, and integrating changes into the main repository branch more often.
It allows us not to overload our heads with details while writing or refactoring code. For all improvement ideas that come up along the way, we can put them on a to-do list. Later, when we're done with a change, we can compare this to-do list with TPP and choose what to implement next.
The “offloaded” details free up the brain's working memory resources. It will improve our attentiveness and focus.
TPP in Action
For example, let's refer to the code snippet we refactored in one of our previous posts.
In the initial snippet,we had a React component that looked like this:
function Checkout({ user }) {
const { cart, status, error, onSubmit } = useCart(user.id);
const hasError = status === "error";
const isIdle = status === "idle";
const isLoading = status === "loading";
if (!hasError) {
if (isIdle) {
return (
<form onSubmit={onSubmit}>
<ProductList products={cart.products} />
<button>Purchase</button>
</form>
);
} else if (isLoading) {
return "Loading...";
}
if (!isLoading && !isIdle) {
return "We'll call you to confirm the order.";
}
} else {
return error;
}
}
In the end, we simplified the render logic, and the component looked like this:
function Checkout({ user }) {
const { cart, status, error, onSubmit } = useCart(user.id);
const hasError = status === "error";
const isLoading = status === "loading";
const isSubmitted = state === "submitted";
if (hasError) return error;
if (isLoading) return "Loading...";
if (isSubmitted) return "We'll call you to confirm the order.";
return (
<form onSubmit={onSubmit}>
<ProductList products={cart.products} />
<button>Purchase</button>
</form>
);
}
However, we didn't try to come up with the final solution in one go. Instead, we went through a series of transformations, each of which was simple and atomic.
We first started with “inverting” only one condition branch:
function Checkout({ user }) {
// ...
// Handle the “Error” state edge case first:
if (hasError) return error;
// Then, handle everything else:
if (isIdle) {
// ...
} else if (isLoading) {
// ...
}
if (!isLoading && !isIdle) {
// ...
}
}
Then, we inverted another branch:
function Checkout({ user }) {
// ...
if (hasError) return error;
if (!isLoading && !isIdle) return "We'll call you to confirm the order.";
if (isIdle) {
// ...
} else {
// ...
}
}
...And so on until we got the final solution.
Doing so, at each step, we only cared about the simplest, nearest change that could bring us a valuable result.
We didn't try to keep the complexity of the whole component in mind. Instead, we split the task into a series of simpler ones and solved each one at a time. It helped to refactor code gradually and deal with complexity without being overwhelmed by it.
More About Refactoring in My Book
In this post, we only discussed TPP and how it can help us split a task into smaller ones.
We haven't mentioned how TPP relates to the tactic git technic or how we can use it to prevent changes from spreading through the code base.
If you want to know more about these aspects and refactoring in general, I encourage you to check out my online book:
The book is free and available on GitHub. In it, I explain the topic in more detail and with more examples.
Hope you find it helpful! Enjoy the book 🙌
Top comments (2)
Hi there, a good post. One thing though, I noticed in the initial code snippet the first condition is if(hasError) but the else component returns the error? Shouldn't that first condition be if(!hasError) ?
Oops, you're correct! Thanks for noticing!
Updated 🙌