Why Long Functions Hurt Maintainability and How to Refactor Them into Pure Units
Long functions exceeding about 20 lines often hide multiple responsibilities, increase cognitive load, impede testing, and introduce hidden side effects; this article explains the problems, illustrates them with real JavaScript examples, and demonstrates a functional‑programming‑inspired refactor that splits logic into small, pure, testable functions.
In code reviews I get uneasy when a function exceeds about 20 lines; I label it a “code length threshold”. Such long functions usually indicate multiple responsibilities, high reading cost, impossible unit testing, and hidden side‑effects.
Why long functions are problematic
They increase cognitive load, mix data fetching, formatting, UI updates, and side effects, making them hard to understand and test. Example of a 50‑line function that generates a user report and sends emails illustrates this.
// This is a function over 50 lines
// Purpose: generate report from user data and send email (does three things)
function handleUserReport(users, sendEmail, isAdmin) {
let result = [];
let flag = false;
console.log("开始处理用户数据...");
for (let i = 0; i < users.length; i++) {
let u = users[i];
if (u.age > 18) {
if (u.active) {
if (u.score > 80) {
result.push({ name: u.name, status: "优秀" });
flag = true;
} else if (u.score > 60) {
result.push({ name: u.name, status: "良好" });
} else {
result.push({ name: u.name, status: "待提升" });
}
} else {
if (isAdmin) {
result.push({ name: u.name, status: "非活跃但保留" });
} else {
result.push({ name: u.name, status: "非活跃" });
}
}
} else {
if (u.active) {
result.push({ name: u.name, status: "未成年用户" });
}
}
}
console.log("用户数据处理完毕");
console.log("生成报告中...");
let report = "用户报告:
";
for (let i = 0; i < result.length; i++) {
report += `${result[i].name} - ${result[i].status}
`;
}
if (flag) {
console.log("存在优秀用户!");
}
if (sendEmail) {
console.log("准备发送邮件...");
// simulate email sending
for (let i = 0; i < result.length; i++) {
if (result[i].status === "优秀") {
console.log(`已发送邮件给:${result[i].name}`);
}
}
}
console.log("处理完成。");
return report;
}The function is hard to read, test, and reason about because it mixes concerns.
Unit‑testing difficulties
Testing such a function requires mocking fetch, localStorage, state hooks, etc., resulting in test code longer than the production code. Often integration tests are the only realistic option.
// Example of a 50‑line mixed async function
async function loadUserProfile(userId) {
setLoading(true);
try {
// 1️⃣ fetch data
const response = await fetch(`/api/user/${userId}`);
const data = await response.json();
// 2️⃣ cache
localStorage.setItem('lastUserId', userId);
// 3️⃣ format data
const displayName = data.firstName + ' ' + data.lastName;
const ageText = data.age ? `${data.age}岁` : '未知年龄';
// 4️⃣ UI state
setUser({
name: displayName,
age: ageText,
hobbies: data.hobbies?.join('、') || '无'
});
// 5️⃣ side effect
if (data.isVIP) {
trackEvent('vip_user_loaded');
showVIPBadge();
}
setLoading(false);
} catch (error) {
console.error('加载失败', error);
setError('加载用户信息失败');
setLoading(false);
}
}Testing this requires mocking fetch, localStorage, and UI helpers, making tests cumbersome.
Hidden side effects
Long functions often mutate globals, dispatch events, or alter window state without callers' knowledge, turning them into unpredictable “mines”. Example of a configuration getter that changes globalCache, window.__APP_MODE__, and localStorage.
function getUserConfig(userId) {
console.log('开始获取用户配置...');
// 1️⃣ modify global cache
globalCache.lastRequestTime = Date.now();
try {
const res = fetch(`/api/config/${userId}`);
const data = res.json();
// 2️⃣ change global mode
window.__APP_MODE__ = data.isAdmin ? 'admin' : 'user';
// 3️⃣ write to localStorage
localStorage.setItem('lastConfigUser', userId);
// 4️⃣ format result
const config = {
theme: data.theme || 'light',
lang: data.lang || 'en-US'
};
return config;
} catch (err) {
console.error('获取配置出错', err);
// 5️⃣ dispatch event
window.dispatchEvent(new CustomEvent('config_load_failed', { detail: { userId } }));
// 6️⃣ reset global cache
globalCache.lastRequestTime = null;
return { theme: 'light', lang: 'en-US' };
}
}Functional‑programming mindset
Adopting functional principles means keeping functions small, single‑purpose, and pure: they receive inputs and return outputs without side effects. The article demonstrates extracting validation, payload creation, and API calls into independent pure functions.
Refactoring example
Original handleRegister mixes validation, payload building, API calls, UI updates, and error handling in ~20 lines. The refactor splits it into:
Pure validation function ( validateRegistration)
Pure payload creator ( createRegisterPayload)
Side‑effect functions for API call and UI handling
export function validateRegistration(formData) {
if (!formData.username) return '用户名不能为空';
if (formData.password.length < 6) return '密码不能少于6位';
return null;
}
export function createRegisterPayload(formData) {
return {
user: formData.username,
pass: btoa(formData.password + 'my_salt'),
source: 'web',
registerTime: new Date().toISOString()
};
}
export async function postRegistration(payload) {
return api.post('/register', payload);
}
function handleRegisterSuccess(userData) {
setUserData(userData);
trackEvent('register_success');
showToast('注册成功!');
router.push('/dashboard');
}
function handleRegisterFail(error) {
showToast(error.message);
trackEvent('register_fail', { msg: error.message });
}The new handleRegister becomes a thin orchestrator that calls these small, testable units, dramatically improving readability and predictability.
Keeping functions under about 20 lines serves as an early warning sign that a function may be violating the Single Responsibility Principle.
Rare Earth Juejin Tech Community
Juejin, a tech community that helps developers grow.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
