Let Code Have Fewer Nestings
This issue talks about how to reduce nesting in code.
Early Return
A common business scenario: get user discount. 10% off for normal users, 12% off for VIP users.
Initially the code was clean.
function getUserDiscount(user) {
if (user?.isVip) return 0.88;
return 1.00;
}
But business is always unpredictable. The boss wants VVIP and VVVIP to make more money.
To encourage users to become VIP, and also treat old users badly, new users get extra discount.
After several brainstorms from the boss and product manager, the code gradually becomes like this.
function getUserDiscount(user) {
if (user) {
if (user.isNewUser) {
if (user.vipLevel === 'vvvip') return 0.65;
else if (user.vipLevel === 'vvip') return 0.72;
else if (user.isVip) return 0.75;
else return 0.79;
} else {
if (user.lastOrderDays > 90) { // returning old user
if (user.vipLevel === 'vvvip') return 0.70;
else if (user.vipLevel === 'vvip') return 0.78;
else if (user.isVip) return 0.82;
else return 0.90;
} else {
if (user.vipLevel === 'vvvip') return 0.68;
else if (user.vipLevel === 'vvip') return 0.75;
else if (user.isVip) return 0.88;
}
}
}
return 1.00;
}
At this rate, soon this function will become a Cthulhu-like horror that no one can understand, and peeking at it drives you mad.
function getUserDiscount(user) {
if (user) { // user exists
if (!user.isBanned && user.accountStatus !== 'frozen') { // account normal not frozen
if (user.isNewUser) { // new user discount
if (!user.hasUsedFirstCoupon && user.registerDays <= 7) { // has first order coupon
if (user.referralCount >= 5) { // has referral code
if (isHoliday('lunarNewYear')) { // holiday special
if (user.vipLevel === 'vvvip') return 0.55;
else if (user.vipLevel === 'vvip') return 0.62;
else if (user.isVip) return 0.65;
else return 0.58;
} else {
// alipay discount
switch (user.paymentMethod) {
case 'alipay':
if (user.vipLevel === 'vvvip') return 0.58;
// ... nesting continues
break;
// 20 cases...
The optimization is simple: exit early.
Use more return statements instead of nested if else layers.
function getUserDiscount(user) {
if (!user) return 1.00;
if (user.isBanned || user.accountStatus === 'frozen') return 1.00;
const vipDiscounts = { vvvip: 0.68, vvip: 0.75, vip: 0.88 };
const vipDisc = vipDiscounts[user.vipLevel];
if (vipDisc) {
if (user.isNewUser) return vipDisc * 0.95; // additional discount for new VIP
if (isTyphoonSignal8() && user.location === 'TseungKwanO') return vipDisc * 0.90; // HK local
return vipDisc;
}
}
This technique is also very common when writing components.
function UserProfile({ userId }: { userId: string }) {
const { data, isLoading, error } = useUser(userId);
return (
<div className="profile-card">
{isLoading ? (
<div className="loading">Loading...</div>
) : error ? (
<div className="error">Failed to load: {error.message}</div>
) : !data ? (
<div className="empty">User not found</div>
) : (
<>
<h1>{data.name}</h1>
<img src={data.avatar} alt={data.name} />
<p>Level: {data.level}</p>
<p>Joined: {formatDate(data.joinAt)}</p>
{/* ... more content */}
</>
)}
</div>
);
}
Can be optimized by early return:
function UserProfile({ userId }: { userId: string }) {
const { data, isLoading, error } = useUser(userId);
if (isLoading) {
return <div className="loading center">Loading...</div>;
}
if (error) {
return (
<div className="error">Failed to load: {error.message}</div>
);
}
if (!data) {
return <div className="empty">User not found</div>;
}
return (
<>
<h1>{data.name}</h1>
<img src={data.avatar} alt={data.name} />
<p>Level: {data.level}</p>
<p>Joined: {formatDate(data.joinAt)}</p>
{/* ... more content */}
</>
);
}
Split Large Functions into Many Small Functions
This optimization is similar to early return.
Take the component above as an example. Obviously it mixes up loading state, error state, 404 state, and normal state UI in one component.
Writing everything in the same component, I can foresee that soon each state will bloat into all sorts of strange shapes.
For instance, loading failure may sprout a [Retry] button, 404 may grow [Auto return after n seconds], etc.
I have seen 5000 lines of code crammed into a single .vue file [vomit].
// For the sake of my sanity, no code shown here
To avoid sanity dropping to zero, we should follow the single responsibility principle. In other words, split the component into smaller parts.
function LoadingState() {
return (
<div className="loading-center">
<div className="spinner-large" />
<p>Loading user profile...</p>
</div>
);
}
function ErrorState({ message }: { message: string }) {
return (
<div className="error-card">
<h3>Failed to load</h3>
<p className="error-message">{message}</p>
<div className="error-actions">
<button onClick={() => window.location.reload()}>Refresh page</button>
<button onClick={() => history.back()}>Go back</button>
</div>
</div>
);
}
function NotFoundState({ userId }: { userId: string }) {
return (
<div className="not-found">
<h2>User not found</h2>
<p>ID: {userId}</p>
<p>It may have been deleted or you entered an incorrect ID</p>
<Link to="/users">View other users</Link>
</div>
);
}
function UserProfile({ userId }: { userId: string }) {
const { data, isLoading, error } = useUser(userId);
if (isLoading) {
return <LoadingState />;
}
if (error) {
return <ErrorState message={error.message} />;
}
if (!data) {
return <NotFoundState userId={userId} />;
}
// Happy path: very clean
return (
<article className="user-profile-page">
<UserHeader user={data} />
<UserBasicInfo user={data} />
</article>
);
}
Table-Driven
How to write a function that returns the day name based on a number?
function getWeekdayName(day) {
if (day === 0) {
return "Sunday";
} else if (day === 1) {
return "Monday";
} else if (day === 2) {
return "Tuesday";
} else if (day === 3) {
return "Wednesday";
} else if (day === 4) {
return "Thursday";
} else if (day === 5) {
return "Friday";
} else if (day === 6) {
return "Saturday";
} else {
return "Invalid weekday value";
}
}
Using switch case doesn't solve the fundamental problem: it's not extensible. For example, to support internationalization, you'd have to rewrite a new function.
Supporting 18 languages means writing 18 new functions. Copy-pasting 18 functions seems easy, but they can't be dynamically selected.
If your boss is also clueless, the team will produce code like this:
if (lang === 'zh-cn') return getWeekdayNameZhCn(day);
if (lang === 'en-us') return getWeekdayNameEnUs(day);
if (lang === 'en-gb') return getWeekdayNameEnGb(day);
if (lang === 'ja') return getWeekdayNameJa(day);
Table-driven replaces if/else with index-based logic. We can organize related data and use indexes to select which part to retrieve.
const WEEKDAYS = {
'zh-cn': {
full: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'],
short: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
veryShort: ['S','M','T','W','T','F','S']
},
'en-us': {
full: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'],
short: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
},
//...
'ja': {
full: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'],
short: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
}
// add as many languages as you like
};
function getWeekdayName(day, lang = 'zh-cn', format = 'full') {
if (!Number.isInteger(day) || day < 0 || day > 6) {
return 'Invalid weekday value';
}
const langData = WEEKDAYS[lang] || WEEKDAYS['zh-cn'];
const names = langData[format] || langData['full'];
return names[day];
}
This technique is generally used when there are many branches (more than 4 or 5) and each branch's logic is highly similar.
For example: dates, HTTP status codes, order status codes, user permission management, etc.