유형을 유지하는 오브젝트 팩토리를 만드는 방법

Dec 23 2020

모든 인터페이스의 구현을 인스턴스화하기 위해 다음 개체 팩토리를 만들었습니다.

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
}

답변

1 jcalz Dec 23 2020 at 09:48

지원하려는 이름 대 유형 매핑의 고정 된 목록이 있으므로 여기서 일반적인 접근 방식 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 통해 이를 구현하고 이것이 같은 유형 이라는 주석을 달았습니다 . 그런 다음로 색인을 생성 하고 호출합니다.compositeInjectableDEFAULT_IMPLEMENTATIONSinterfaceName

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()적절한 인수와 예상되는 유형을 반환하고, 또한 런타임에서 작동합니다.


코드에 대한 플레이 그라운드 링크