유형을 유지하는 오브젝트 팩토리를 만드는 방법
모든 인터페이스의 구현을 인스턴스화하기 위해 다음 개체 팩토리를 만들었습니다.
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");
보시다시피이 팩토리를 사용하면 이러한 인터페이스를 인스턴스화하고 조롱 할 수 있습니다. 주된 문제는 할당에서 유형을 유지하기 위해 인터페이스 이름과 인터페이스로이 함수를 호출해야한다는 것입니다.
ObjectFactory.getInstance<SomeInterface>("SomeInterface")
내가 본 이 질문을 하지만, 내가 사용하는 아이디어 좋아하지 않는 Base
인터페이스를. 게다가 그 접근 방식은 유형도 유지하지 않습니다.
이상적으로는 내 접근 방식을 사용하고 싶지만 인터페이스를 사용할 필요없이, 즉 인터페이스 이름 만 사용합니다.
참고 :의 선언은 Injectable
해당 코드가 작동하도록하는 해킹입니다. 이상적으로는 구현 이름 만 사용할 수 있습니다. 즉,
let DEFAULT_IMPLEMENTATIONS = {
SomeInterface: Implementation
}
답변
지원하려는 이름 대 유형 매핑의 고정 된 목록이 있으므로 여기서 일반적인 접근 방식 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
함수는 개체 매핑 유형에서 일반적 이며 type T
이라는 인수 DEFAULT_IMPLEMENTATIONS
를 사용합니다 { [K in keyof T]: () => T[K] }
. 이것은이다 맵형 그 키들 K
과 동일하다 T
그 속성 유형 값을 반환 제로 ARG 기능하지만 T[K]
. 기존 항목 DEFAULT_IMPLEMENTATIONS
이 어떻게되었는지 확인할 수 있습니다 . 각 속성은 해당 인터페이스의 값을 반환하는 인수가 0 인 함수였습니다.
함수 구현 내에서 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()
적절한 인수와 예상되는 유형을 반환하고, 또한 런타임에서 작동합니다.
코드에 대한 플레이 그라운드 링크