搜索
首页web前端js教程掌握依赖倒置原则:使用 DI 实现干净代码的最佳实践

如果您熟悉面向对象编程,或者刚刚开始探索它,您可能遇到过缩写词SOLID。 SOLID 代表了一组旨在帮助开发人员编写干净、可维护且可扩展的代码的原则。在这篇文章中,我们将重点关注 SOLID 中的“D”,它代表依赖倒置原则

但在深入了解细节之前,让我们首先花点时间了解这些原则背后的“原因”。

在面向对象编程中,我们通常将应用程序分解为类,每个类封装特定的业务逻辑并与其他类交互。例如,想象一个简单的在线商店,用户可以将产品添加到购物车中。此场景可以通过多个类一起进行建模来管理商店的运营。让我们以这个例子为基础来探索依赖倒置原则如何改进我们系统的设计。

class ProductService {
 getProducts() {
   return ['product 1', 'product 2', 'product 3'];
 }
}


class OrderService {
 constructor() {
   this.productService = new ProductService();
 }

 getOrdersForUser() {
   return this.productService.getProducts();
 }
}


class UserService {
 constructor() {
   this.orderService = new OrderService();
 }

 getUserOrders() {
   return this.orderService.getOrdersForUser();
 }
}

正如我们所见,像 OrderServiceProductService 这样的依赖关系在类构造函数中紧密耦合。这种直接依赖使得替换或模拟这些组件变得困难,这在测试或交换实现时提出了挑战。

依赖注入(DI)

依赖注入 (DI) 模式提供了这个问题的解决方案。通过遵循 DI 模式,我们可以解耦这些依赖关系,并使我们的代码更加灵活和可测试。以下是我们如何重构代码来实现 DI:

Mastering the Dependency Inversion Principle: Best Practices for Clean Code with DI

class ProductService {
 getProducts() {
   return ['product 1', 'product 2', 'product 3'];
 }
}


class OrderService {
 constructor(private productService: ProductService) {}

 getOrdersForUser() {
   return this.productService.getProducts();
 }
}


class UserService {
 constructor(private orderService: OrderService) {}

 getUserOrders() {
   return this.orderService.getOrdersForUser();
 }
}


new UserService(new OrderService(new ProductService()));

我们显式地将依赖项传递给每个服务的构造函数,这虽然是朝着正确方向迈出的一步,但仍然会导致紧密耦合的类。这种方法确实稍微提高了灵活性,但它并没有完全解决使我们的代码更加模块化且易于测试的根本问题。

依赖倒置原理(DiP)

依赖倒置原理(DiP)通过回答关键问题更进一步:我们应该传递什么?该原则表明,我们不应传递具体的实现,而应仅传递必要的抽象,特别是与预期接口匹配的依赖项。

例如,考虑带有 getProducts 方法的 ProductService 类,该方法返回 产品数组 。我们可以通过多种方式实现它,而不是直接将 ProductService 耦合到特定的实现(例如,从数据库中获取数据)。一种实现可能从数据库获取产品,而另一种实现可能返回硬编码的 JSON 对象以进行测试。关键是两种实现共享相同的接口,确保灵活性和可互换性。

Mastering the Dependency Inversion Principle: Best Practices for Clean Code with DI

控制反转 (IoC) 和服务定位器

为了将这一原则付诸实践,我们经常依赖一种称为控制反转 (IoC) 的模式。 IoC 是一种技术,将对依赖项的创建和管理的控制从类本身转移到外部组件。这通常是通过依赖注入容器或服务定位器来实现的,它充当注册表,我们可以从中请求所需的依赖项。通过 IoC,我们可以动态地注入适当的依赖项,而无需将它们硬编码到类构造函数中,从而使系统更加模块化并且更易于维护。

Mastering the Dependency Inversion Principle: Best Practices for Clean Code with DI

class ProductService {
 getProducts() {
   return ['product 1', 'product 2', 'product 3'];
 }
}


class OrderService {
 constructor() {
   this.productService = new ProductService();
 }

 getOrdersForUser() {
   return this.productService.getProducts();
 }
}


class UserService {
 constructor() {
   this.orderService = new OrderService();
 }

 getUserOrders() {
   return this.orderService.getOrdersForUser();
 }
}

正如我们所看到的,依赖项是在容器内注册的,这使得它们可以在必要时被替换或交换。这种灵活性是一个关键优势,因为它促进了组件之间的松散耦合。

但是,这种方法有一些缺点。由于依赖项是在运行时解析的,因此如果出现问题(例如,如果依赖项丢失或不兼容),可能会导致运行时错误。此外,无法保证注册的依赖项将严格符合预期的接口,这可能会导致微妙的问题。这种依赖关系解析方法通常称为服务定位器模式,并且在许多情况下被认为是反模式,因为它依赖于运行时解析并且有可能掩盖依赖关系。

InversifyJS

JavaScript 中用于实现 控制反转 (IoC) 模式的最流行的库之一是 InversifyJS。它提供了一个强大且灵活的框架,用于以干净、模块化的方式管理依赖关系。然而,InversifyJS 有一些缺点。一项主要限制是设置和管理依赖项所需的样板代码量。此外,它通常需要以特定的方式构建应用程序,这可能并不适合每个项目。

Mastering the Dependency Inversion Principle: Best Practices for Clean Code with DI

InversifyJS 的替代方案是Friendly-DI,这是一种轻量级且更简化的方法,用于管理 JavaScript 和 TypeScript 应用程序中的依赖关系。它的灵感来自于 Angular 和 NestJS 等框架中的 DI 系统,但设计得更加简约、简洁。

Friendly-DI 的一些主要优势包括:

  • 体积小:只有 2 KB,没有外部依赖。
  • 跨平台:在浏览器和 Node.js 环境中无缝工作。
  • 简单的 API:直观且易于使用,只需最少的配置。
  • MIT 许可证:具有宽松许可的开源。

但是,需要注意的是,Friendly-DI 是专为 TypeScript 设计的,您需要先安装其依赖项才能开始使用它。

class ProductService {
 getProducts() {
   return ['product 1', 'product 2', 'product 3'];
 }
}


class OrderService {
 constructor() {
   this.productService = new ProductService();
 }

 getOrdersForUser() {
   return this.productService.getProducts();
 }
}


class UserService {
 constructor() {
   this.orderService = new OrderService();
 }

 getUserOrders() {
   return this.orderService.getOrdersForUser();
 }
}

并且还扩展tsconfig.json:

class ProductService {
 getProducts() {
   return ['product 1', 'product 2', 'product 3'];
 }
}


class OrderService {
 constructor(private productService: ProductService) {}

 getOrdersForUser() {
   return this.productService.getProducts();
 }
}


class UserService {
 constructor(private orderService: OrderService) {}

 getUserOrders() {
   return this.orderService.getOrdersForUser();
 }
}


new UserService(new OrderService(new ProductService()));

上面的例子可以用Friendly-DI修改:

class ServiceLocator {
 static #modules = new Map();

 static get(moduleName: string) {
   return ServiceLocator.#modules.get(moduleName);
 }

 static set(moduleName: string, exp: never) {
   ServiceLocator.#modules.set(moduleName, exp);
 }
}

class ProductService {
 getProducts() {
   return ['product 1', 'product 2', 'product 3'];
 }
}


class OrderService {
 constructor() {
   const ProductService = ServiceLocator.get('ProductService');
   this.productService = new ProductService();
 }

 getOrdersForUser() {
   return this.productService.getProducts();
 }
}


class UserService {
 constructor() {
   const OrderService = ServiceLocator.get('OrderService');
   this.orderService = new OrderService();
 }

 getUserOrders() {
   return this.orderService.getOrdersForUser();
 }
}

ServiceLocator.set('ProductService', ProductService);
ServiceLocator.set('OrderService', OrderService);


new UserService();
  1. 正如我们所看到的,我们添加了 @Injectable() 装饰器,它将我们的类标记为可注入的,表明它们是依赖注入系统的一部分。这个装饰器允许 DI 容器知道这些类可以在需要的地方实例化和注入。

  2. 当在构造函数中将类声明为依赖项时,我们不会直接绑定到具体类本身。相反,我们根据其接口来定义依赖关系。这将我们的代码与具体实现解耦,并提供更大的灵活性,从而在需要时更容易交换或模拟依赖项。

  3. 在此示例中,我们将 UserService 放置在 App 类中。这种模式被称为组合根组合根是应用程序中组装和注入所有依赖项的中心位置 - 本质上是我们应用程序依赖关系图的“根”。通过将此逻辑保留在一个位置,我们可以更好地控制如何在整个应用程序中解析和注入依赖项。

最后一步是在 DI 容器中注册 App 类,这将使容器能够在应用程序启动时管理生命周期和所有依赖项的注入。

Mastering the Dependency Inversion Principle: Best Practices for Clean Code with DI

npm i friendly-di reflect-metadata

如果我们需要替换应用程序中的任何类,我们只需要按照原始接口创建模拟类:

class ProductService {
 getProducts() {
   return ['product 1', 'product 2', 'product 3'];
 }
}


class OrderService {
 constructor() {
   this.productService = new ProductService();
 }

 getOrdersForUser() {
   return this.productService.getProducts();
 }
}


class UserService {
 constructor() {
   this.orderService = new OrderService();
 }

 getUserOrders() {
   return this.orderService.getOrdersForUser();
 }
}

然后使用替换方法,我们将可替换类声明为模拟类:

class ProductService {
 getProducts() {
   return ['product 1', 'product 2', 'product 3'];
 }
}


class OrderService {
 constructor(private productService: ProductService) {}

 getOrdersForUser() {
   return this.productService.getProducts();
 }
}


class UserService {
 constructor(private orderService: OrderService) {}

 getUserOrders() {
   return this.orderService.getOrdersForUser();
 }
}


new UserService(new OrderService(new ProductService()));

友好-DI我们可以多次替换:

class ServiceLocator {
 static #modules = new Map();

 static get(moduleName: string) {
   return ServiceLocator.#modules.get(moduleName);
 }

 static set(moduleName: string, exp: never) {
   ServiceLocator.#modules.set(moduleName, exp);
 }
}

class ProductService {
 getProducts() {
   return ['product 1', 'product 2', 'product 3'];
 }
}


class OrderService {
 constructor() {
   const ProductService = ServiceLocator.get('ProductService');
   this.productService = new ProductService();
 }

 getOrdersForUser() {
   return this.productService.getProducts();
 }
}


class UserService {
 constructor() {
   const OrderService = ServiceLocator.get('OrderService');
   this.orderService = new OrderService();
 }

 getUserOrders() {
   return this.orderService.getOrdersForUser();
 }
}

ServiceLocator.set('ProductService', ProductService);
ServiceLocator.set('OrderService', OrderService);


new UserService();

就这样,如果您对此主题有任何意见或澄清,请在评论中写下您的想法。

以上是掌握依赖倒置原则:使用 DI 实现干净代码的最佳实践的详细内容。更多信息请关注PHP中文网其他相关文章!

声明
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn
JavaScript的角色:使网络交互和动态JavaScript的角色:使网络交互和动态Apr 24, 2025 am 12:12 AM

JavaScript是现代网站的核心,因为它增强了网页的交互性和动态性。1)它允许在不刷新页面的情况下改变内容,2)通过DOMAPI操作网页,3)支持复杂的交互效果如动画和拖放,4)优化性能和最佳实践提高用户体验。

C和JavaScript:连接解释C和JavaScript:连接解释Apr 23, 2025 am 12:07 AM

C 和JavaScript通过WebAssembly实现互操作性。1)C 代码编译成WebAssembly模块,引入到JavaScript环境中,增强计算能力。2)在游戏开发中,C 处理物理引擎和图形渲染,JavaScript负责游戏逻辑和用户界面。

从网站到应用程序:JavaScript的不同应用从网站到应用程序:JavaScript的不同应用Apr 22, 2025 am 12:02 AM

JavaScript在网站、移动应用、桌面应用和服务器端编程中均有广泛应用。1)在网站开发中,JavaScript与HTML、CSS一起操作DOM,实现动态效果,并支持如jQuery、React等框架。2)通过ReactNative和Ionic,JavaScript用于开发跨平台移动应用。3)Electron框架使JavaScript能构建桌面应用。4)Node.js让JavaScript在服务器端运行,支持高并发请求。

Python vs. JavaScript:比较用例和应用程序Python vs. JavaScript:比较用例和应用程序Apr 21, 2025 am 12:01 AM

Python更适合数据科学和自动化,JavaScript更适合前端和全栈开发。1.Python在数据科学和机器学习中表现出色,使用NumPy、Pandas等库进行数据处理和建模。2.Python在自动化和脚本编写方面简洁高效。3.JavaScript在前端开发中不可或缺,用于构建动态网页和单页面应用。4.JavaScript通过Node.js在后端开发中发挥作用,支持全栈开发。

C/C在JavaScript口译员和编译器中的作用C/C在JavaScript口译员和编译器中的作用Apr 20, 2025 am 12:01 AM

C和C 在JavaScript引擎中扮演了至关重要的角色,主要用于实现解释器和JIT编译器。 1)C 用于解析JavaScript源码并生成抽象语法树。 2)C 负责生成和执行字节码。 3)C 实现JIT编译器,在运行时优化和编译热点代码,显着提高JavaScript的执行效率。

JavaScript在行动中:现实世界中的示例和项目JavaScript在行动中:现实世界中的示例和项目Apr 19, 2025 am 12:13 AM

JavaScript在现实世界中的应用包括前端和后端开发。1)通过构建TODO列表应用展示前端应用,涉及DOM操作和事件处理。2)通过Node.js和Express构建RESTfulAPI展示后端应用。

JavaScript和Web:核心功能和用例JavaScript和Web:核心功能和用例Apr 18, 2025 am 12:19 AM

JavaScript在Web开发中的主要用途包括客户端交互、表单验证和异步通信。1)通过DOM操作实现动态内容更新和用户交互;2)在用户提交数据前进行客户端验证,提高用户体验;3)通过AJAX技术实现与服务器的无刷新通信。

了解JavaScript引擎:实施详细信息了解JavaScript引擎:实施详细信息Apr 17, 2025 am 12:05 AM

理解JavaScript引擎内部工作原理对开发者重要,因为它能帮助编写更高效的代码并理解性能瓶颈和优化策略。1)引擎的工作流程包括解析、编译和执行三个阶段;2)执行过程中,引擎会进行动态优化,如内联缓存和隐藏类;3)最佳实践包括避免全局变量、优化循环、使用const和let,以及避免过度使用闭包。

See all articles

热AI工具

Undresser.AI Undress

Undresser.AI Undress

人工智能驱动的应用程序,用于创建逼真的裸体照片

AI Clothes Remover

AI Clothes Remover

用于从照片中去除衣服的在线人工智能工具。

Undress AI Tool

Undress AI Tool

免费脱衣服图片

Clothoff.io

Clothoff.io

AI脱衣机

Video Face Swap

Video Face Swap

使用我们完全免费的人工智能换脸工具轻松在任何视频中换脸!

热工具

SublimeText3汉化版

SublimeText3汉化版

中文版,非常好用

SublimeText3 英文版

SublimeText3 英文版

推荐:为Win版本,支持代码提示!

SublimeText3 Linux新版

SublimeText3 Linux新版

SublimeText3 Linux最新版

WebStorm Mac版

WebStorm Mac版

好用的JavaScript开发工具

mPDF

mPDF

mPDF是一个PHP库,可以从UTF-8编码的HTML生成PDF文件。原作者Ian Back编写mPDF以从他的网站上“即时”输出PDF文件,并处理不同的语言。与原始脚本如HTML2FPDF相比,它的速度较慢,并且在使用Unicode字体时生成的文件较大,但支持CSS样式等,并进行了大量增强。支持几乎所有语言,包括RTL(阿拉伯语和希伯来语)和CJK(中日韩)。支持嵌套的块级元素(如P、DIV),