Источник изображения:bz.zzzmh.cn/
Автор этой статьи: Чен Гуантун
Введение
Недавно я получил задание, и мне нужно было инкапсулировать фреймворк NodeJS верхнего уровня на основе EggJS для команды.В этом процессе я сослался наNestJS,MidwayДождавшись проектов с открытым исходным кодом, я обнаружил, что все они представили важную функцию — IoC, поэтому автор воспользовался этой возможностью, чтобы изучить и разобраться с IoC. Эта статья в основном относится к исходному коду Midway и реализует IoC на основе TypeScript в соответствии с моим собственным пониманием.Я надеюсь предоставить вам некоторую ссылку.
2. ИОК
в соответствии сВикипедия, IoC (инверсия управления) Инверсия управления — это принцип проектирования в объектно-ориентированном программировании для уменьшения связи между компьютерными кодами.
В традиционном объектно-ориентированном процессе кодирования, когда существует зависимость между классами, зависимые объекты обычно создаются непосредственно внутри класса, что приводит к образованию связи между классами Чем сложнее зависимость, тем больше степень связи , будет выше, а сильно связанный код будет очень трудно модифицировать и тестировать. IoC же предоставляет контейнер для создания и поиска зависимых объектов, и передает управление зависимыми объектами изнутри класса контейнеру, что реализует развязку классов и обеспечивает гибкость всех классов Исправлять.
2.1 Соединение
Глядя прямо на объяснение IoC в Википедии, может показаться туманным, что такое связь? Здесь мы возьмем простой пример, предположим, что у нас естьA
,B
Два класса, существующие между ними зависимостиA
полагатьсяB
, с такой зависимостью легко столкнуться в повседневной разработке.Если мы используем традиционные методы кодирования, мы обычно реализуем это следующим образом:
// b.ts
class B {
constructor() {
}
}
// a.ts
class A {
b:B;
constructor() {
this.b = new B();
}
}
// main.ts
const a = new A();
В приведенном выше коде вроде бы нет проблем, однако в это время мы неожиданно получили новые требования, внизуB
Вам нужно передать параметр при инициализации объектаp
:
// b.ts
class B {
p: number;
constructor(p: number) {
this.p = p;
}
}
После модификации возникла проблема, т.к.B
вA
для создания экземпляра в конструктореA
передайте это в конструктореp
,Тем не мениеA
внутриp
Почему? Конечно, мы не можем записать его на смерть, иначе было бы бессмысленно устанавливать этот параметр, поэтому мы можем только установить его вp
также установлен наA
Параметр в конструкторе, как показано ниже:
// a.ts
class A {
b:B;
constructor(p: number) {
this.b = new B(p);
}
}
// main.ts
const a = new A(10);
console.log(a); // => A { b: B { p: 10 } }
Еще более хлопотно, когда мы закончили менятьA
позже нашелB
нужныйp
не может быть однимnumber
, необходимо изменить наstring
, так что мы должны переделатьA
параметр средней парыp
модификация типа. А пока давайте подумаем об этом, предполагая, что существуют также зависимости классов верхнего уровня.A
, точно так же, должен ли высший класс претерпеть ту же модификацию. Это проблема, вызванная связью.Очевидно, что это изменение параметра базового класса, но для этого необходимо изменить все файлы в его ссылке на зависимость.Когда зависимости приложения в определенной степени сложны, это легко вызвать проблемы Явление движения всего тела создает большие трудности для обслуживания приложения.
2.2 Развязка
На самом деле, мы можем обнаружить, что в приведенном выше примере параметр действительно требуетсяp
ТолькоB
,иA
Просто потому, что внутренне зависимый объект должен быть создан.p
, вы должны определить этот параметр, на самом деле это правильноp
Что это такое, совершенно неважно. Следовательно, мы можем рассмотреть возможность удаления экземпляра объекта, от которого зависит класс, от самого класса.Например, мы можем переписать приведенный выше пример следующим образом:
// b.ts
class B {
p: number;
constructor(p: number) {
this.p = p;
}
}
// a.ts
class A {
private b:B;
constructor(b: B) {
this.b = b;
}
}
// main.ts
const b = new B(10);
const a = new A(b);
console.log(a); // A => { b: B { p: 10 } }
В приведенном выше примереA
больше не принимает параметрыp
, но предпочитает напрямую получать объект, от которого он зависит внутренне, и не заботится о том, где создается экземпляр объекта, что эффективно решает проблемы, с которыми мы столкнулись выше.p
, нам нужно только изменитьB
можно без доработокA
, в этом процессе мы реализуем развязку между классами.
2.3 Контейнеры
Хотя мы достигаем разделения, нам все равно нужно инициализировать все классы самим и передать их в качестве параметров конструктора. Если есть глобальный контейнер, внутрипредварительная регистрацияНу и определение класса и параметры инициализации нужных нам объектов, каждый объект имеет уникальный ключ. Затем, когда нам нужно использовать объект, нам нужно только сообщить контейнеру соответствующий ключ, и тогда мы сможем получить к нему прямой доступ из контейнера.выигратьПосле создания экземпляра объекта разработчику не нужно заботиться о процессе создания экземпляра объекта, а также ему не нужно передавать зависимый объект в качестве параметра конструктора по ссылке зависимости.
То есть наш контейнер должен иметь две определенные функции,регистрация экземпляраиПолучать, что очень напоминает Map, исходя из этой идеи мы сначала просто реализуем контейнер:
// container.ts
export class Container {
bindMap = new Map();
// 实例的注册
bind(identifier: string, clazz: any, constructorArgs: Array<any>) {
this.bindMap.set(identifier, {
clazz,
constructorArgs
});
}
// 实例的获取
get<T>(identifier: string): T {
const target = this.bindMap.get(identifier);
const { clazz, constructorArgs } = target;
const inst = Reflect.construct(clazz, constructorArgs);
}
}
Здесь мы используемReflect.construct, который немного похож на новый оператор и помогает нам создавать экземпляры объектов. После того, как у нас есть контейнер, мы можем полностью отказаться от передачи параметров, чтобы добиться развязки, как показано ниже:
// b.ts
class B {
constructor(p: number) {
this.p = p;
}
}
// a.ts
class A {
b:B;
constructor() {
this.b = container.get('b');
}
}
// main.ts
const container = new Container();
container.bind('a', A);
container.bind('b', B, [10]);
// 从容器中取出a
const a = container.get('a');
console.log(a); // A => { b: B { p: 10 } }
Пока что мы в основном реализовали IoC, а разделение классов и классов выполнено на основе контейнера. Но с точки зрения количества кода он не кажется очень лаконичным.Ключевая проблема в том, что инициализация контейнера и регистрация классов по-прежнему заставляют нас чувствовать себя громоздкими.Если эту часть кода можно инкапсулировать в framework регистрация всех классов может производиться автоматически, при этом все классы могут напрямую получать экземпляр зависимого объекта при создании экземпляра, а не указывать его вручную в конструкторе, так что руки разработчика могут быть полностью освобождается и логика внутри класса может быть сосредоточена на написании, которое также называетсяDI (внедрение зависимостей) внедрение зависимостей.
3. ДИ
Многие путают DI и IoC. IoC — это просто принцип проектирования, а DI — это технология реализации для реализации IoC. Проще говоря, мы можем внедрять зависимости в вызывающую программу, не требуя от вызывающей стороны активного получения зависимостей. Для реализации DI необходимо решить следующие две проблемы:
- Классы, которые необходимо зарегистрировать в контейнере IoC, могут автоматически регистрироваться при запуске программы.
- Когда создается экземпляр класса в контейнере IoC, вы можете напрямую получить экземпляр зависимого объекта, не указывая его вручную в конструкторе.
На самом деле существуют разные решения этих двух проблем, такие как знаменитыйJava SpringРазработчику необходимо определить файл XML для зависимостей в контейнере, а структура основана на регистрации экземпляра файла XML и внедрении зависимостей. Но для клиентской разработки управление зависимостями на основе XML слишком громоздко.Идея, предлагаемая Midway Injection, состоит в том, чтобы использовать функцию декоратора TypeScript для определения зависимостей, которые необходимо зарегистрировать и внедрить посредством оформления метаданных, чтобы Полная инъекция зависимостей.
3.1 Reflect Metadata
Чтобы использовать декораторы для решения двух упомянутых выше проблем, нам нужно кратко понятьReflect Metadata. Reflect Metadata — это предложение ES7, которое в основном используется для добавления и чтения метаданных во время объявления TypeScript уже поддерживает его в версии 1.5+.
Метаданные можно понимать как информацию описания для класса или атрибута в классе.Это не влияет на поведение самого класса, но вы можете в любое время получить метаданные, определенные для класса, и выполнить класс на основе метаданные конкретная операция.
Использовать Reflect Metadata очень просто, во-первых, вам нужно установить библиотеку Reflect-metadata:
npm i reflect-metadata --save
В tsconfig.json emitDecoratorMetadata необходимо настроить какtrue
.
Затем мы можем определить и получить метаданные в соответствии с Reflect.defineMetadata и Reflect.getMetadata, например:
import 'reflect-metadata';
const CLASS_KEY = 'ioc:key';
function ClassDecorator() {
return function (target: any) {
Reflect.defineMetadata(CLASS_KEY, {
metaData: 'metaData',
}, target);
return target;
};
}
@ClassDecorator()
class D {
constructor(){}
}
console.log(Reflect.getMetadata(CLASS_KEY, D)); // => { metaData: 'metaData' }
С помощью Reflect мы можем помечать произвольные классы и применять специальную обработку к отмеченным классам. Для получения дополнительной информации о метаданных см.Reflect Metadata.
3.2 Provider
Возвращаясь к только что упомянутому вопросу, нам нужно автоматически определять и регистрировать все классы при запуске приложения, проблема в том, что не все классы нужно регистрировать в контейнере, и мы не знаем, какие классы нужно регистрировать. , и непонятно, какой класс нужно зарегистрировать и как выглядят параметры его инициализации.
Здесь для решения этой проблемы могут быть введены метаданные.Поскольку к метаданным этого класса добавляется специальная метка при его определении, его можно идентифицировать при сканировании. В соответствии с этой идеей мы сначала реализуем декоратор, чтобы пометить класс, который необходимо зарегистрировать.Этот декоратор можно назвать Provider, что означает, что он будет потребляться другими классами как поставщик.
// provider.ts
import 'reflect-metadata'
export const CLASS_KEY = 'ioc:tagged_class';
export function Provider(identifier: string, args?: Array<any>) {
return function (target: any) {
Reflect.defineMetadata(CLASS_KEY, {
id: identifier,
args: args || []
}, target);
return target;
};
}
Как видите, разметка здесь содержитid
иargs
,вid
это то, что мы собираемся использовать для регистрации контейнера IoCkey
,иargs
Это параметр, необходимый при инициализации экземпляра. Провайдер можно использовать непосредственно в виде декоратора следующими способами:
// b.ts
import { Provider } from 'provider';
@Provider('b', [10])
export class B {
constructor(p: number) {
this.p = p;
}
}
После завершения разметки снова возникает проблема, а что если определения этих классов будут получены при старте приложения?
Самая простая идея — сканировать все файлы при запуске, получать классы, экспортируемые каждым файлом, а затем связывать их в соответствии с метаданными. Для простоты предположим, что каталог проекта имеет только один уровень файлов, который реализован следующим образом:
// load.ts
import * as fs from 'fs';
import { CLASS_KEY } from './provider';
export function load(container) { // container 为全局的 IoC 容器
const list = fs.readdirSync('./');
for (const file of list) {
if (/\.ts$/.test(file)) { // 扫描 ts 文件
const exports = require(`./${file}`);
for (const m in exports) {
const module = exports[m];
if (typeof module === 'function') {
const metadata = Reflect.getMetadata(CLASS_KEY, module);
// 注册实例
if (metadata) {
container.bind(metadata.id, module, metadata.args)
}
}
}
}
}
}
Итак, теперь нам нужно только запустить загрузку в main, чтобы завершить привязку всех оформленных классов в каталоге проекта.Стоит отметить, что логика загрузки и контейнера полностью общая, и они могут быть полностью инкапсулированы в пакеты. Фреймворк IoC обретает форму.
import { Container } from './container';
import { load } from './load';
// 初始化 IOC 容器,扫描文件
const container = new Container();
load(container);
console.log(container.get('a')); // A => { b: B { p: 10 } }
3.2 Inject
После решения проблемы с регистрацией рассмотрим вторую упомянутую выше проблему: как получить экземпляр объекта, от которого он зависит, напрямую при инициализации класса, без необходимости вручную передавать параметры через конструктор. На самом деле идея тоже очень проста: мы поместили все классы, которые нужно зарегистрировать, в IoC-контейнер, а затем, когда нам нужно использовать определенный класс, мы можем рекурсивно пройтись по свойствам класса, когда получим экземпляр этого класса и начните с Выньте соответствующий объект из контейнера IoC и назначьте его для завершения внедрения зависимостей.
Итак, аналогичный вопрос, как отличить, какие свойства нужно вводить? Опять же, мы можем использовать метаданные, чтобы решить эту проблему. Просто определите декоратор, чтобы отметить, какие свойства необходимо внедрить.Этот декоратор называется Inject, что означает, что свойство должно быть внедрено с зависимостями.
// inject.ts
import 'reflect-metadata';
export const PROPS_KEY = 'ioc:inject_props';
export function Inject() {
return function (target: any, targetKey: string) {
const annotationTarget = target.constructor;
let props = {};
if (Reflect.hasOwnMetadata(PROPS_KEY, annotationTarget)) {
props = Reflect.getMetadata(PROPS_KEY, annotationTarget);
}
props[targetKey] = {
value: targetKey
};
Reflect.defineMetadata(PROPS_KEY, props, annotationTarget);
};
}
Следует отметить, что хотя мы изменяем свойства здесь, фактические метаданные должны быть определены в классе, чтобы поддерживать список свойств, которые должен внедрить класс, поэтому мы должны использовать target.constructor в качестве цели для работы. Кроме того, для удобства имя свойства (targetKey) используется непосредственно в качестве ключа, соответствующего экземпляру из IoC-контейнера.
Затем нам нужно изменить метод get контейнера IoC, чтобы рекурсивно вводить все свойства:
// container.ts
import { PROPS_KEY } from './inject';
export class Container {
bindMap = new Map();
bind(identifier: string, clazz: any, constructorArgs?: Array<any>) {
this.bindMap.set(identifier, {
clazz,
constructorArgs: constructorArgs || []
});
}
get<T>(identifier: string): T {
const target = this.bindMap.get(identifier);
const { clazz, constructorArgs } = target;
const props = Reflect.getMetadata(PROPS_KEY, clazz);
const inst = Reflect.construct(clazz, constructorArgs);
for (let prop in props) {
const identifier = props[prop].value;
// 递归获取注入的对象
inst[ prop ] = this.get(identifier);
}
return inst;
}
}
При его использовании используйте Inject для изменения необходимых свойств:
// a.ts
import { Provider } from 'provider';
@Provider('a')
export class A {
@Inject()
b: B;
}
3.3 Окончательный код
После вышеуказанных корректировок наш бизнес-код наконец стал таким:
// b.ts
@Proivder('b', [10])
class B {
constructor(p: number) {
this.p = p;
}
}
// a.ts
@Proivder('a')
class A {
@Inject()
private b:B;
}
// main.ts
const container = new Container();
load(container);
console.log(container.get('a')); // => A { b: B { p: 10 } }
Видно, что ручного инстанцирования в коде не будет, сколько бы классов не было зарегистрировано, уровень фреймворка может автоматически все обработать и внедрить необходимые свойства при инстанцировании этих классов. Все экземпляры, которые может предоставить класс, поддерживаются самим классом, и никакие другие файлы не нужно изменять, даже если есть модификации.
резюме
В этой статье описывается необходимость IoC и отделение классов, а также реализуется упрощенная структура IoC на основе TypeScript. На самом деле, помимо развязки, IoC также может принести нам множество преимуществ, таких как быстрое модульное тестирование на основе контейнеров, анализ зависимостей между классами и так далее.
Хотя IoC изначально была концепцией, предложенной сервером, в области внешнего интерфейса были различные приложения. Например, AngularJS реализовал собственную структуру IoC для повышения эффективности разработки и модульности. Заинтересованные читатели могут пройти официальныйкейсПочувствуйте преимущества IoC для внешнего кодирования. Считается, что с расширением фронтенд-функций и ростом сложности приложений эти классические принципы дизайна постепенно становятся обязательным курсом для каждой фронтенд-разработки.
использованная литература
Эта статья была опубликована сКоманда внешнего интерфейса NetEase Cloud Music, Любое несанкционированное воспроизведение статьи запрещено. Мы набираем front-end, iOS и Android круглый год.Если вы готовы сменить работу и любите облачную музыку, присоединяйтесь к нам на grp.music-fe(at)corp.netease.com!