我的博客

如何判断你的函数是否"只做了一件事"?

用三个实用标准判断函数是否符合单一职责原则

在代码审查(Code Review)中,我们最常听到的建议之一就是:"这个函数太大了,拆分一下。" 或者更学术一点的说法:"违反了单一职责原则(SRP)。"

Robert C. Martin 在《代码整洁之道》中有一句名言:

"函数应该做一件事。做好这件事。只做这件事。"

这句话听起来像真理,但在实际的前端开发中却很难落地。比如一个"提交表单"的按钮点击事件,它似乎天生就要做很多事:校验数据、开启 Loading 状态、发送请求、处理报错、跳转路由……这难道不是"一件事"吗?

今天我们就从前端的视角出发,用三个实用的标准来判断:你的函数到底是在做一件事,还是在做一锅粥?

标准一:能否用"TO"段落清晰描述?

这是最直观的判断方法。尝试用自然语言描述这个函数在做什么,如果你的描述中出现了 "并且 (AND)"、"或者 (OR)" 这样的连接词,那么它很可能做了不止一件事。

❌ 反面教材

假设我们有一个 handleLogin 函数:

"要 (TO) 处理登录,我们需要校验表单数据,并且如果有错误就显示错误提示,否则开启 Loading 状态,并且发送 API 请求,并且将返回的 Token 存入 localStorage,最后跳转到首页。"

这一连串的"并且",说明这个函数承载了:校验逻辑 + UI 状态管理 + 网络请求 + 数据持久化 + 路由控制。

✅ 正面例子

重构后的 handleLogin 应该像是一个"指挥官",而不是"搬砖工":

"要 (TO) 处理登录,我们执行登录流程,成功后重定向。"

代码可能长这样:

// 这是一个只做一件事的函数(它只负责指挥)
async function handleLogin(formData) {
  if (!isValid(formData)) return;
  
  try {
    showLoading();
    await loginService.login(formData);
    redirectToHome();
  } catch (error) {
    showErrorToast(error);
  } finally {
    hideLoading();
  }
}

注意,虽然它调用了多个函数,但它本身只做了一件事:协调登录流程。具体的校验、请求、存储细节,都交给了下层函数去完成。

标准二:抽象层级是否一致?(The Abstraction Level Test)

这是前端代码最容易踩的坑。"做一件事"并不意味着函数只能有一行代码,而是意味着函数内的所有步骤都在同一个抽象层级上。

想象一个公司的组织架构:CEO 负责制定战略(高层级),经理负责分配任务(中层级),员工负责具体干活(低层级)。如果一个函数里既有"战略决策",又有"拧螺丝"的代码,那就是混合抽象层级,它一定做了不止一件事。

🔍 前端视角的混合层级

看看下面这段 React 代码:

// ❌ 混合了不同层级
const UserProfile = () => {
  // ...
  const fetchUser = async () => {
    // 高层语义:获取用户
    
    // 低层实现细节:具体的 fetch 配置、Header 拼接
    const token = localStorage.getItem('token');
    const res = await fetch('/api/user', {
      headers: { 'Authorization': `Bearer ${token}` }
    });
    
    // 低层实现细节:手动解析 JSON
    const data = await res.json();
    
    // 高层语义:设置数据
    setUser(data);
  };
  // ...
}

在这个 fetchUser 函数中:

  • 高层概念:我要获取用户。
  • 低层细节:HTTP Header 怎么拼、LocalStorage 的 Key 是什么、JSON 怎么解析。

判断技巧: 如果你的函数里同时出现了 businessLogic()(业务逻辑)和 element.style.color = 'red'(原生 DOM 操作)或 headers: {}(底层配置),那就是层级混乱。

✅ 修正后

const fetchUser = async () => {
  // 全是高层级,只做一件事:调用服务并更新状态
  const data = await userApi.getUser();
  setUser(data);
};

标准三:代码块是否在"视觉上"分组?

当你阅读一个长函数时,如果你发现自己下意识地想在某些行之间加空行,或者想给某几行代码加个注释来说明"这部分是在初始化数据"、"这部分是在处理 DOM",那么这些"块"就暗示了它们应该被提取出去。

场景:由 useEffect 引发的"大杂烩"

React 的 useEffect 是重灾区。很多时候我们会在一个 Effect 里做太多事。

// ❌ 视觉上明显分成了三块
useEffect(() => {
  // 块1:订阅窗口大小变化
  const handleResize = () => setWidth(window.innerWidth);
  window.addEventListener('resize', handleResize);

  // 块2:埋点上报
  analytics.track('page_view');

  // 块3:初始化数据
  fetchData();

  return () => {
    window.removeEventListener('resize', handleResize);
  };
}, []);

虽然它们都发生在"组件挂载时",但在逻辑上它们是三件完全不相干的事。这不仅违反了单一职责,还让依赖项数组(Dependency Array)变得极其难以管理。

修正: 拆分成三个独立的 useEffect,或者自定义 Hooks (useWindowSize, useTracking, useDataFetching)。

总结:前端"单一职责"实战心法

要判断你的函数是否只做了一件事,请问自己三个问题:

  1. 描述测试:我能用一句不带"并且"的话描述它吗?
  2. 层级测试:我是不是在同一个函数里既写了业务逻辑,又写了 document.querySelectorfetch 配置?
  3. 视觉测试:我是否需要用空行和注释把代码分成好几块?

记住: 我们将大函数拆小的目的,不是为了增加文件数量,而是为了将"做什么(What)"和"怎么做(How)"分离开来。当你把所有的"怎么做(细节)"都藏在小函数里,你的主函数就会像一篇清晰的文章,只告诉你它在"做什么"。

这才是代码整洁之道的精髓。

On this page