Cara membuat Pabrik Objek dengan mempertahankan tipe

Dec 23 2020

Saya telah membuat pabrik objek berikut untuk membuat contoh implementasi dari semua antarmuka saya:

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

Seperti yang Anda lihat, Pabrik ini memungkinkan Anda memberi contoh dan mengejek antarmuka tersebut. Masalah utamanya adalah saya harus memanggil fungsi ini dengan nama antarmuka DAN antarmuka untuk mempertahankan tipe dalam tugas:

ObjectFactory.getInstance<SomeInterface>("SomeInterface")

Saya telah melihat pertanyaan ini , tetapi saya tidak suka ide menggunakan Baseantarmuka. Selain itu, pendekatan itu juga tidak mempertahankan tipe tersebut.

Idealnya, saya ingin menggunakan pendekatan saya tetapi tanpa harus menggunakan antarmuka, yaitu hanya menggunakan nama antarmuka.

Catatan tambahan: deklarasi Injectableis a hack untuk membuat kode itu berfungsi, idealnya, saya hanya ingin menggunakan nama implementasi, yaitu:

let DEFAULT_IMPLEMENTATIONS = {
    SomeInterface: Implementation
}

Jawaban

1 jcalz Dec 23 2020 at 09:48

Karena Anda memiliki daftar tetap dari pemetaan nama-ke-tipe yang perlu Anda dukung, pendekatan umum di sini adalah memikirkan jenis objek yang Tmewakili pemetaan ini, dan kemudian untuk nama antarmuka yang didukung K extends keyof T, Anda akan berurusan dengan fungsi-fungsi yang mengembalikan properti pada nama itu ... yaitu, fungsi tipe () => T[K]. Cara lain untuk mengatakan ini adalah bahwa kami akan menggunakan keyofdan jenis pencarian untuk membantu memberikan jenis ke pabrik Anda.

Kita hanya akan menggunakan tipe konkret seperti {"SomeInterface": SomeInterface; "Date": Date}for T, tetapi selanjutnya kompilator memiliki waktu yang lebih mudah jika Tbersifat generik. Berikut adalah kemungkinan implementasi generik dari ObjectFactorypembuat:

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

Saya mengubah versi Anda menjadi sesuatu yang sebagian besar dapat diverifikasi oleh kompilator sebagai tipe aman, untuk menghindari pernyataan tipe . Mari kita telusuri.

The makeFactoryfungsi generik di jenis pemetaan obyek T, dan membawa argumen bernama DEFAULT_IMPLEMENTATIONStipe { [K in keyof T]: () => T[K] }. Ini adalah tipe yang dipetakan yang kuncinya Ksama dengan yang dimiliki Ttetapi propertinya adalah fungsi zero-arg yang mengembalikan nilai tipe T[K]. Anda dapat melihat bagaimana properti Anda DEFAULT_IMPLEMENTATIONSsaat ini: setiap properti adalah fungsi zero-arg yang mengembalikan nilai antarmuka terkait.

Di dalam implementasi fungsi, kami membuat MOCK_IMPLEMENTATIONS. Variabel ini memiliki tipe yang hampir sama DEFAULT_IMPLEMENTATIONStetapi dengan propertinya bersifat opsional (seperti yang dipengaruhi oleh pengubah opsionalitas ?di [K in keyof T]?).

Fungsi mengembalikan pabrik itu sendiri, yang memiliki dua metode:

The getInstancemetode adalah generik di K, jenis nama interface, dan nilai kembali adalah tipe T[K], properti antarmuka yang sesuai. Saya menerapkan ini dengan menggabungkan DEFAULT_IMPLEMENTATIONSdan MOCK_IMPLEMENTATIONSmelalui penyebaran objek , dan menjelaskan bahwa ini compositeInjectableadalah tipe yang sama dengan DEFAULT_IMPLEMENTATIONS. Kemudian kami mengindeksnya dengan interfaceNamedan menyebutnya.

The mockWithInstanceMetode ini juga di generik K, jenis nama interface, dan menerima parameter tipe K(nama interface), dan parameter tipe T[K](yang sesuai interface).


Mari kita lihat aksinya:

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

Ini semua bekerja seperti yang saya pikir Anda harapkan. Kami membuat ObjectFactorydengan memanggil makeFactorydengan DEFAULT_IMPLEMENTATIONSobjek yang diinginkan . Di sini saya telah menjelaskan bahwa SomeInterfaceproperti mengembalikan nilai tipe SomeInterface(jika tidak, kompilator akan menyimpulkan Implementationmana yang mungkin lebih spesifik daripada yang Anda inginkan).

Kemudian kita dapat melihat bahwa kompilator memungkinkan kita memanggil ObjectFactory.getInstance()dan ObjectFactory.mockWithInstance()dengan argumen yang tepat serta mengembalikan tipe yang diharapkan, dan juga bekerja pada waktu proses.


Tautan taman bermain ke kode