So erstellen Sie eine Object Factory, in der der Typ beibehalten wird

Dec 23 2020

Ich habe die folgende Objektfactory erstellt, um Implementierungen aller meiner Schnittstellen zu instanziieren:

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

Wie Sie sehen können, können Sie mit dieser Factory diese Schnittstellen instanziieren und verspotten. Das Hauptproblem ist, dass ich diese Funktion mit dem Namen der Schnittstelle UND der Schnittstelle aufrufen muss, um den Typ in den Zuweisungen beizubehalten:

ObjectFactory.getInstance<SomeInterface>("SomeInterface")

Ich habe diese Frage gesehen , aber die Idee, eine BaseSchnittstelle zu verwenden, gefällt mir nicht . Darüber hinaus behält dieser Ansatz den Typ auch nicht bei.

Im Idealfall möchte ich meinen Ansatz verwenden, ohne jedoch die Schnittstelle verwenden zu müssen, dh nur den Namen der Schnittstelle.

Randnotiz: Die Deklaration von Injectableist ein Hack, damit dieser Code funktioniert. Idealerweise möchte ich nur den Implementierungsnamen verwenden können, dh:

let DEFAULT_IMPLEMENTATIONS = {
    SomeInterface: Implementation
}

Antworten

1 jcalz Dec 23 2020 at 09:48

Da Sie eine feste Liste von Zuordnungen von Namen zu Typ haben, die Sie unterstützen möchten, besteht der allgemeine Ansatz darin, in Bezug auf den Objekttyp zu denken, der Tdiese Zuordnung darstellt, und sich dann für jeden unterstützten Schnittstellennamen K extends keyof Tmit Funktionen zu befassen, die Geben Sie die Eigenschaft unter diesem Namen zurück ... nämlich Funktionen vom Typ () => T[K]. Eine andere Möglichkeit, dies zu sagen, besteht darin, dass wir keyofTypen verwenden und nachschlagen , um Ihrer Fabrik Typen zu geben.

Wir werden nur einen konkreten Typ wie {"SomeInterface": SomeInterface; "Date": Date}für verwenden T, aber im Folgenden hat der Compiler eine einfachere Zeit, wenn er Tgenerisch ist. Hier ist eine mögliche generische Implementierung eines ObjectFactoryHerstellers:

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

Ich habe Ihre Version in etwas umgestaltet, das der Compiler meistens als typsicher überprüfen kann, um Typzusicherungen zu vermeiden . Lass uns durchgehen.

Die makeFactoryFunktion ist im Objektzuordnungstyp generisch Tund verwendet ein Argument mit dem Namen DEFAULT_IMPLEMENTATIONStype { [K in keyof T]: () => T[K] }. Dies ist ein zugeordneter Typ, dessen Schlüssel Kmit denen von identisch sind T, dessen Eigenschaften jedoch Null-Argument-Funktionen sind, die einen Wert vom Typ zurückgeben T[K]. Sie können sehen, wie Ihre vorhandene DEFAULT_IMPLEMENTATIONSEigenschaft war: Jede Eigenschaft war eine Null-Argument-Funktion, die einen Wert der entsprechenden Schnittstelle zurückgibt.

Innerhalb der Funktionsimplementierung erstellen wir MOCK_IMPLEMENTATIONS. Diese Variable hat fast den gleichen Typ wie, DEFAULT_IMPLEMENTATIONSjedoch sind die Eigenschaften optional (wie durch den Modifikator ?für die Optionalität in bewirkt [K in keyof T]?).

Die Funktion gibt die Factory selbst zurück, die zwei Methoden hat:

Die getInstanceMethode ist generisch in K, der Typ des Schnittstellennamens, und der Rückgabewert ist vom Typ T[K], der entsprechenden Schnittstelleneigenschaft. Ich implementiere dies, indem ich sie zusammenführe DEFAULT_IMPLEMENTATIONSund MOCK_IMPLEMENTATIONSüber Objekte verteile und annotiere, dass dies compositeInjectableder gleiche Typ ist wie DEFAULT_IMPLEMENTATIONS. Dann indizieren wir mit dem interfaceNameund nennen es.

Die mockWithInstanceMethode ist auch generisch in K, dem Typ des Schnittstellennamens, und akzeptiert einen Parameter vom Typ K(den Schnittstellennamen) und einen Parameter vom Typ T[K](die entsprechende Schnittstelle).


Lassen Sie es uns in Aktion sehen:

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

Das alles funktioniert so, wie ich denke, dass Sie es erwarten. Wir machen ObjectFactorydurch den Aufruf makeFactorymit dem gewünschten DEFAULT_IMPLEMENTATIONSObjekt. Hier habe ich kommentiert, dass die SomeInterfaceEigenschaft einen Wert vom Typ zurückgibt SomeInterface(andernfalls würde der Compiler schließen, Implementationwas spezifischer sein könnte, als Sie möchten ).

Dann können wir sehen, dass der Compiler uns mit den richtigen Argumenten aufrufen ObjectFactory.getInstance()und ObjectFactory.mockWithInstance()die erwarteten Typen zurückgeben lässt, und es funktioniert auch zur Laufzeit.


Spielplatz Link zum Code