摘要:维基百科该原则规定高层次的模块不应该依赖与低层次的模块,两者都应该依赖于抽象接口。依赖反转原则则颠倒这种依赖关系,并以上面提到的两个规定作为指导思想。维基百科这些话的意思就是将依赖对象的创建和绑定转移到被依赖对象类的外部来实现。
在这个标题中,除了 JS 是乱入之外,其它的几个词汇都是存在一个共同点的,那就是依赖。
那么,依赖是什么呢?
比如,现在我正在写这篇博客文,但是我得在电脑上编辑,电脑便是我完成这件事的依赖。而在代码中,最直观的体现是模块之间的依赖。如某个模块依赖另外一个模块,那么另外的那个模块就是该模块的依赖。其实在上篇博客文章《JaVaScript中的模块》中,我们也手写了一个模块依赖管理器。
依赖这个理解起来很简单,但这不代表可以随意的依赖。在写模块的时候,讲究个高内聚低耦合,以提高模块的可拓展性和可维护性。模块依赖了谁,怎么去依赖,都关乎了最终模块的好与坏。
还好在编程界有着提高代码质量的金科玉律,我们可以用理论来指导实践,写出更好的代码。
依赖反转原则依赖反转原则(Dependency inversion principle,DIP),是一种特定的解耦形式,使得高层次的模块不依赖于低层次的模块的实现细节,依赖关系被颠倒(反转),从而使得低层次模块依赖于高层次模块的需求抽象。———— 维基百科
该原则规定:
高层次的模块不应该依赖与低层次的模块,两者都应该依赖于抽象接口。
抽象接口不应该依赖于具体实现。而具体实现则应该依赖于抽象接口。
现在用一个例子来解释一波。
// Ajax.js class Ajax { get() { return this.constructor.name; } } export default Ajax; // main.js import Ajax from "./Ajax"; class Main { constructor() { this.render() } render() { let content = (new Ajax()).get(); console.log("content from", content); } } new Main();
刚开始的时候,我们基于 XMLHttpRequest 对象,封装了 Ajax 用于请求数据。后来 fetch 出来了,我们打算跟上时代的脚步,封装 fetch 以取代 Ajax。
// Fetch.js class Fetch { fetch() { return this.constructor.name; } } export default Fetch; // main.js import Fetch from "./Fetch"; class Main { constructor() { this.render(); } render() { let content = (new Fetch()).fetch(); console.log("content from", content); } } new Main();
从以上可以看出来,整个替代过程很麻烦,我们需要找出封装请求模块(Ajax、Fetch)的所有引用,然后替换掉。又由于 Ajax、Fetch 的方法命名也是不同,所以也需要对应地做更改。
这就是传统的处理依赖关系的方式。在这里 Main 是高层次模块,Ajax、Fetch 是低层次模块。依赖关系创建于高层次模块,且高层次模块直接依赖低层次模块,这种依赖关系限制了高层次模块的复用性。
依赖反转原则则颠倒这种依赖关系,并以上面提到的两个规定作为指导思想。
// Service.js class Service { request(){ throw `${this.constructor.name} 没有实现 request 方法!` } } class Ajax extends Service { request(){ return this.constructor.name; } } export default Ajax; // Main.js import Service from "./Service.js"; class Main { constructor() { this.render(); } render() { let content = (new Service).request(); console.log("content from", content); } } new Main();
在这里我们把共同依赖的 Service 作为抽象接口,它就是高层次模块与低层次模块需要共同遵守的契约。在高层次模块中,它会默认 Service 会有 request 方法用来请求数据。在低层次模块中,它会遵从 Service 复写应该存在的方法。这在《在JavaScript中尝试组合模式》中,无论分支对象还是叶对象都实现 expense() 方法的道理差不多。
即使后来需要封装 axios 取代 fetch,我们也只需要在 Service.js 中修改即可。
再次回顾下传统的依赖关系。
依赖关系创建于高层次模块,且高层次模块直接依赖低层次模块。
经过以上的折腾,我们充其量只是解决了高层次模块直接依赖低层次模块的问题。那么依赖关系创建于高层次模块的问题呢?
控制反转如果说依赖反转原则告诉我们该依赖谁,那么控制反转则告诉们谁应该来控制依赖。
像上面的 Main 模块,它依赖 Service 模块。为了获得 Service 实例的引用,Main 在内部靠自身 new 出了一个 Service 实例。这样明显地引用其它模块,无异加大了模块间的耦合。
控制反转(Inversion of Control,IoC),通过控制反转,对象在被创建的时候,有一个控制系统内所有对象的外界实体,将其所依赖的对象的引用传递给它。可以说,依赖被注入到对象中。———— 维基百科
这些话的意思就是将依赖对象的创建和绑定转移到被依赖对象类的外部来实现。实现控制反转最常见的方式是依赖注入,还有一种方式依赖查找。
依赖注入依赖注入(Dependency Injection,DI),在软件工程中,依赖注入是种实现控制反转用于解决依赖性设计模式。一个依赖关系指的是可被利用的一种对象(即服务提供端)。依赖注入是将所依赖的传递给将使用的从属对象(即客户端)。该服务将会变成客户端的状态的一部分。传递服务给客户端,而非允许客户端来建立或寻找服务,是本设计模式的基本要求。
没看懂?没关系。这句话讲的是,把过程放在外面,将结果带入内部。在《JaVaScript中的模块》中,我们已经用到过依赖注入,就是对于依赖模块的模块,则把依赖作为参数使用。
所以我们再次改造下,
// Service.js class Service { request() { throw `${this.constructor.name} 没有实现 request 方法!` } } class Ajax extends Service { request() { return this.constructor.name; } } export default Ajax; // Main.js class Main { constructor(options) { this.Service = options.Service; this.render(); } render() { let content = this.Service.request(); console.log("content from", content); } } export default Main; // index.js import Service from "./Service.js"; import Main from "./Main.js"; new Main({ Service: new Service() })
在 Main 模块中, Service 的实例化是在外部完成,并在 index.js 中注入。相比上一次,改动后的代码并没有看出带来多大的好处。如果我们再增加一个模块呢?
class Router { constructor() { this.init(); } init() { console.log("Router::init") } } export default Router;
# Main.js + this.Service = options.Router; # index.js + import Router from "./Router.js" new Main({ + Router: new Service() })
若是内部实例化就不好处理了。可换成依赖注入后,这个问题就很好解决了。
// utils.js export const toOptions = params => Object.entries(params).reduce((accumulator, currentValue) => { accumulator[currentValue[0]] = new currentValue[1]() return accumulator; }, {}); // Main.js class Main { constructor(options) { Object.assign(this, options); this.render(); } render() { let content = this.Service.request(); console.log("content from", content); } } export default Main; // index.js import Service from "./Service.js"; import Router from "./Router.js"; import Main from "./Main.js"; import { toOptions } from "./utils.js" /** * toOptions 转换成参数形式 * @params {Object} 类 * @return {Object} {Service: Service实例, Router: Router实例} */ const options = toOptions({Service, Router}); new Main(options);
因为依赖注入把依赖的引用从外部引入,所以这里使用 Object.assign(this, options) 方式,把依赖全部加到了 this 上。即使再增加模块,也只需要在 index.js 中引入即可。
到了这里,DIP、IoC、DI 的概念应该有个清晰的认识了。然后我们再结合实际,加个功能再次巩固以下。作为一功能个独立的模块,一般都有个初始化的过程。
现在我们要做的是遵守一个初始化的约定,定义一个抽象接口,
// Interface.js export class Service { request() { throw `${this.constructor.name} 没有实现 request 方法!` } } export class Init { init() { throw `${this.constructor.name} 没有实现 init 方法!` } } // Service.js import { Init, Service } from "./Interface.js"; import { mix } from "./utils.js" class Ajax extends mix(Init, Service) { constructor() { super(); } init() { console.log("Service::init") } request() { return this.constructor.name; } } export default Ajax;
Main、Service、Router 都依赖 Init 接口(在这里就是一种协定),Service 模块比较特殊,所以做了 Mixin 处理。要做到统一初始化,Main 还需要做些事。
// Main.js import { Init } from "./Interface.js" class Main extends Init { constructor(options) { super(); Object.assign(this, options); this.options = options; this.render(); } init() { (Object.values(this.options)).map(item => item.init()); console.log("Main::init"); } render() { let content = this.Service.request(); console.log("content from", content); } } export default Main;
至此,结束
// index.js import Service from "./Service.js"; import Router from "./Router.js"; import Main from "./Main.js"; import { toOptions } from "./utils.js" /** * toOptions * 转换成参数形式 * @params {Object} 类 * @return {Object} * { * Service: Service实例, * Router: Router实例 * } */ const options = toOptions({ Service, Router }); (new Main(options)).init(); // content from Ajax // Service::init // Router::init // Main::init
(以上所有示例可见GitHub)
文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。
转载请注明本文地址:https://www.ucloud.cn/yun/109275.html
摘要:服务本省作为一个高层类,对外提供访问,却受制于提供具体服务的服务提供者定义的实现,高层模块依赖底层模块实现,违背了依赖倒置原则。遵循依赖倒置原则的例子场景同介绍中场景。 1. 名词介绍 OOD,面向对象设计 DIP,依赖倒置(软件设计原则) IOC,控制反转(软件设计模式) DI,依赖注入 IOC Container,控制反转容器,也是依赖注入容器 2. 组成部分 服务清单(功能...
摘要:在中使用解耦,有两种注入方式构造函数注入属性注入。对象的实例化解析依赖信息该方法实质上就是通过的反射机制,通过类的构造函数的参数分析他所依赖的单元。 有关概念 依赖倒置原则(Dependence Inversion Principle, DIP) 传统软件设计中,上层代码依赖于下层代码,当下层出现变动时,上层也要相应变化。 DIP的核心思想是:上层定义接口,下层实现这个接口,从而使的下...
摘要:构造器注入实现特定参数的构造函数,在新建对象时传入所依赖类型的对象。 基本概念 1.依赖倒置(反转)原则(DIP):一种软件架构设计的原则(抽象概念,是一种思想)在面向对象编程领域中,依赖反转原则(Dependency inversion principle,DIP)是指一种特定的解耦(传统的依赖关系创建在高层次上,而具体的策略设置则应用在低层次的模块上)形式,使得高层次的模块不依赖于...
摘要:依赖注入控制反转的一种具体实现方法。接下来,我们使用依赖注入实现控制反转,使依赖关系倒置依赖被动传入。从单元测试的角度看,依赖注入更方便和操作,方便了测试人员写出质量更高的测试代码。 前言 好的设计会提高程序的可复用性和可维护性,也间接的提高了开发人员的生产力。今天,我们就来说一下在很多框架中都使用的依赖注入。 一些概念 要搞清楚什么是依赖注入如何依赖注入,首先我们要明确一些概念。 D...
摘要:可以为服务提供者的方法设置类型提示。方法将在所有其他服务提供者均已注册之后调用。所有服务提供者都在配置文件中注册。可以选择推迟服务提供者的注册,直到真正需要注册绑定时,这样可以提供应用程序的性能。 本文最早发布于 Rootrl的Blog 导言 Laravel是一款先进的现代化框架,里面有一些概念非常重要。在上手Laravel之前,我认为先弄懂这些概念是很有必要的。你甚至需要重温下PHP...
阅读 1788·2021-08-19 11:12
阅读 1373·2021-07-25 21:37
阅读 945·2019-08-30 14:07
阅读 1240·2019-08-30 13:12
阅读 622·2019-08-30 11:00
阅读 3501·2019-08-29 16:28
阅读 954·2019-08-29 15:33
阅读 2926·2019-08-26 13:40