Причуды машинописного текста, которые подтолкнули меня к созданию новой библиотеки внедрения зависимостей

Причуды машинописного текста, которые подтолкнули меня к созданию новой библиотеки внедрения зависимостей

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, можете любить его или ненавидеть, если вы добрались сюда и получили удовольствие от чтения, вы уже сделали мой день.

:::информация Также опубликовано здесь.

:::


Оригинал
PREVIOUS ARTICLE
NEXT ARTICLE