工厂模式——设计模式与前端的结合
想象一下。一家汽车经销商正在销售汽车🚗。突然,他们想拓展业务,销售卡车🚛。你最初编写的订单和销售系统是用来处理汽车的。现在你该怎么做?是否要复制系统中的大部分业务逻辑,专门用于处理卡车?
当然,这样做可以快速见效。不久之后,这家经销商决定开始销售摩托车。
🤦 哦不!代码又重复了?如果订单系统需要修改怎么办?我们现在需要在三个地方更新吗?
我们都经历过这种情况。很难预测这种情况何时会发生。但一旦发生,请记住,有一个解决方案,最初可能需要一些重构,但肯定意味着一个更易于维护的解决方案,尤其是当经销商说要开始销售船舶的时候!🛥️
在本文中,我们将讨论:
- 💪 解决方案 - 工厂模式
- 🤔 什么时候应该使用它?
- 🤯 一些优点和缺点
- ❓ 它在前端世界中被用在哪里?
- 🏭 让我们看一个例子!
💪 解决方案 - 工厂模式
工厂模式是一种创建型设计模式,它在多个泛型对象之间通用的基本行为上添加了一个抽象层。客户端代码
(即将使用此层的代码)无需了解行为的具体实现,只要它存在即可。
如果我们以上面汽车经销商转型为多车经销商的例子为例,我们可以看到,汽车、卡车和船的共同点在于它们都是车辆。经销商内的订单系统只需要与基础车辆打交道,无需了解正在处理的车辆的具体信息。
让我们快速看一下 UML 图来说明这一点:
从图中我们可以看出,系统包含Vehicle
接口的具体实现。OrderSystem
不知道,也不需要知道这些具体实现是什么,它只需依赖于 来VehicleFactory
创建并在需要时返回它们,从而将 我们OrderSystem
与Vehicles
经销商想要销售的 解耦!🚀🚀🚀
他们现在可以扩展到任意数量的车辆,我们只需要创建一个新的接口实现Vehicle
并更新我们的VehicleFactory
接口来创建它!🔥🔥🔥
🤔 什么时候应该使用它?
除了上面描述的情况外,还有几种情况也非常适合此模式:
- 在运行时或运行期间您不知道代码特定部分需要处理的确切类型或依赖关系的任何情况。
- 如果您正在开发一个库,使用工厂模式可以为您提供一种方法,让开发人员扩展其内部组件,而无需访问源本身!
- 如果需要节省系统资源,可以使用此模式创建一个对象池,当新对象不存在时,将其存储在对象池中,当新对象存在时,将从中检索,而不是创建一个新的对象。
🤯 一些优点和缺点
优点:
- 它避免了工厂消费者和具体实现之间的紧密耦合。
- 在某种程度上,它允许在一个区域内维护创建代码,从而满足单一责任原则。
- 它还符合开放/封闭原则,允许添加新的具体实现而不破坏现有代码。
缺点:
- 它会增加代码库的复杂性和可维护性,因为它需要为每个工厂和具体实现创建大量新的子类
❓ 它在前端世界中被用在哪里?
令人惊讶的是(或许并非如此),Angular 允许在其模块提供程序中使用工厂。开发人员可以使用工厂为模块提供依赖项,当提供程序所需的信息直到运行时才可用时,这非常有用。
您可以在 Angular Docs for Factory Providers上阅读有关它们的更多信息。
🏭 让我们看一个例子!
前端的一个很好的例子是跨平台 UI。
想象一下,有一个跨平台应用显示一个对话框。该应用本身应该允许对话框的渲染和隐藏。对话框在移动应用和桌面应用上的渲染方式可能不同。但功能应该相同。应用可以使用工厂在运行时创建正确的对话框。
在这个例子中,我们将使用 TypeScript 创建 a 的两个实现Dialog
,即 aMobileDialog
和 a DesktopDialog
。该应用程序将使用用户代理字符串 (User Agent String) 来判断应用程序是在桌面设备还是移动设备上查看,并使用工厂 (Factory) 创建正确的对话框 (Dialog)。
注意:通常,开发一个响应式对话框是更理想的,但是,这是一个说明工厂模式的示例。
让我们首先创建一个基本的对话框界面
interface Dialog {
template: string;
title: string;
message: string;
visible: boolean;
hide(): void;
render(title: string, message: string): string;
}
此接口定义了所有具体实现都应遵循的通用行为和状态。
让我们创建以下具体实现:
class MobileDialog implements Dialog {
title: string;
message: string;
visible = false;
template = `
<div class="mobile-dialog">
<h2>${this.title};</h2>
<p class="dialog-content">
${this.message}
</p>
</div>
`;
hide() {
this.visible = false;
}
render(title: string, message: string) {
this.title = title;
this.message = message;
this.visible = true;
return this.template;
}
}
class DesktopDialog implements Dialog {
title: string;
message: string;
visible = false;
template = `
<div class="desktop-dialog">
<h1>${this.title};</h1>
<hr>
<p class="dialog-content">
${this.message}
</p>
</div>
`;
hide() {
this.visible = false;
}
render(title: string, message: string) {
this.title = title;
this.message = message;
this.visible = true;
return this.template;
}
}
此功能仅略有不同,您可能会认为抽象类更适合,因为其render
和hide
方法相同。为了便于本示例,我们将继续使用接口。
接下来我们要创建我们的工厂:
class DialogFactory {
createDialog(type: 'mobile' | 'desktop'): Dialog {
if (type === 'mobile') {
return new MobileDialog();
} else {
return new DesktopDialog();
}
}
}
我们的工厂采用了type
并将随后创建正确的实现Dialog
。
最后我们的应用程序需要使用我们的工厂:
class App {
dialog: Dialog;
factory = new DialogFactory();
render() {
this.dialog = this.factory.createDialog(isMobile() ? 'mobile' : 'desktop');
if (this.dialog.visible) {
this.dialog.render('Hello World', 'Message here');
}
}
}
查看上面的代码,我们可以看到应用程序不需要了解具体实现,相反,逻辑是的责任DialogFactory
。
希望这个代码示例有助于阐明工厂模式及其在前端世界中的潜在用途。
就我个人而言,我理解此模式的概念和优势,但我不喜欢它在实现过程中对继承的关注和依赖。
欢迎您讨论任何其他示例或您对此模式的看法,因为我对此模式尚未确定。
如果您有任何疑问,请随时在下方提问或在 Twitter 上联系我:@FerryColum。