Причуды машинописного текста, которые подтолкнули меня к созданию новой библиотеки внедрения зависимостей
20 декабря 2022 г.Я знаю, что есть несколько библиотек для внедрения зависимостей с помощью Typescript; более того, я много использовал их и многому у них научился. Inversify, tsyringe…, это спасибо всем им за то, что я смог написать это, но очевидно, что у создания моего собственного есть мотивация, и я постараюсь изо всех сил объяснить это.
Отказ от ответственности: если вы еще не догадались, поясню: это письмо о библиотеке, которую я создал. Хорошая новость заключается в том, что даже если вы не используете его, прочитав эту статью, вы узнаете о некоторых особенностях Typescript по сравнению с классическими языками, такими как Java или C#, о которых вы, возможно, не знали.
В поисках невозможного
Кто-то может сказать, что внедрение зависимостей — это «просто причудливый способ сказать «передача параметра»»; это хорошее предложение для забавного мема, но вы, вероятно, знаете, что оно имеет большее значение. В частности, вы заметите его реальные преимущества, используя его вместе с другими принципами, например, SOLID.
Буква D в SOLID тесно связана с внедрением зависимостей и относится к инверсии зависимостей; этот принцип побуждает разработчиков использовать абстракции для определения зависимостей в определенных ситуациях. Обычный способ объявить эти абстракции — через интерфейсы, и я бы сделал это с любым другим языком, но давайте посмотрим, что происходит с Typescript.
Представьте себе этот простой компонент:
class NewsletterSender {
public constructor(
private readonly mailer: Mailer,
private readonly postRepository: PostRepository
) {}
public sendLastBlogPosts(emailTo: string): void {
const articles = this.postRepository.getLast(5)
const content = articles.map(a => a.toString()).join("nn")
this.mailer.send(emailTo, 'My blog newsletter', content)
}
}
Как видите, это зависит от двух других сервисов. Следуя оптимальной идее использования интерфейсов для абстрагирования наших зависимостей, мы могли бы определить их как:
interface Mailer {
send(to: string, subject: string, content: string): void
}
interface PostRepository {
getLast(limit: number): Post[]
}
Очевидно, что нам понадобятся некоторые реализации и для этих абстракций:
class MailMonkeyMailer implements Mailer {
public send(to: string, subject: string, content: string): void {
// Some real implementation here
}
}
class MyDbEnginePostRepository implements PostRepository {
public getLast(limit: number): Post[] {
// Some real implementation here
}
}
Основываясь на этом примере, моя идеальная библиотека внедрения зависимостей позволила бы нам регистрировать и использовать эти сервисы примерно так:
container.register(Mailer).use(MailMonkeyMailer)
container.register(PostRepository).use(MyDbEnginePostRepository)
container.registerAndUse(NewsletterSender) // as an alias of container.register(NewsletterSender).use(NewsletterSender)
const newsletterSender = container.get(NewsletterSender)
newsletterSender.sendLastBlogPosts('[email protected]')
К сожалению, это просто невозможно.
Время разработки VS время выполнения
Со временем, если вы используете Typescript, вероятность поиска в Google информации об использовании интерфейсов во время выполнения стремится к единице; так что, боюсь, вы уже знаете ответ: нельзя. Интерфейсы, типы и то, что некоторые люди называют синтаксическим сахаром, недоступны после компиляции в Javascript; поэтому они недоступны во время выполнения.
Приведенные выше классы и интерфейсы приведут к следующему скомпилированному коду JS:
class NewsletterSender {
constructor(mailer, postRepository) {
this.mailer = mailer;
this.postRepository = postRepository;
}
sendLastBlogPosts(emailTo) {
const articles = this.postRepository.getLast(5);
const content = articles.map(a => a.toString()).join("nn");
this.mailer.send(emailTo, 'My blog newsletter', content);
}
}
class MailMonkeyMailer {
send(to, subject, content) {
// Some real implementation here
}
}
class MyDbEnginePostRepository {
getLast(limit) {
// Some real implementation here
}
}
Как видите, никаких следов интерфейсов нет, и по этой причине большинство существующих DI-библиотек предлагают решение на основе строк или символов, что мне не нравится. Отношения между этими символами или строками и соответствующими им интерфейсами слабые и основаны на соглашениях; таким образом, они подвержены ошибкам и мешают рефакторингу.
Лучшей альтернативой является использование абстрактных классов. Это весьма самоуверенное утверждение, вероятно, вызовет нежелание многих людей; но чистый абстрактный класс TS будет доступен во время выполнения и позволит автоматически подключаться с внедрением зависимостей на основе конструктора. Хотя вы можете подумать, что этот подход может привести к проблемам наследования, это не так; примите во внимание, что я предлагаю реализовать, а не расширять эти абстрактные классы.
После внесения этого изменения наши новые абстракции будут выглядеть так (остальный код останется прежним):
abstract class Mailer {
abstract send(to: string, subject: string, content: string): void
}
abstract class PostRepository {
abstract getLast(limit: number): Post[]
}
К сожалению, этого недостаточно.
Декораторы: необходимое зло
Нам удалось создать некоторые конструкции времени выполнения для абстракций наших соавторов классов, используя абстрактные классы вместо интерфейсов; но остается проблема: скомпилированный Javascript не связывает эти абстракции с параметрами конструктора.
Typescript предоставляет единственный официальный способ предоставления данных о типизации в окончательно скомпилированном коде. ; он состоит из декораторов с активированными параметрами компилятора experimentalDecorators
и emitDecoratorMetadata
.
Наконец, код нашего примера будет таким:
@SomeDecorator()
class NewsletterSender {
// ...
}
// ...
И окончательный скомпилированный код:
// here some auto-generated helpers like __decorate or __metadata
class Mailer {
}
class PostRepository {
}
let NewsletterSender = class NewsletterSender {
constructor(mailer, postRepository) {
this.mailer = mailer;
this.postRepository = postRepository;
}
sendLastBlogPosts(emailTo) {
const articles = this.postRepository.getLast(5);
const content = articles.map(a => a.toString()).join("nn");
this.mailer.send(emailTo, 'My blog newsletter', content);
}
};
NewsletterSender = __decorate([
SomeDecorator(),
__metadata("design:paramtypes", [Mailer,
PostRepository])
], NewsletterSender);
class MailMonkeyMailer {
send(to, subject, content) {
// Some real implementation here
}
}
class MyDbEnginePostRepository {
getLast(limit) {
return [];
}
}
Если вы изучите сгенерированный код, показанный выше, вы увидите, что теперь в нем достаточно информации для реализации идеальной библиотеки, о которой я думал в первом разделе этого поста. Тем не менее, я предположу еще кое-что.
Если вы примените принцип инверсии зависимостей к многоуровневой архитектуре, вы получите то, что называется гексагональной архитектурой (или портами). и адаптеры). Этот тип архитектуры пытается отделить ядро ваших приложений от вашей инфраструктуры или конкретных зависимостей, что также означает, что он не должен знать о вашем контейнере с инверсией управления или вашем инструменте внедрения зависимостей. Это трудно сделать с помощью существующих библиотек, потому что большинство из них требуют, чтобы вы использовали свои декораторы для каждой службы, включая те, что находятся во внутренних слоях вашего приложения. Следующий пример — это то, что я не хочу видеть во всем приложении:
import { Injectable } from 'some-fancy-di-lib'
@Injectable()
export class ThisIsADomainService {
// ...
}
Этот код будет связывать каждый отдельный сервис с библиотекой some-fancy-di-lib
. Кроме того, если я захочу прекратить использовать его в будущем, мне нужно будет изменить каждый служебный файл, чтобы удалить или изменить его. Так почему бы и нет…
export const MyService = (): ClassDecorator => {
return <TFunction extends Function>(target: TFunction): TFunction => {
return target
}
}
import { MyService } from '../MyService'
@MyService()
export class ThisIsADomainService {
// ...
}
Каждый декоратор, включая тот, что показан выше, и который ничего не делает, будет инициировать генерацию метаданных, и это все, что нам нужно. Проблема с большинством существующих DI-библиотек заключается в том, что они не будут работать с такими пользовательскими декораторами, как этот, потому что критическая часть их логики находится в декораторах, которые они предоставляют. Моя идеальная библиотека, и та, которую я создал, позволяет пользователям использовать созданные ими декораторы, если они хотят.
Новое колесо: DIOD
DIOD – это новая библиотека внедрения зависимостей, которую я создал для Typescript (Node.js или браузер). трудно (если не невозможно) найти в других библиотеках:
* Зависимости будут автоматически связываться исключительно на основе типов конструктора; поэтому он будет принимать только абстрактные или конкретные классы в качестве типов конструктора. * Функциональность никогда не будет скомпрометирована требованием использовать декораторы, предоставляемые библиотекой. Основные функции будут доступны при использовании любого декоратора, созданного пользователем, чтобы избежать привязки к поставщику и связывания.
Помимо этих специальных функций, он также предоставляет следующие готовые функции:
* Легкий: он всегда будет свободен от зависимостей и весит менее 2 КБ. * Фабричные сервисы: Использование фабрики для создания сервисов. * Услуги, предоставляемые пользователем: использование экземпляра, созданного вручную, для определения услуги. * Область действия: по умолчанию каждый сервис является временным, но они могут быть зарегистрированы как синглтоны или как «по запросу» (один и тот же экземпляр сервиса будет использоваться в одном запросе). * Компилятор: После того, как все необходимые сервисы зарегистрированы, необходимо собрать контейнер. Во время этой сборки DIOD проверит наличие таких ошибок, как отсутствующие зависимости, неправильные конфигурации или циклические зависимости. * Видимость: услуги могут быть помечены как частные. Частные сервисы будут доступны только как зависимости, и их нельзя будет запрашивать из контейнера IoC. * Тегирование: возможность маркировать сервисы в контейнере и запрашивать сервисы на основе тегов. * Поддержка vanilla JS: использование vanilla Javascript возможно путем ручного определения зависимостей службы.
Библиотека доступна в реестре NPM или на Github, можете любить его или ненавидеть, если вы добрались сюда и получили удовольствие от чтения, вы уже сделали мой день.
:::информация Также опубликовано здесь.
:::
Оригинал