拨开依赖注入的面纱【内含Angular版本简单实现】

262 阅读6分钟

作为一名前端开发者,尤其是Angular的使用者,经常会听到依赖倒置、控制反转、依赖注入这些词汇,也可能已经在不自觉的使用了他们。但是究其原理,大多数还是云里雾里,似懂非懂,今天我们就一起来彻底弄懂这些名词以及他们之间的关系~我们先上他们各自的定义:

依赖倒置(Dependency inversion principle,缩写为 DIP)
是面向对象六大基本原则之一。它是指一种特定的解耦形式。该规则规定:
1. 高层次的模块不应该依赖于低层次的模块,两者都应该依赖于抽象接口。
2. 抽象接口不应该依赖于具体实现。而具体实现则应该依赖于抽象接口。
控制反转(Inversion of Control,缩写为 IOC)
是面向对象编程中的一种设计原则,用来降低计算机代码之间的耦合度。是实现依赖倒置原则的一种代码设计思路。其中最常见的方式叫做依赖注入,还有一种方式叫依赖查找。
依赖注入(Dependency Injection,缩写为 DI)
是实现控制反转的一种方式。常用的依赖注入方法有 3 种: 接口注入 构造函数注入 属性注入

他们之间的关系如下图所示:

图解

看不懂也没关系,读完本文后,我们再回头看这些生涩的定义,说不定会有新的体会。

接下来我们从依赖注入入手,一点一点解释他们之间的关系~

在软件工程中,依赖注入(dependency injection,缩写为 DI)是一种软件设计模式,也是实现控制反转(Inversion of Control,缩写为IoC)的其中一种技术。这种模式能让一个对象接收它所依赖的其他对象。“依赖”是指接收方所需的对象。“注入”是指将“依赖”传递给接收方的过程。在“注入”之后,接收方才会调用该“依赖”。此模式确保了任何想要使用给定服务的对象不需要知道如何创建这些服务。取而代之的是,连接收方对象(像是 client)也不知道它存在的外部代码(注入器)提供接收方所需的服务。 该设计的目的是为了分离关注点,分离接收方和依赖,从而提供松耦合以及代码重用性。

以上定义来自维基百科, 我们从上边的定义提炼出几点

  1. 依赖注入是一种设计模式。
  2. 依赖注入是实现控制反转的一种方式。
  3. 依赖注入的主要作用是松耦合和复用代码。

接下来,我们以一个更贴近于现实的例子,来讲一下依赖注入,控制反转到底是个啥。

一个汽车工厂造了一辆汽车,想为汽车再安装一个中控显示屏,但目前工厂没有早显示屏的能力,于是决定自己成立一个部门研发显示屏:

    // 汽车显示屏
    class CarScreen extends Screen {
        // 播放音乐
        playMusic() {}
    }
    class Car {
        public screen!: Screen;
        constructor() {
            this.screen = new CarScreen();
        }
        playMusic() {
            // 调用中控显示屏播放音乐
            this.screen.playMusic()
        }
    }
    const car = new Car()

这么乍一看没啥问题,虽然多花了不少研发经费,但是功能也实现了,老板很开心。好景不长,到汽车销售出去的时候,用户们集体反馈,显示屏太小,功能太小,要求提供多款不同型号,价位的供用户自助选择。此时工厂犯了愁,因为刚成立的部门技术弱,没法满足用户的需求。决定寻求其他专门做显示器的厂家合作。
    // 第三方汽车显示屏
    class ThirdPartyScreen extends Screen {
        // 播放音乐
        playMusic() {}
    }
    class Car {
        constructor(private screen: ThirdPartyScreen) {}
        playMusic() {
            // 调用中控显示屏播放音乐
            this.screen.playMusic()
        }
    }
    class IocContainer {
        // 接受一个类,返回类的实例
        static get(someClass) {
            ...
            return instance
        }
    }
    IocContainer.get(Car);

IocContainer是一个依赖容器,用来查找Car的依赖项并进行实例化。 这样,车厂就不用关心显示屏到底该怎么造,能不能满足用户后续的需求。用户需求变了,只需要 ThirdPartyScreen进行对应的改造升级即可。

  1. 这个例子中,我们认为车厂和显示屏处于松耦合的状态,即当类依赖于某一个服务时候,不再类中直接对服务进行实例化,而是抛出一个接口告诉类需要什么,具体的实例工作交给第三方的 Ioc容器 去处理。
  2. 从车对显示屏的直接控制,编程了交给 Ioc容器去控制,这种行为我们称之为 控制反转
  3. 这种通过构造函数进行注入其他服务的方式,就是 依赖注入 最常用的方式之一。

那么,如何实现着一套依赖注入呢?我们可以看到,实现依赖注入的核心,就是如何显示IocContainer 这个容器。我们先观察一下再Angular中,是如何运用依赖注入的。

    // 组件
    @Component({
        selector: 'demo-component',
    })
    export class DemoComponent {
        constructor(private someService: DemoServiceA) {}
    }

    // 服务A
    @Injectable()
    export class DemoServiceA {
        constructor(private demoServiceB: DemoServiceB) {}
    }

    // 服务B
    @Injectable({
        providedIn: 'root'
    })
    export class DemoServiceB {}

这个例子中,DemoComponent组件依赖于DemoServiceA, 而DemoServiceA又依赖了DemoServiceB。我们发现,在Angular组件和服务上都有一个装饰器,分别为ComponentInjectable。其中,Component 表示该类是一个Angular中的组件,而Injectable标明该类是一个可以被注入的服务类。我们在使用组件的时候,只需要在模版中写入 <demo-component></demo-component> 即可神器的将组件呈现在页面上,组件也会 '自动的' 使用了其依赖的服务。

providedIn: 'root' 表示该服务只生成一次,且作为为全局单例存在。


这里有一点需要解决的事,如何知道某一个类的依赖项是什么,也就是怎么知道一个类到底依赖于哪个服务类。幸运的是,在TS中通过元数据reflect-metadata可以轻松的实现这一点。(想详细了解的可以看我的这篇文章)。下面是简单的基于reflect-metadata的实现。


    import "reflect-metadata";

    enum InjectableType {
        Component = "Component",
        Service = "Service",
    }

    export interface anyObject {
     [prop: string]: any;
    }

    // 一个装饰器,标明该类是一个Component,不可直接注入到service中,但是可以在Component类中注入service
    export const Component = (): ClassDecorator => {
        // target 为类的构造函数
        return (target: any) => {
            Reflect.defineMetadata("InjectableType", InjectableType.Component, target);
        };
    };

    // 一个装饰器,标明该类是一个service,可以被注入到Component和Service中
    export const Injectable = (metadata?: IInjectable): ClassDecorator => {
        return (target) => {
            Reflect.defineMetadata("InjectableType", InjectableType.Service, target);
            Reflect.defineMetadata("Injectable", metadata, target);
        };
    };

    export class IocContainer {
        // 全局共享的依赖项存在这里
        static shareInjectableDependence = new Map();

        // 获取所有注入的服务
        static get<T>(target: Type<T>): T {
            const InjectableOption = Reflect.getMetadata("Injectable", target) as IInjectable;
            const isRootService = InjectableOption?.providedIn === "root";

            // 如果为root服务,尝试直接取出
            if (isRootService && IocContainer.shareInjectableDependence.get(target)) {
            return IocContainer.shareInjectableDependence.get(target);
            }

            // 获取当前类的依赖项
            const providers = (Reflect.getMetadata("design:paramtypes", target) as Type[]) || [];

            // 递归的获取类依赖项中的依赖
            const args = providers.map((provider: Type) => IocContainer.get(provider));
            const instance = new target(...args) as any;
            return instance;
        }
    }

    export declare interface Type<T = any> extends Function {
         new (...args: any[]): T;
    }
    export declare interface IInjectable {
        providedIn?: Type<any> | "root" | "platform" | "any" | null;
    }

关键代码都有注释,笔者就不再赘述了。
针对于这个依赖注入体系,还可以实现属性注入,lifecycle_hooks等angular中的功能,想要看具体的实现的话可以看下我这个类谷歌离线小恐龙的实现

传送门