如果您熟悉面向对象编程,或者刚刚开始探索它,您可能遇到过缩写词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(); } }
正如我们所见,像 orderservice 和 productservice 这样的依赖关系在类构造函数中紧密耦合。这种直接依赖使得替换或模拟这些组件变得困难,这在测试或交换实现时提出了挑战。
依赖注入(di)
依赖注入 (di) 模式提供了这个问题的解决方案。通过遵循 di 模式,我们可以解耦这些依赖关系,并使我们的代码更加灵活和可测试。以下是我们如何重构代码来实现 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 对象以进行测试。关键是两种实现共享相同的接口,确保灵活性和可互换性。
控制反转 (ioc) 和服务定位器
为了将这一原则付诸实践,我们经常依赖一种称为控制反转 (ioc) 的模式。 ioc 是一种技术,将对依赖项的创建和管理的控制从类本身转移到外部组件。这通常是通过依赖注入容器或服务定位器来实现的,它充当一个注册表,我们可以从中请求所需的依赖项。通过 ioc,我们可以动态地注入适当的依赖项,而无需将它们硬编码到类构造函数中,从而使系统更加模块化并且更易于维护。
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();
正如我们所看到的,依赖项是在容器内注册的,这使得它们可以在必要时被替换或交换。这种灵活性是一个关键优势,因为它促进了组件之间的松散耦合。
但是,这种方法有一些缺点。由于依赖项是在运行时解析的,因此如果出现问题(例如,如果依赖项丢失或不兼容),可能会导致运行时错误。此外,无法保证注册的依赖项将严格符合预期的接口,这可能会导致微妙的问题。这种依赖关系解析方法通常称为服务定位器模式,并且在许多情况下被认为是反模式,因为它依赖于运行时解析并且有可能掩盖依赖关系。
inversifyJS
JavaScript 中用于实现 控制反转 (ioc) 模式的最流行的库之一是 inversifyjs。它提供了一个强大且灵活的框架,用于以干净、模块化的方式管理依赖关系。然而,inversifyjs 有一些缺点。一项主要限制是设置和管理依赖项所需的样板代码量。此外,它通常需要以特定的方式构建应用程序,这可能并不适合每个项目。
inversifyjs 的替代方案是friendly-di,这是一种轻量级且更简化的方法,用于管理 javascript 和 typescript 应用程序中的依赖关系。它的灵感来自于 angular 和 nestjs 等框架中的 di 系统,但设计得更加简约、简洁。
friendly-di 的一些主要优势包括:
- 体积小:只有 2 kb,没有外部依赖。
- 跨平台:在浏览器和 node.js 环境中无缝工作。
- 简单的 api:直观且易于使用,只需最少的配置。
- mit 许可证:具有宽松许可的开源。
但是,需要注意的是,friendly-di 是专为 typescript 设计的,您需要先安装其依赖项才能开始使用它。
npm i friendly-di reflect-metadata
并且还扩展tsconfig.json:
{ "compileroptions": { "experimentaldecorators": true, "emitdecoratormetadata": true } }
上面的例子可以用friendly-di修改:
import 'reflect-metadata'; import { injectable } from 'friendly-di'; @injectable() class productservice { getproducts() { return ['product 1', 'product 2', 'product 3']; } } @injectable() class orderservice { constructor(private productservice: productservice) {} getordersforuser() { return this.productservice.getproducts(); } } @injectable() class userservice { constructor(private orderservice: orderservice) {} getuserorders() { return this.orderservice.getordersforuser(); } } @injectable() class app { constructor(private userservice: userservice) {} run() { return this.userservice.getuserorders(); } }
-
正如我们所看到的,我们添加了 @injectable() 装饰器,它将我们的类标记为可注入的,表明它们是依赖注入系统的一部分。这个装饰器允许 di 容器知道这些类可以在需要的地方实例化和注入。
-
当在构造函数中将类声明为依赖项时,我们不会直接绑定到具体类本身。相反,我们根据其接口来定义依赖关系。这将我们的代码与具体实现解耦,并提供更大的灵活性,从而在需要时更容易交换或模拟依赖项。
-
在此示例中,我们将 userservice 放置在 app 类中。这种模式被称为组合根。 组合根是应用程序中组装和注入所有依赖项的中心位置 – 本质上是我们应用程序依赖关系图的“根”。通过将此逻辑保留在一个位置,我们可以更好地控制如何在整个应用程序中解析和注入依赖项。
最后一步是在 di 容器中注册 app 类,这将使容器能够在应用程序启动时管理生命周期和所有依赖项的注入。
import { container } from 'friendly-di'; const app = new container(app).compile(); app.run();
如果我们需要替换应用程序中的任何类,我们只需要按照原始接口创建模拟类:
@injectable() class mockproductservice { getproducts() { return ['new product 1', 'new product 2', 'new product 3']; } }
然后使用替换方法,我们将可替换类声明为模拟类:
import { container } from 'friendly-di'; const app = new container(app) .replace(productservice, mockproductservice) .compile(); app.run();
友好-di我们可以多次替换:
const app = new Container(App) .replace(ProductService, MockProductService) .replace(OrderService, MockOrderService) .compile(); app.run();
就这样,如果您对此主题有任何意见或澄清,请在评论中写下您的想法。