Как сделать фабрику объектов, поддерживающую тип

Dec 23 2020

Я создал следующую фабрику объектов для создания экземпляров реализаций всех моих интерфейсов:

interface SomeInterface {
  get(): string;
}

class Implementation implements SomeInterface {
  constructor() {}
  get() {
    return "Hey :D";
  }
}

type Injectable = {
  [key: string]: () => unknown;
};

// deno-lint-ignore prefer-const
let DEFAULT_IMPLEMENTATIONS: Injectable = {
  SomeInterface: () => new Implementation(),
};

let MOCK_IMPLEMENTATIONS: Injectable = {};

class Factory {
  static getInstance(interfaceName: string, parameters: unknown = []) {
    if (MOCK_IMPLEMENTATIONS[interfaceName])
      return MOCK_IMPLEMENTATIONS[interfaceName]();
    return DEFAULT_IMPLEMENTATIONS[interfaceName]();
  }

  static mockWithInstance(interfaceName: string, mock: unknown) {
    MOCK_IMPLEMENTATIONS[interfaceName] = () => mock;
  }
}

export const ObjectFactory = {
  getInstance<T>(name: string): T {
    return Factory.getInstance(name) as T;
  },

  mockWithInstance: Factory.mockWithInstance,
};

const impl = ObjectFactory.getInstance<SomeInterface>("SomeInterface");

Как видите, эта Factory позволяет создавать экземпляры этих интерфейсов и имитировать их. Основная проблема заключается в том, что я должен вызывать эту функцию с именем интерфейса И интерфейса, чтобы сохранить тип в назначениях:

ObjectFactory.getInstance<SomeInterface>("SomeInterface")

Я видел этот вопрос , но мне не нравится идея использовать Baseинтерфейс. Более того, такой подход не сохраняет тип.

В идеале я хотел бы использовать свой подход, но без использования интерфейса, то есть использовать только имя интерфейса.

Боковое примечание: объявление Injectable- это взлом, чтобы заставить этот код работать, в идеале я хотел бы иметь возможность использовать только имя реализации, то есть:

let DEFAULT_IMPLEMENTATIONS = {
    SomeInterface: Implementation
}

Ответы

1 jcalz Dec 23 2020 at 09:48

Поскольку у вас есть фиксированный список сопоставлений имен и типов, которые вы заботитесь о поддержке, общий подход здесь состоит в том, чтобы думать в терминах типа объекта, Tпредставляющего это сопоставление, а затем для любого поддерживаемого имени интерфейса K extends keyof Tвы будете иметь дело с функциями, которые вернуть свойство с этим именем ... а именно функции типа () => T[K]. Другими словами, мы будем использовать keyofтипы поиска и поиска, чтобы помочь предоставить типы вашей фабрике.

Мы будем использовать только конкретный тип, например {"SomeInterface": SomeInterface; "Date": Date}for T, но в дальнейшем компилятору будет проще, если он Tбудет общим. Вот возможная общая реализация ObjectFactoryпроизводителя:

function makeFactory<T>(DEFAULT_IMPLEMENTATIONS: { [K in keyof T]: () => T[K] }) {
  const MOCK_IMPLEMENTATIONS: { [K in keyof T]?: () => T[K] } = {};
  return {
    getInstance<K extends keyof T>(interfaceName: K) {
      const compositeInjectable: typeof DEFAULT_IMPLEMENTATIONS = {
        ...DEFAULT_IMPLEMENTATIONS,
        ...MOCK_IMPLEMENTATIONS
      };
      return compositeInjectable[interfaceName]();
    },
    mockWithInstance<K extends keyof T>(interfaceName: K, mock: T[K]) {
      MOCK_IMPLEMENTATIONS[interfaceName] = () => mock;
    }
  }
}

Я преобразовал вашу версию во что-то, что компилятор может в основном проверить как безопасное по типу, чтобы избежать утверждений типа . Давайте пройдемся по нему.

Эта makeFactoryфункция является универсальной для типа сопоставления объектов Tи принимает аргумент с именем DEFAULT_IMPLEMENTATIONSтипа { [K in keyof T]: () => T[K] }. Это сопоставленный тип , ключи которого Kтакие же, как ключи , Tно свойства которого являются функциями с нулевым аргументом, возвращающими значение типа T[K]. Вы можете увидеть, как выглядело ваше существующее DEFAULT_IMPLEMENTATIONS: каждое свойство было функцией с нулевым аргументом, возвращающей значение соответствующего интерфейса.

Внутри реализации функции мы создаем MOCK_IMPLEMENTATIONS. Эта переменная имеет почти тот же тип, DEFAULT_IMPLEMENTATIONSно со свойствами, которые являются необязательными (что определяется модификатором необязательности ?в [K in keyof T]?).

Функция возвращает саму фабрику, у которой есть два метода:

getInstanceМетод является общим в K, типа имени интерфейса, а возвращаемое значение имеет тип T[K], соответствующий интерфейс собственности. Я осуществить это путем слияния DEFAULT_IMPLEMENTATIONSи с MOCK_IMPLEMENTATIONSпомощью объекта распространения , и аннотирования , что это compositeInjectableимеет тот же тип , как DEFAULT_IMPLEMENTATIONS. Затем мы индексируем его с помощью interfaceNameи вызываем.

mockWithInstanceМетод также родовой в K, тип имени интерфейса, и принимает параметр типа K(имя интерфейса), и параметр типа T[K](соответствующий интерфейс).


Посмотрим на это в действии:

const ObjectFactory = makeFactory({
  SomeInterface: (): SomeInterface => new Implementation(),
  Date: () => new Date()
});

console.log(ObjectFactory.getInstance("SomeInterface").get().toUpperCase()); // HEY :D
ObjectFactory.mockWithInstance("SomeInterface", { get: () => "howdy" });
console.log(ObjectFactory.getInstance("SomeInterface").get().toUpperCase()); // HOWDY
console.log(ObjectFactory.getInstance("Date").getFullYear()); // 2020

Все работает так, как я думаю. Делаем ObjectFactoryпутем звонка makeFactoryс нужным DEFAULT_IMPLEMENTATIONSобъектом. Здесь я отметил, что SomeInterfaceсвойство возвращает значение типа SomeInterface(в противном случае компилятор определит, Implementationчто может быть более конкретным, чем вы хотели бы).

Затем мы видим, что компилятор позволяет нам вызывать ObjectFactory.getInstance()и ObjectFactory.mockWithInstance()с правильными аргументами и возвращать ожидаемые типы, а также он работает во время выполнения.


Ссылка на игровую площадку на код