为什么要在 React 组件组合中应用开闭原则?
你有没有看过一段乱七八糟的代码,恨不得把它烧掉?我肯定有过😊。这就是我开始学习软件架构的原因。我开始思考如何构建一个简洁、可扩展、可靠的代码库,让开发变得充满乐趣。毕竟,实现新功能应该是令人兴奋的,而不是压力重重的。
在本文中,我们将探讨如何利用组合模式并应用开/闭原则(来自SOLID 原则)来设计我们的应用程序,使其易于使用、可扩展且编写功能令人愉悦。
请注意:这是我的 React 设计模式系列文章(基于 SOLID 原则)的第二部分。您可以在这里找到第一部分。
什么是开闭原理?
在面向对象编程中,开闭原则指出“软件实体(类、模块、函数等)应该对扩展开放,但对修改关闭”;也就是说,这样的实体允许在不修改其源代码的情况下扩展其行为。
如何在 React 中应用 OCP?
在Java 或 Python 等面向对象编程语言中,继承是实现这一概念的途径。它能保持代码的DRY原则,并降低耦合度。如果您熟悉 Angular 2+,就会知道 Angular 2+ 也支持继承。然而,JavaScript 并非纯粹的面向对象语言,它不支持像 Java、Python 或 C# 等面向对象语言那样的经典继承。因此,在 Angular 2+ 中,当您实现接口或扩展类时,框架本身会在后台进行一些处理,让您感觉像是在编写面向对象代码。但在 React 中,我们没有这种便利。React 团队鼓励使用函数式组合而非继承。高阶函数是 JavaScript 重用代码并保持 DRY 原则的一种方式。
让我们来看一些代码,了解组件的构成方式,以及如何遵循开闭原则编写简洁可靠的代码。
下面是一个App正在渲染的组件OrderReport,我们传入了一个客户对象作为 props。
function App() {
const customer = {
name: 'Company A',
address: '720 Kennedy Rd',
total: 1000
}
return (
<div className="App">
<OrderReport customer={customer}/>
</div>
);
}
现在让我们来看看我们的OrderReport组件
function OrderReport(props) {
return (
<div>
<b>{props.customer.name}</b>
<hr />
<span>{props.customer.address}</span>
<br />
<span>Orders: {props.customer.total}</span>
{props.children}
</div>
);
}
这个组件有个小秘密 ;),它不太喜欢改动。比如说,假设我们新增了一个客户对象,比原来的客户对象多了几个字段。我们想根据作为 props 传递的新客户对象来渲染一些额外的信息。那么,让我们来看看下面的代码。
const customerB = {
name: "Company B",
address: "410 Ramsy St",
total: 1000,
isEligible: true,
isFastTracked: false
};
const customerC = {
name: "Company C",
address: "123 Abram Ave",
total: 1010,
specialDelivery: true
};
我们添加了两个新的客户对象,它们都带有一些新的额外键。假设我们需要根据这些键在组件中渲染额外的 HTML 元素。因此,我们的App组件现在返回类似这样的内容:
return (
<div className="App">
<OrderReport customer={customer} />
<OrderReport customer={customerB} />
<OrderReport customer={customerC} />
</div>
);
我们会OrderReport相应地修改组件,以便根据传递的 props 渲染额外的功能。所以我们的组件现在看起来像这样:
function OrderReport(props) {
const [fastTracker, setFastTracker] = React.useState(props.isFastTracked);
return (
<div>
<b>{props.customer.name}</b>
<hr />
<span>{props.customer.address}</span>
<br />
<span>Orders: {props.customer.total}</span>
{props.customer.isEligible ? (
<React.Fragment>
<br />
<button
onClick={() => {
setFastTracker(!fastTracker);
}}
/>
</React.Fragment>
) : null}
{props.customer.specialDelivery ? (
<div>Other Logic</div>
) : (
<div>Some option for specialDelivery logic...</div>
)}
{props.children}
</div>
);
}
正如你所见,代码已经开始显得非常混乱。这也违反了单一职责原则。这个组件现在承担了太多任务。根据开闭原则,组件应该对扩展开放,对修改关闭,但在这里我们一次性修改了太多逻辑。我们也给代码引入了不必要的复杂性。为了解决这个问题,让我们创建一个高阶组件来拆分这些逻辑。
const withFastTrackedOrder = BaseUserComponent => props => {
const [fastTracker, setFastTracker] = React.useState(props.isFastTracked);
const baseElments = (
<BaseUserComponent customer={props.customer}>
<br />
<button
onClick={() => {
setFastTracker(!fastTracker);
}}
>
Toggle Tracking
</button>
{fastTracker ? (
<div>Fast Tracked Enabled</div>
) : (
<div>Not Fast Tracked</div>
)}
</BaseUserComponent>
);
return baseElments;
};
如上所示,我们创建了withFastTrackedOrder一个 HOC,它使用一个OrderReport组件并添加了一些额外的逻辑和 html。
现在,我们所有的快速订单逻辑都封装在一个withFastTrackedOrder组件中。这里withFastTrackedOrder我们添加一些额外的功能,并扩展我们已有的逻辑OrderReport。让我们把代码恢复OrderReport到如下所示的最小形式。
function OrderReport(props) {
return (
<div>
<b>{props.customer.name}</b>
<hr />
<span>{props.customer.address}</span>
<br />
<span>Orders: {props.customer.total}</span>
{props.children}
</div>
);
}
我们App现在正在像这样渲染它们
function App() {
const FastOrder = withFastTrackedOrder(OrderReport);
return (
<div className="App">
<OrderReport customer={customer} />
<FastOrder customer={customerB} />
</div>
);
}
好了,就是这样。我们已经将逻辑拆分成两个易于维护、简洁的组件。OrderReport现在允许扩展,但不允许修改。
现在假设我们的业务规则要求我们为有特殊订单的客户渲染一些额外的 HTML。我们能否OrderReport再次扩展它?当然可以。让我们创建另一个高阶组件 (HOC) 来组合OrderReport……
const withSpecialOrder = BaseUserComponent => props => {
return (
<BaseUserComponent customer={props.customer}>
<div>I am very special</div>
{props.children}
</BaseUserComponent>
);
};
withSpecialOrder组件正在使用 OrderReport 并添加额外的 HTML 代码。
现在,App我们只需执行以下操作:
function App() {
const FastOrder = withFastTrackedOrder(OrderReport);
const SpecialOrder = withSpecialOrder(OrderReport);
return (
<div className="App">
<OrderReport customer={customer} />
<FastOrder customer={customerB} />
<SpecialOrder customer={customerC} />
</div>
);
}
很棒,对吧?我们将组件拆分成小块,并按逻辑进行划分,避免重复编写相同的逻辑。所有组件都支持扩展,代码可以复用,并保持代码的DRY 原则( Don't Repeat Yourself,不要重复自己) 。
让我们更进一步。假设现在我们的业务允许某些特殊订单当日送达。我们可以编写一个更高阶的组件来封装现有组件,SpecialOrderComponent并在其中添加额外的逻辑。记住,我们的组件始终支持扩展,但不支持修改。因此,通过创建一个新的高阶组件,我们可以扩展现有组件的功能。现在让我们来编写这个高阶组件。
const withSameDayDeliver = SpecialOrderComponent => props => {
return (
<SpecialOrderComponent customer={props.customer}>
<div>I am also same day delivery</div>
{props.children}
</SpecialOrderComponent>
);
};
现在将这个新的 HOC 应用于我们App这样的情况
function App() {
const FastOrder = withFastTrackedOrder(OrderReport);
const SpecialOrder = withSpecialOrder(OrderReport);
const SameDayDelivery = withSameDayDeliver(withSpecialOrder(OrderReport));
return (
<div className="App">
<OrderReport customer={customer} />
<FastOrder customer={customerB} />
<SpecialOrder customer={customerC} />
<SameDayDelivery customer={customerC} />
</div>
);
}
现在,正如您所见,我们创建了一种使用高阶组件 (HOC) 的模式,这种模式始终保持开放性,便于扩展,但难以进行复杂的修改。我们可以添加尽可能多的 HOC,并且随着代码复杂性的增长,我们甚至可以混合搭配这些 HOC。这既能保持代码的简洁性,又能确保代码的封装性,避免更改影响整个系统。从长远来看,它还能维护代码的健全性。
这些文章的内容仍在不断完善中,我会根据行业最佳实践和个人经验持续更新。您的反馈至关重要,如果您有任何意见或建议,请留言。欢迎关注我,获取更多类似文章。
您可以在这里找到本系列上一篇文章的链接。
如果您喜欢这篇文章,请点赞,这会激励我继续创作 :)
接下来我们将讨论里氏替换法如何在 React 组件架构中应用。敬请期待。
文章来源:https://dev.to/shadid12/why-apply-open-close-principles-in-react-component-composition-26p1