In one of my articles a commenter wrote that he was "in general, refactoring towards pure functions". Now, usually the word "towards" does not lend itself to bold statements, but I grew fond of the expression because it expresses precisely a couple of things that I rarely read or hear about.
Black and White Thinking
Before I get into specifics, this article has a message: black-and-white thinking about paradigms is costing us code quality. In other words forcing one paradigm onto a varied set of problems will result in over- or underengineering.
As I wrote in my previous article Do you need classes in JS/TS?, when you have state to persist between calls and it's not trivial to manage that, using a class
(or an equivalent object + scope combo) is the right way. It's akin to serving your dish with the appropriate cutlery (note that in JavaScript people can eat with their hands regardless of your attempts to neatly tuck things away - see also "monkey patching").
I maintain that even in a mostly functional codebase, if you encounter this particular problem, you're better off using a class
than creating convoluted functional workarounds.
That's why I like the term "refactoring towards pure functions"! We try to convert everything into a pure function, but if that results in something awkward or it just complicates the code, we keep what we have - or even rewrite it in OO-style!
Spotting a Useless Class
With the mindset of turning everything into pure functions, I began finding patterns of useless classes
- ones that just complicate things without leveraging what a class
could offer.
Here is my simple algorithm to spot and remove such classes, hopefully resulting in a cleaner code base!
A class
Without Methods
If a class
has no methods
then, it's just you a POD (plain-old-data) structure. You can simply use your everyday JS object: {}
(of course you should define its type
in TypeScript
).
class Person {
constructor(firstName: string, lastName: string, occupation: string) {
this.firstName = firstName;
this.lastName = lastName;
this.occupation = occupation;
}
// we have no methods here
}
const onePerson = new Person('Edward', 'Elric', 'Alchemist');
// Instead, simply:
type SimplerPerson = {
firstName: string;
lastName: string;
occupation: string;
}
const otherPerson: SimplerPerson = {
firstName: 'Alphonse',
lastName: 'Elric',
occupation: 'Alchemist'
};
// Notice that the second solution is more descriptive!
// If you make a mistake and pass "Alchemist" to "firstName",
// seeing the label "firstName" will alert you or the reviewer
// that something is off.
(You might have a special case where you may want to use a class
for relying on instanceof
, but I can provide an alternative for that in TypeScript - would you rise up for the challenge in the comments? 😉).
A class
Without (Serious) Members
Any method
of the class
that doesn't use this
(or doesn't us this seriously) can be moved into a standalone function
. By "seriously", I mean the members on this
are not just there to "avoid taking arguments" (see below to get what I mean!).
class Operations {
sum(a, b) {
return a + b;
}
// These methods never access `this` — they’re just pure functions!
multiply(a, b) {
return a * b;
}
}
const sum = (a, b) => a + b;
const multiply = (a, b) => a * b;
// While I usually dislike using _named_ unnamed functions,
// they're perfect for 1-liners!
While this one was trivial, often this pattern appears in a more concealed way. See this variety: arguments are shoveled into the constructor
and we dig them back out in the methods - without ever changing them:
export class StringValidator {
constructor(private input: string | null | undefined) {
}
sanitizeString(): string {
if (this.input === undefined || this.input === null) {
return '';
}
return this.input.trim().replace(/\s+/g, ' ');
}
isEmpty(): boolean {
return this.input === undefined || this.input === null || this.input === '';
}
}
// This class has no real internal state!
// We don't modify its internal state ever!
// The constructor just passes a value, and the methods act on it.
// Moreover, there's low cohesion — the methods don't rely on each other.
// Instead you can replace them with two pure functions:
export function sanitizeString(input: string | undefined | null): string {
if (input === undefined || input === null) {
return '';
}
return input.trim().replace(/\s+/g, ' ');
}
export function isEmptyString(input: string | undefined | null): boolean {
return input === undefined || input === null || input === '';
}
Observing the Result
Now, take a look at what's left of the original class
! If you see either of the patterns shows up, continue pulling out data or functions.
Eventually, either your class
disappeared already or it has a tightly-knit set of variables
and methods
.
👆 You may also want to consider splitting large
classes
usingclass
cohesion: if you have disjunct sets ofmethods
andvariables
they can go into their own classes.
The true benefit a class
provides is access to this
. If a method doesn’t use it, or just passes arguments around, then a function is likely more appropriate.
Working Backwards from Usage
You can also spot unnecessary classes by looking at how they're used. A big clue can be one-time usage.
Here's what I spotted recently:
async function transformDataAndSendEmail(recipient: string, someData: SomeData) {
const mailDocument = formatSomeData(someData);
// Why insantiate an entire that's discarded immediately?
// Where's the persisting state we need to manage between calls?
await (new MailService()).sendMail(recipient, mailDocument);
}
As you see MailService
is not a real "service" waiting in memory to handle further requests. It holds no state. It's just extra steps and bureaucracy to reach one function, and it'll be garbage collected right after.
Cleaning Up a Long Component (React)
We can extract pure (or purish) functions not only from classes but from any large code block, as long as:
- We can isolate arguments with a little effort,
- Avoid mutating them,
- Same input yields the same output*,
- With clear output values.
Note(*): there are obvious exceptions, for example if you have to use Math.random()
. My advice is: don't fret about theoretical correctness if the logic can be neatly extracted, just know about it when you need it.
It's ingrained in many React developers that a component must contain everything for rendering — either via props or hooks. This is an anti-pattern resulting in breaking the Single Responsibility Principle, not to mention the mental load of juggling huge components.
import { parseTimeRange } from 'date-lib-of-the-day';
interface ArticleProps {
articleId: string;
}
function PostTime({ articleId }: ArticleProps) {
const article = useArticle(articleId);
const translate = useTranslator();
// other hook calls, etc.
let ago = '';
if (article.timestamp) {
const now = Date.now();
const articleCreationTime = new Date(article.timestamp);
const timePassed = now - articleCreationTime;
const parsedRange = parseTimeRange(timePassed);
if (parsedRange.year > 0) {
// ay-ay, we have to translate!
// On top of everything, "translate" function comes from a hook!
// 🥲 - all hope is lost!
ago = translate(parsedRange.year, 'yearAgo');
} else if (parsedRange.month > 0) {
ago = translate(parsedRange.month, 'monthAgo');
}
// } else if () { ... and so on
}
return <div class="article">
{/* imagine here some useful extra markup */}
<div class="article-time">
{ago}
</div>
</div>
}
It does look like it cannot be helped, right? Calculation of "ago" seemed like a great candidate, but it seems to be tightly coupled with translate
function coming from a hook
.
But is all hope truly lost?
// calculate-ago-from-timestamp.ts
import { parseTimeRange } from 'date-lib-of-the-day';
export type CalculatedAgo {
value: number;
translationKey: string;
}
// I guarantee you that, like this, you'll be more likely to find edge cases!
export function calculateAgoFromTimestamp(timestamp: string): CalculatedAgo {
const now = Date.now();
const articleCreationTime = new Date(article.timestamp);
const timePassed = now - articleCreationTime;
const parsedRange = parseTimeRange(timePassed);
if (parsedRange.year > 0) {
return {
value: parsedRange.year,
translationKey: 'yearAgo'
};
}
if (parsedRange.month > 0) {
return {
value: parsedRange.month,
translationKey: 'monthAgo'
};
}
// if () { ... and so on
}
Resulting in a cleaner React component, check this out!
// article.tsx
import { calculateAgoFromTimestamp } from './calculate-ago-from-timestamp';
// ... types and interfaces
function PostTime({ articleId }: ArticleProps) {
const article = useArticle(articleId);
const translate = useTranslator();
// Look how much cleaner this component looks like now!
let ago = '';
if (article.timestamp) {
const processedTimestamp = calculateAgoFromTimestamp(article.timestamp)
// Oops! We've just solved the coupling with a bit of creativity!
ago = translate(processedTimestamp.value, processedTimestamp.translationKey);
}
return <div class="article">
{/* ... */}
<div class="article-time">
{ago}
</div>
</div>
}
Yeii, 🎉 now we can test this functionality without writing UI tests or without setting up a complicated translation mock! I also guarantee you that the moment you put this in a file and start writing tests you'll get creative finding edge-cases.
Summary
As you can see now, while "refactoring towards pure functions" is not going to give you the feeling of technical superiority it is an excellent tool to clean up code and make sure testing stays easy!
Easy testing usually means better code coverage!
I hope I have convinced you to look for these patterns and mercilessly cut complexity!