We've written about the importance of frequent refactoring in any greenfield project, or when introducing new function into existing projects.
Much of refactoring is self-evident following these guidelines:
- No Duplicated code ever!
- One time, One Place, One Concern
- ~10 to 20 lines of code per Function
- Input and output are well-known and bulletproof
- Async First
- Optional mutation
Hyper Focus Causes Problems
As developers, we tend to hyper-focus on the problems at hand. Take for example a MaterialTable component.
We customize it for our project and it works great! But did we implement functions within our specific component for just that specific component?
If the answer is yes, then we may be short changing ourselves. We prefer Reusable code, through favoring composition over inheritance.
Step Back a Bit, Analyze the Work
Have we put functions into our component with names like these?
- insert(x,data)
- remove(x)
- update(data)
- find(predicate)
- addToTop(data)
While these all work perfectly for our component, we want to ask: 'Are they only usable within that component, and What are they really doing?
In this case, they are about altering an array within the component. Therefore; we should refactor these functions by moving to an array module and injecting content there. That way, we can use these functions repeatedly for any array, anywhere, anytime.
Refactoring Methodology
Original Code
/** Inserts data into existing area starting at the index, returns updated data */
insert(index, data) {
this.dataSource.data.splice(index, 0, data);
this.dataSource.data = [...this.dataSource.data];
this.cdf.detectChanges();
return this.dataSource.data;
}
/** Removes the item in the data array at the index location for a length of 1 */
removeByIndex(index) {
this.dataSource.data.splice(index, 1);
this.dataSource.data = [...this.dataSource.data];
this.cdf.detectChanges();
return this.dataSource.data;
}
Any "this." operator above means there is a reference to something else within just that View component.
This is called 'Close Coupling' which can be bad practice. Let's decouple specific work for a better pattern.
Steps
Copy all the code above to a new file for array functions. Name it arrays.ts.
Remove all "this." code.
Create parameters for all errors shown by red lines.
Here's a partial refactor showing the process where the first function refactor is done and the second one just removed the 'this.' code.
The red indicates we need to create a parameter named dataSource.
By removing closely coupled code we are forced to create parameters which become the single interface to our new Reusable Function.
It is the start of thinking in compositional terms where we compose applications by joining the parts together from a parent container. The parent container controls the work flow and states of the application.
Naming Conventions for Functions
If we adopt a rule that says all reusable functions start with the prefix "func" we are easily able to find all functions in our libraries.
/** Takes in a predicate as a call back to find the index */
export function funcFindIndex(dataSource: Array<any>, predicate) {
let index = dataSource.findIndex((item) => predicate(item));
return index;
}
/** Inserts data into existing area starting at the index, returns updated data */
export function funcInsert(index, dataSource, data) {
dataSource.splice(index, 0, data);
dataSource = [...dataSource.data];
return dataSource;
}
Results in this:
How's that for auto-discovery of all functions in the project?
Injecting behavior via re-use
From our specific component we can now easily use and re-use these functions simply by typing in 'func' and allowing intellisense to find the list of functions to use we simply highlight the function and press tab.
The Refactored View
/** Find the index of the
// dataSource.data items
// using predicate (callback)*/
findIndex(predicate) {
// here we are injecting the data!
let index =
funcFindIndex(
this.dataSource.data,
predicate);
return index;
}
/** Inserts data into
// existing area starting at
// the index, returns updated
// data */
insert(index, data) {
// We inject the data
funcInsert(
index,
this.dataSource.data,
data);
this.cdf.detectChanges();
return this.dataSource.data;
}
Notice that only the view calls detectChanges()? This is because only the view is concerned with re-rendering the view. funcInsert should never touch the view!
The Importance of Code Comments
Notice how at each layer we use Code Comments. This is so that intellisense will show us what each function does (without having to look at the code).
Any function or re-usable component without code comments is ultimately worthless down the road. Why? Because new people cannot get into your mind. Leave clues for them especially when they would need to spend days or weeks single stepping through the code to "kind-of-sort-of" get it.
Some code comments will show an example of usage, always good for just getting it up and running.
Summary:
- Refactoring is always moving from specific implementation to a more generic one.
- It separates concerns, continually
- It enforces DRY and SOLID
- It produces bullet-proof code
- It creates lots of small reusable parts.
- It's all good.
JWP2020 Refactoring Typescript
Top comments (3)
Like that series and all of them pinned for future comprehensive reading! 😁
I'd add JSDoc to declare params and what a function returns and it will be perfect 🌝
Thanks Joel what an honor. I've read many of your excellent posts too.
Oh don't be so smoothie, you've at least 15 years of experience more than me! 😂