Open innocces opened 2 months ago
August 27, 2024 • 5 minute read
The SOLID principles were the first software design concepts I ever studied and to this day they remain the most influential piece of knowledge in my career. If it wasn’t for them maybe I never would’ve started to pay attention to the quality of my code and the structure of my projects.
Even though they’re best suited for object-oriented development, I always keep them in the back of my mind regardless of the environment and patterns I’m working with.
If there’s one SOLID principle that I’ve managed to apply everywhere it’s the one about interface segregation.
This principle is about the idea that we should avoid creating large interfaces that contain many methods or values. Instead, we should create smaller ones that are more specific to the needs of the functions or classes that use them.
If you’re up for a history lesson, as far as I know, the principle was conceived at Xerox where their software had a single Job class that was responsible for all the tasks you could perform. With time this class grew to a point where its maintainability was a problem.
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();
}
The code became simpler when they split it into smaller classes and interfaces that only covered specific tasks.
interface PrintJob {
print(): void;
}
function print(job: PrintJob) {
// We only expect the interface to implement the print method
job.print();
}
This is only meant as a simple example to illustrate the point. Working with smaller interfaces is not only easier to implement and maintain, but it’s also easier to test.
When I was learning about these principles, though, I found it hard to translate them to front-end development. The only interfaces I interacted with were the prop definitions. And they were always specific to the component, reusing large interfaces wasn’t really a problem.
But after a few years, I found out that the abstract principle of not depending on values you don’t need actually has a place in React.
Imagine that we have the following component that expects a user object to be passed to it as a prop:
interface Props {
user: User;
}
function UserGreeting({ user }: Props) {
return <h1>Hey, {user.name}!</h1>;
}
Don’t focus on the issues of referential equality or re-rendering, they’re not the point right now.
Our component needs a user object, it’s a mandatory prop so we provide it. But after a closer look at the implementation of the component, we notice that it’s actually using only a single value - the name.
This is a violation of the interface segregation principle.
This last sentence sounded quite cryptic but don’t be too bothered. This component is just a little bit deceptive. It communicates to the developers using it that it needs an object even though it uses only a fraction of it. But in programming, as in life, it helps to be honest.
If we depend on the entire object we may make it more difficult for the component to be used. It would be better if we’re clear about the values we need.
interface Props {
name: string;
}
function UserGreeting({ name }: Props) {
return <h1>Hey, {name}!</h1>;
}
The proxy metric we can use to see if we’ve improved our design is to evaluate whether it’s easier to test with this API.
With its previous props, we’d have to mock the entire user object when we wanted to test the component. Any changes to the user object, like adding an extra field, had to be reflected in the component’s tests - even though they wouldn’t be needed.
But with this implementation, we only need to change the primitive values the component expects and it wouldn’t be affected by future changes to the object.
It’s definitely simpler.
Another common way that we violate this principle is through prop drilling. This is a common anti-pattern found in all front-end frameworks. It happens when we pass a value through multiple components that don’t need it so it can reach a specific one.
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>;
}
This is a problem because we’re once again being deceptive to our code’s readers. Looking at our component’s props they’d believe that they need the user object so they can render something from it, but in fact, they’re only passing it to their child components. They don’t actually use it.
In this case, we need to look for a different solution.
In React, this is most often done by using a context or using a state management library that will allow our component to read the value directly.
function UserGreeting() {
const user = useUser();
return <h1>Hey, {user.name}!</h1>;
}
But another option that we often forget is component composition.
function Dashboard({ user }) {
return (
<section>
<Header>
<Navigation>
<UserGreeting name={user.name} />
</Navigation>
</Header>
</section>
)
}
This way we’re not passing values to components that don’t need them.
The beauty in this idea is in how simply it can be described - code shouldn’t depend on values and methods it doesn’t use. If you don’t need something don’t ask for it. It’s as simple as that.
The only interfaces in the examples above are the prop definitions. We’re not doing anything more complex than we usually do. What’s important is that we’re following the main principle, it’s not about an arbitrary implementation.
We don’t want our code to depend on things it doesn’t need and that’s what interface segregation is about.
2024 年 8 月 27 日 • 阅读 5 分钟
SOLID 原则是我研究过的第一个软件设计概念,直到今天它们仍然是我职业生涯中最有影响力的知识。如果没有他们,也许我永远不会开始关注我的代码质量和项目结构。
尽管它们最适合面向对象的开发,但无论我使用的环境和模式如何,我总是将它们牢记在心。
如果有一项我能在任何地方应用的可靠原则,那就是接口隔离。
这个原则是关于我们应该避免创建包含许多方法或值的大型接口的想法。相反,我们应该创建更小的函数,更具体地满足使用它们的函数或类的需求。
如果您准备上历史课,据我所知,该原理是在 Xerox 构思的,他们的软件有一个 Job 类,负责您可以执行的所有任务。随着时间的推移,这个类发展到了可维护性成为问题的地步。
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 <h1>Hey, {user.name}!</h1>;
}
不要关注引用相等或重新渲染的问题,它们不是现在的重点。
我们的组件需要一个用户对象,它是一个强制性的道具,所以我们提供它。但仔细查看组件的实现后,我们注意到它实际上只使用一个值 - 名称。
这违反了接口隔离原则。
最后一句话听起来很神秘,但不要太担心。这个组件只是有点欺骗性。它向使用它的开发人员传达它需要一个对象,即使它只使用了其中的一小部分。但在编程中,就像在生活中一样,诚实是有帮助的。
如果我们依赖于整个对象,我们可能会使组件的使用变得更加困难。如果我们清楚我们需要的价值观会更好。
interface Props {
name: string;
}
function UserGreeting({ name }: Props) {
return <h1>Hey, {name}!</h1>;
}
我们可以用来查看是否改进了设计的代理指标是评估使用此 API 是否更容易测试。
使用之前的 props,当我们想要测试组件时,我们必须模拟整个用户对象。对用户对象的任何更改(例如添加额外字段)都必须反映在组件的测试中 - 即使不需要它们。
但通过这种实现,我们只需要更改组件期望的原始值,并且不会受到未来对象更改的影响。
这绝对更简单。
违反这一原则的另一种常见方式是通过支柱钻井。这是所有前端框架中常见的反模式。当我们通过多个不需要它的组件传递一个值以便它可以到达特定的值时,就会发生这种情况。
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>;
}
这是一个问题,因为我们再次欺骗了代码的读者。看看我们组件的 props,他们会认为他们需要用户对象,以便他们可以从中渲染一些东西,但事实上,他们只是将其传递给他们的子组件。他们实际上并不使用它。
在这种情况下,我们需要寻找不同的解决方案。
在 React 中,这通常是通过使用上下文或使用状态管理库来完成的,这将允许我们的组件直接读取值。
function UserGreeting() {
const user = useUser();
return <h1>Hey, {user.name}!</h1>;
}
但我们经常忘记的另一个选择是组件组合。
function Dashboard({ user }) {
return (
<section>
<Header>
<Navigation>
<UserGreeting name={user.name} />
</Navigation>
</Header>
</section>
)
}
这样我们就不会将值传递给不需要它们的组件。
这个想法的美妙之处在于它的描述非常简单——代码不应该依赖于它不使用的值和方法。如果你不需要什么东西,就不要索要。就这么简单。
上面示例中唯一的接口是 prop 定义。我们并没有做比平常更复杂的事情。重要的是我们遵循主要原则,而不是任意实现。
我们不希望我们的代码依赖于它不需要的东西,这就是接口隔离的目的。
The original blog info