Fundamentals 12 min read

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.

Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
Why Long Functions Hurt Maintainability and How to Refactor Them into Pure Units

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.

Illustration
Illustration
unit testingcode qualityrefactoringPure Functions
Rare Earth Juejin Tech Community
Written by

Rare Earth Juejin Tech Community

Juejin, a tech community that helps developers grow.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.