本文为翻译
本文译者为 360 奇舞团前端开发工程师
原文标题:Interface Segregation Principle in React
原文作者:Alex Kondovt
原文地址:https://alexkondov.com/interface-segregation-principle-in-react/
React 中的接口隔离原则
SOLID 原则是我学习的第一个软件设计概念,时至今日,它们仍然是对我的职业生涯影响最大的知识。如果没有它们,也许我永远不会开始关注代码的质量和项目的结构。
尽管它们最适合面向对象的开发,但无论我在什么环境和模式下工作,我都会将它们牢记在心。
说有一条 SOLID 原则我能够应用到任何地方,那就是关于接口隔离的原则。
接口隔离原则
该原则是指我们应避免创建包含许多方法或值的大型接口。相反,我们应该创建更小的接口,以满足使用这些接口的函数或类的需要。
如果你想了解一下历史,据我所知,该原则是施乐公司提出的,当时他们的软件只有一个 “工作 ”类,负责你可以执行的所有任务。随着时间的推移,这个类的可维护性成了问题。
interface Job {
fax(): void;
scan(): void;
print(): void;
}
function print(job: Job) {
// We're using only a single method from the interface
// But we expect the entire interface to be implemented
job.print();
}
于是,他们将其拆分成更小的类和接口,这些类和接口只负责特定的任务,这样代码就变得更简单了。
interface PrintJob {
print(): void;
}
function print(job: PrintJob) {
// We only expect the interface to implement the print method
job.print();
}
这只是一个简单的例子,旨在说明问题:使用较小的接口不仅更易于实现和维护,而且也更易于测试。
然而,当我学习这些原则时,我发现很难将它们转化为前端开发。我交互的唯一接口是 prop 定义。而且它们总是特定于组件,重用大型接口并不是真正的问题。
但几年后,我发现 “不依赖于不需要的值 ”这一抽象原则实际上在 React 中也有用武之地。
依赖庞大的组件
想象一下,我们有如下组件,它希望将用户对象作为prop传递给它:
interface Props {
user: User;
}
function UserGreeting({ user }: Props) {
return Hey, {user.name}!</h1>;
}
不要关注引用相等或重新渲染的问题,它们不是现在的重点。
我们的组件需要一个用户对象,这是一个必须的prop,所以我们提供了它。但仔细观察组件的实现后,我们发现它实际上只使用了一个值--name。
这违反了接口隔离原则。
最后一句话听起来很隐晦,但不用太在意。这个组件只是有点欺骗性。它向使用它的开发人员表明,它需要一个对象,尽管它只使用了对象的一小部分。但在编程中,就像在生活中一样,诚实是有帮助的。
如果我们依赖于整个对象,可能会增加组件的使用难度。如果我们能明确自己需要的值,那就更好了。
interface Props {
name: string;
}
function UserGreeting({ name }: Props) {
return <h1>Hey, {name}!h1>;
}
我们可以用代理指标来衡量我们是否改进了设计,那就是评估使用此 API 进行测试是否更容易。
使用以前的道具,我们在测试组件时必须模拟整个用户对象。对用户对象的任何改动,如添加额外的字段,都必须反映在组件的测试中,即使这些改动是不需要的。
但有了这种实现方式,我们只需更改组件所期望的原始值,而不会受到对象未来变化的影响。
这无疑更简单。
Prop Drilling
违反这一原则的另一种常见方式是Prop Drilling。这是所有前端框架中常见的一种反模式。当我们将一个值传递到多个不需要该值的组件时,就会发生这种情况。
function Dashboard({ user }) {
return (
<section>
<Header />
...
section>
)
}
function Header({ user }) {
return (
<header>
<Navigation user={user} />
header>
)
}
function Navigation({ user }) {
return (
<nav>
<UserGreeting name={user.name} />
...
nav>
)
}
function UserGreeting({ name }) {
return <h1>Hey, {name}!h1>;
}
这是一个问题,因为我们又一次欺骗了代码的读者。通过观察我们组件的道具,他们会认为他们需要用户对象以便从中渲染一些内容,但事实上,他们只是将用户对象传递给他们的子组件。他们实际上并没有使用它。
在这种情况下,我们需要寻找一种不同的解决方案。
在 React 中,最常见的做法是使用上下文或使用状态管理库,让我们的组件直接读取值。
function UserGreeting() {
const user = useUser();
return Hey, {user.name}!</h1>;
}
但我们经常忘记的另一个选择是组件合成。
function Dashboard({ user }) {
return (
<section>
<Header>
<Navigation>
<UserGreeting name={user.name} />
Navigation>
Header>
section>
)
}
这样我们就不会将值传递给不需要它们的组件。
接口隔离化繁为简
这个想法的美妙之处在于它可以被描述得如此简单--代码不应该依赖于它不使用的值和方法。如果你不需要某些东西,就不要要求它。就是这么简单。
上述示例中唯一的接口就是道具定义。我们没有做任何比平时更复杂的事情。重要的是,我们遵循的是主要原则,而不是任意实现。
我们不希望我们的代码依赖于它不需要的东西,这就是接口隔离的意义所在。