如果您曾经构建过被复用的代码,那么您可能有过以下经历:
- 你构建一个可重用的代码(函数、React 组件或 React hook 等)并共享它。
- 有人向你提出了一个新的用例,你的代码并不完全支持, 但可以通过一些调整。
- 将参数/prop/选项添加到可重用代码和要支持的该用例的关联逻辑中。
- 重复步骤 2 和 3 几次(或多次😬)。
- 可重用代码现在是使用和维护😭的噩梦
究竟是什么让代码成为使用和维护的噩梦?有几件事可能是问题所在:
- 😵 捆绑包大小和/或性能: 设备需要运行的更多代码,这可能会对性能产生负面影响。
- 😖 维护开销: 以前你的可重用代码只有几个选项,它专注于做好一件事,但现在它可以做很多不同的事情,你需要这些功能的文档。此外,您会遇到很多人询问您如何将其用于其特定用例的问题,这些用例可能会或可能不会很好地映射到您已经添加支持的用例。您甚至可能有两个功能,它们基本上允许相同的操作,但略有不同,因此您将回答有关哪种方法更好的问题。
- 🐛实现复杂性:它从来都不是"只是一个声明"。代码中的每个逻辑分支都与现有的逻辑分支复合在一起。事实上,在某些情况下,你可以支持没有人使用的arguments/options/props的组合,但你必须确保在添加新功能时不要中断,因为你不知道是否有人正在使用这种组合。
- 😕 API 复杂性: 您添加到可重用代码中的每个新arguments/options/props都会使最终用户更难使用,因为您现在有一个很复杂地逻辑,其中记录了所有可用的功能,人们必须学习所有可用的功能才能有效地使用它们。使用起来不太愉快,因为 API 的复杂性通常会以一种使他们的代码更复杂的方式泄漏到应用开发人员的代码中。
我学到的一个原则是"控制反转",这是抽象简单性的真正有效的机制。控制反转的设计思想是将更多的控制权交给用户,而您要做更少的事。
以一个filter函数为例,我们过去可能是这样设计的
function filter(
array,
{
filterNull = true,
filterUndefined = true,
filterZero = false,
filterEmptyString = false,
} = {}
) {
const newArray = [];
for (let index = 0; index < array.length; index++) {
const element = array[index];
if (
(filterNull && element === null) ||
(filterUndefined && element === undefined) ||
(filterZero && element === 0) ||
(filterEmptyString && element === "")
) {
continue;
}
newArray.push(element);
}
return newArray
}
考虑所有可能出现的情况,然后为所有情况做出相应的处理,然而这种方式很容易造成代码耦合,并且使得代码变得很复杂。更好地做法是将需要过滤哪些元素的控制权交给用户。
function filter(array, checkFn = () => true) {
const newArray = [];
for (let index = 0; index < array.length; index++) {
if (checkFn(array[index])) {
newArray.push(array[index]);
}
}
return newArray;
}
console.log(filter([1, 2, 3, 0], (item) => item !== 0));
您只需要写很少地代码,却能得到更好地效果,您不用在绞尽脑汁地考虑各种情况,只需要将一部分控制权交给用户,由用户来选择如何呈现这段代码。
这只是最简单的案例,事实上Array.prototype.map
、Array.prototype.filter
这些API就是采用这种设计思想。此外还有组件插槽,通过slot机制,将组件一部分UI和逻辑的控制权交给用户,从而让组件更灵活,并减少耦合。
参考: