Cara membuat Pabrik Objek dengan mempertahankan tipe
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 Base
antarmuka. 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 Injectable
is a hack untuk membuat kode itu berfungsi, idealnya, saya hanya ingin menggunakan nama implementasi, yaitu:
let DEFAULT_IMPLEMENTATIONS = {
SomeInterface: Implementation
}
Jawaban
Karena Anda memiliki daftar tetap dari pemetaan nama-ke-tipe yang perlu Anda dukung, pendekatan umum di sini adalah memikirkan jenis objek yang T
mewakili 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 T
bersifat generik. Berikut adalah kemungkinan implementasi generik dari ObjectFactory
pembuat:
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 makeFactory
fungsi generik di jenis pemetaan obyek T
, dan membawa argumen bernama DEFAULT_IMPLEMENTATIONS
tipe { [K in keyof T]: () => T[K] }
. Ini adalah tipe yang dipetakan yang kuncinya K
sama dengan yang dimiliki T
tetapi propertinya adalah fungsi zero-arg yang mengembalikan nilai tipe T[K]
. Anda dapat melihat bagaimana properti Anda DEFAULT_IMPLEMENTATIONS
saat 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_IMPLEMENTATIONS
tetapi 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 getInstance
metode adalah generik di K
, jenis nama interface, dan nilai kembali adalah tipe T[K]
, properti antarmuka yang sesuai. Saya menerapkan ini dengan menggabungkan DEFAULT_IMPLEMENTATIONS
dan MOCK_IMPLEMENTATIONS
melalui penyebaran objek , dan menjelaskan bahwa ini compositeInjectable
adalah tipe yang sama dengan DEFAULT_IMPLEMENTATIONS
. Kemudian kami mengindeksnya dengan interfaceName
dan menyebutnya.
The mockWithInstance
Metode 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 ObjectFactory
dengan memanggil makeFactory
dengan DEFAULT_IMPLEMENTATIONS
objek yang diinginkan . Di sini saya telah menjelaskan bahwa SomeInterface
properti mengembalikan nilai tipe SomeInterface
(jika tidak, kompilator akan menyimpulkan Implementation
mana 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