职责分离 (Separation of Concerns)。它不仅仅是一种编程技巧,更是一种设计思想,指导我们如何组织代码,使其更易于理解、维护和复用。
在软件开发中,一个复杂的系统可以被分解成多个独立的部分,每个部分都只负责一个特定的功能。这就是“职责分离”的核心思想。这样做的好处是:
- 单一职责:每个模块只做一件事,而且把它做好。
- 高内聚:相关的代码和逻辑都聚集在一起。
- 低耦合:不同模块之间互相依赖的程度很低,一个模块的修改不会轻易影响到其他模块。
目前我遇到的设计该概念的情况,一般是涉及到请求数据并且展示的时候。
代码1:面向过程的职责分离
在第一个例子中,使用了一组独立的函数来实现职责分离。
- 数据请求职责:updateCartInfo() 函数负责发起请求,并处理业务逻辑(如判断购物车更新)。它将数据请求、购物车数量、红点显示逻辑封装在内部,最终只返回一个包含 cartCount 和 redPointShow 的对象。
- 缓存清除职责:hideRedPointAndCache() 函数负责处理另一个完全独立的任务:清除本地缓存。它与数据请求逻辑解耦,但两者可以配合使用。
- 外部调用:负责 UI 展示的模块(例如小程序页面)只需要调用这些函数,然后根据返回的 redPointShow 来决定是否展示小红点,而不需要关心这个值是如何计算出来的。
这种模式的优点是简单直接,易于理解。它将逻辑函数化,通过函数的输入和输出来明确职责边界。
// 伪代码
// 第一个职责:数据请求
export async function updateCartInfo() {
/** cartCount: number = await 请求数据
* redPointShow: boolean = handleRedPointShow(readStatus, cartCount)
*/
return {
cartCount,
redPointShow
}
}
function handleRedPointShow(read: boolean = false, currentCount) {
/** shouldShow = false 下面的逻辑更改这个值,最后返回
* oldCount = wx.getStorageSync 缓存的旧的购物车数量数据
* if (oldCount < currentCount) { shouldShow = true } 请求的数据比缓存的多,即购物车更新了
*/
return shouldShow
}
// 另外一个职责:清除缓存
// (*) 新增一个函数,用于在进入购物车页面时调用
export function hideRedPointAndCache(count) {
const newRedPointStatusOBJ = {
cartProductCount: count
// 不需要缓存中的 show 了,因为 handleRedPointShow 如果没有修改 shouldShow,那么小红点就是隐藏的
}
wx.setStorageSync("currentRedPointStatus", JSON.stringify(newRedPointStatusOBJ))
}我们先不要去过多理解细节,只需要看看代码的结构。也可以看看这个,在 react 中可以使用 hook 来封装数据请求逻辑。
代码2:面向组件的职责分离
第二个例子使用了 React 的自定义 Hook,这是一种面向组件化的职责分离。
- 数据请求职责:useGETFetch() 这个自定义 Hook 承担了所有的数据请求和状态管理职责。它内部包含了 useState、useTransition 和 useEffect 等 Hook,将异步请求、加载状态、数据存储等所有复杂逻辑全部封装起来。
- 外部调用:App 组件是职责分离的受益者。它完全不关心数据是如何被请求、如何处理加载状态、如何更新的。它只通过调用 useGETFetch('/getCarouselData'),然后直接解构出 data 和 isPending 这两个最终结果。
- UI 展示职责:App 组件只专注于一个职责——根据 Hook 返回的状态来渲染 UI。当 isPending 为 true 时,显示“Loading”;当 isPending 变为 false 时,传入 data 渲染 <Carousel> 组件。
这种模式的优点是它利用了现代前端框架的特性,使得逻辑与 UI 的关系更为清晰。自定义 Hook 就像一个智能的数据源,为组件提供了“我想要什么数据”的清晰接口,而组件则只负责“如何展示数据”。
// 第一个职责:数据请求
// fetch.ts
function useGETFetch(url: string) {
const [data, setData] = useState([])
const [isPending, startTransition] = useTransition()
useEffect(() => {
const fetchData = async () => {
try {
const data = await getFetchData(url)
startTransition(async () => {
setData(data) // 会改变 isPending 的值
})
} catch (error) {
console.error("error", error)
}
}
fetchData()
}, [url])
return {
data,
isPending
}
}
// app.tsx
// 另外一个职责:显示控制
function App() {
const { data, isPending } = useGETFetch('/getCarouselData')
return (
<>
{isPending ? 'Loading' : <Carousel data={data}></Carousel>}
</>
)
}异同但形同:核心思想的统一
尽管两段代码的实现方式和技术栈完全不同,但它们都遵循了相同的核心设计思想:
- 对内封装:将复杂的实现细节(如 fetch、try/catch、缓存操作、状态管理等)隐藏在内部。
- 对外开放:只通过简洁、清晰的接口(函数返回值或 Hook 的返回值)暴露必要的结果。
- 职责单一:将“获取数据”和“展示 UI”这两个完全不同的职责分开。
通过这种方式,代码变得更具弹性。无论你决定将后端 API 的 URL 改变,或是更换数据请求库,你都只需要在 useGETFetch() 或者 updateCartInfo() 内部修改,而不需要改动任何 UI 相关的代码。这种强大的解耦能力,正是职责分离带给我们的最大价值。