Jak utworzyć fabrykę obiektów zachowującą typ

Dec 23 2020

Utworzyłem następującą fabrykę obiektów, aby utworzyć instancje wszystkich moich interfejsów:

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");

Jak widać, ta fabryka umożliwia tworzenie instancji i mockowanie tych interfejsów. Głównym problemem jest to, że muszę wywołać tę funkcję z nazwą interfejsu ORAZ interfejsem, aby zachować typ w przypisaniach:

ObjectFactory.getInstance<SomeInterface>("SomeInterface")

Widziałem to pytanie , ale nie podoba mi się pomysł korzystania z Baseinterfejsu. Co więcej, takie podejście również nie zachowuje tego typu.

Idealnie chciałbym użyć swojego podejścia, ale bez konieczności korzystania z interfejsu, to znaczy użyć tylko nazwy interfejsu.

Na marginesie: deklaracja Injectableto hack, aby kod działał, najlepiej, gdybym mógł używać tylko nazwy implementacji, czyli:

let DEFAULT_IMPLEMENTATIONS = {
    SomeInterface: Implementation
}

Odpowiedzi

1 jcalz Dec 23 2020 at 09:48

Ponieważ masz ustaloną listę mapowań typu nazwa-typ, które chcesz wspierać, ogólne podejście polega na rozważeniu typu obiektu Treprezentującego to mapowanie, a następnie w przypadku dowolnej obsługiwanej nazwy interfejsu K extends keyof Tbędziesz miał do czynienia z funkcjami, które zwraca własność pod tą nazwą ... mianowicie funkcje typu () => T[K]. Innym sposobem na powiedzenie tego jest to, że będziemy używać keyofi wyszukiwać typy, aby pomóc w nadaniu typów w Twojej fabryce.

Będziemy używać tylko konkretnego typu, takiego jak {"SomeInterface": SomeInterface; "Date": Date}for T, ale w dalszej części kompilator ma łatwiejszy czas, jeśli Tjest ogólny. Oto możliwa ogólna implementacja ObjectFactorykreatora:

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;
    }
  }
}

Przebudowałem twoją wersję w coś, co kompilator może w większości zweryfikować jako bezpieczne dla typów, aby uniknąć asercji typu . Przejdźmy przez to.

makeFactoryFunkcja jest nazwą typu odwzorowania obiektu T, i przyjmuje argument o nazwie DEFAULT_IMPLEMENTATIONStypu { [K in keyof T]: () => T[K] }. Jest to typ mapowany, którego klucze Ksą takie same jak w przypadku, Tale którego właściwościami są funkcje typu zero-arg, które zwracają wartość typu T[K]. Możesz zobaczyć, jak DEFAULT_IMPLEMENTATIONSwyglądało to w ten sposób: każda właściwość była funkcją o zerowym argumencie, zwracającą wartość odpowiedniego interfejsu.

W ramach implementacji funkcji tworzymy MOCK_IMPLEMENTATIONS. Ta zmienna ma prawie taki sam typ jak, DEFAULT_IMPLEMENTATIONSale z właściwościami opcjonalnymi (na co wpływa modyfikator opcjonalności ?in [K in keyof T]?).

Funkcja zwraca samą fabrykę, która ma dwie metody:

getInstanceMetoda jest nazwą K, typu nazwie interfejsu i wartość powrotna jest typu T[K], odpowiednie właściwości interfejsu. Zaimplementować to poprzez łączenie DEFAULT_IMPLEMENTATIONSi MOCK_IMPLEMENTATIONSprzez obiekt rozprzestrzeniania i opisywania że to compositeInjectablejest tego samego typu jak DEFAULT_IMPLEMENTATIONS. Następnie indeksujemy go za pomocą interfaceNamei nazywamy go.

mockWithInstanceSposób jest niespecyficzny K, typ nazwą interfejsu i przyjmuje parametr typu K(nazwa interface) i parametr typu T[K](odpowiedniego interfejsu).


Zobaczmy to w akcji:

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

To wszystko działa tak, jak myślę, że się spodziewasz. Wykonujemy ObjectFactorydzwoniąc makeFactoryz żądanym DEFAULT_IMPLEMENTATIONSprzedmiotem. Tutaj dodałem adnotację, że SomeInterfacewłaściwość zwraca wartość typu SomeInterface(w przeciwnym razie kompilator wywnioskuje, Implementationktóra może być bardziej szczegółowa niż chcesz).

Następnie widzimy, że kompilator pozwala nam wywołać ObjectFactory.getInstance()i ObjectFactory.mockWithInstance()z odpowiednimi argumentami i zwrócić oczekiwane typy, a także działa w czasie wykonywania.


Link do kodu placu zabaw