Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

IOC in Typescript #51

Open
zhangxiang958 opened this issue Jan 14, 2020 · 0 comments
Open

IOC in Typescript #51

zhangxiang958 opened this issue Jan 14, 2020 · 0 comments

Comments

@zhangxiang958
Copy link
Owner

什么是 IOC ?

背景

在软件开发领域,模块之间的相互依赖情况是非常常见的,如何降低模块之间的耦合度是软件工程中无法避免的话题。应用会随着业务的发展不断复杂化,而应用功能是由很多不同的模块进行支撑的,如果模块设计不当,会出现牵一发而动全身的情况。

IOC

这个时候,也许你会需要 IOC。所谓 IOC,就是 Inversion Of Control,也就是控制反转。简单地来说,IOC 是通过第三方来降低模块之间的耦合度。

由上图可以知道,模块之间的相互作用与依赖完全靠中间的容器来实现,各个模块之间其实并不知道也不需要知道其他模块的存在与实现。那为什么称为控制反转呢?原因是在通常情况下,模块 A 在初始化或者执行到某个方法或在某个时机会主动创建模块 B 的实例对象,在这个时候,创建权与使用权都在模块 A 手上。而使用 IOC 容器的情况下,模块 B 的创建是在容器手上,而且会将模块 A 需要的模块 B 注入到需要的地方。

而谈及 IOC,同样也会谈起 DI(依赖注入),他们两者是共体的,只是在不同角度来谈论对象模块之间依赖问题。

我们为什么需要 IOC ?

控制反转其实是获取模块依赖的过程被反转了,以往我们在实现一个模块的时候会在模块内部依赖另外一些模块。

我们看下下面的例子:

const FrameWork = require('./framework');
const Wheel = require('./wheel');

class Car {
    constructor() {
        this.frameWork = new FrameWork();
        this.wheel = new Wheel();
    }
    
    run() {
        this.frameWork.startUp();
        this.wheel.scroll();
    }
}

可以看到,Car 类内部实现是需要依赖 FrameWork 和 Wheel 这两个类的。而在引入依赖的时候需要初始化操作-- new 操作符。且不提每个模块文件在 require 引入的时候,我们需要确切的模块文件位置,同时我们在初始化的时候,作为 Car 这一个调用方来讲,它需要知道每个依赖模块的初始化参数,并且在实例化的时候传入,万一依赖方的参数发生了变动,调用方的代码也需要进行变动。

而且随着系统的复杂度不断变化,模块之间的依赖关系可能不再是简单地模块之间的单向依赖,有可能是多向发散型的依赖关系。

所以我们需要一个第三方的模块来帮助具体模块之间的解耦,在我们需要的时候去这个第三方模块取,这个第三方模块就是 IOC 容器。

如果使用了 IOC 容器,那么:

//container.js
const Framework = require('./framework');
const Wheel = require('./wheel');

expor const container = new Container();
container.bind(Framework);
container.bind(Wheel);

// main.js
const Container = require('./container');

class Car {
    constructor() {
        this.frameWork = Container.get('frameWork');
        this.wheel = Container.get('wheel');
    }
    
    run() {
        this.frameWork.startUp();
        this.wheel.scroll();
    }
}

这样主模块 Car 与 Framework 和 Wheel 的耦合度就降低了许多,需要的时候再去容器中取,而且在为 Car 模块做单元测试的时候,切换依赖模块的实现,方便地实现测试环境与生产环境的切换。

如何实现 IoC ?

interface IConfig<T> {
    object?: INewAble<T>;
    factory?: Factory<T>;
    value?: Value<T>;
    cache?: T;
    singleton: boolean;
}

interface INewAble<T> {
    new (...args: any[]): T;
}

type Factory<T> = () => T;
type Registry = Map<symbol, IConfig<any>>

class Options<T> {
    constructor(private _target: IConfig<T>) {}
    
    isSingletonScope() {
        this._target.singleton = true;
    }
}

class Bind<T> {
	private _target: IConfig<T>
    constructor(_target: IConfig<T>) {
        this._target = _target;
    }
    
    to(object: INewAble<T>): Options<T> {
        this._target.object = object;
        return new Options<T>(this._target);
    }
    
    toFactory(factory: Factory<T>): Options<T> {
        this._target.factory = factory;
        return new Options<T>(this._target);
    }
    
    toValue(value: Value<T>): Options<T> {
        this._target.value = value;
        return new Options<T>(this._target);
    }
}

class Container {
	private _registry: Registry = new Map<symbol, IConfig<any>>();
	private _snapshots: Registry[] = [];
	
	bind<T = never>(type: symbol) {
        return new Bind(this._add<T>(type));
	}
	unbind(type: symbol): Container {
        if (this._registry.get(type) === undefined) {
            throw new Error(`${type.toString()} was never bind`);
        }
        
        this._registry.delete(type);
        return this;
	}
	rebind<T = never>(type: symbol): Bind<T> {
        this.unbind(type).bind<T>(type)
	}
	
	get<T = any>(type: symbol): T {
        const regItem = this.registry.get(type);
        
        if (regItem === undefined) {
            throw new Error(`nothing bound to type${type.toString()}`);
        }
        
        const { object, factory, value, cache, singleton } = regItem;
        
        const cacheItem = (creator: () => T): T => {
            if (singleton && typeof cache !== 'undefined') return cache;
            if (!singleton) return creator();
            regItem.cache = creator();
            return regItem.cache;
        }
        
        if (typeof value !== 'undefined') return value;
        if (typeof object !== 'undefined') return cacheItem(() => new object());
        if (typeof factory !== 'undefined') return cacheItem(() => factory());
        
        throw new Error(`nothing bound to type${type.toString()}`);
	}
	
	snapshot(): Container {
        this._snapshots.push(new Map(this._registry));
        return this;
	}
	restore(): Container {
        this._registry = this._snapshots.pop() || this.registry;
        return this;
	}
	
	private _add<T>(type: symbol): IConfig {
        if (this._registry.get(type) !== undefined) {
            throw new Error(`object can only bound once: ${type.toString()}`)
        }
        const conf = { singleton: false };
        this._registry.set(type, conf);
        return conf
	}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant