【问题标题】:How to make an Object Factory maintaining the type如何使对象工厂保持类型
【发布时间】:2020-12-22 19:28:58
【问题描述】:

我创建了以下对象工厂来实例化我所有接口的实现:

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

我见过this question,但我不喜欢使用Base 接口的想法。此外,这种方法也不保留类型。

理想情况下,我想使用我的方法,但不必使用接口,即只使用接口的名称。

旁注:Injectable 的声明是使该代码工作的一种技巧,理想情况下,我希望能够仅使用实现名称,即:

let DEFAULT_IMPLEMENTATIONS = {
    SomeInterface: Implementation
}

【问题讨论】:

  • 是编译时已知的接口列表吗?或者你会随着时间的推移添加新的?我希望这里的代码是minimal reproducible example,其中唯一存在的问题是您要询问的问题,其中一些行显示了预期与实际的行为。现在为了解决这个问题,我必须处理 ConcreteImplementation 的任何内容,而且我没有在代码中看到用例。
  • 我已经编辑了示例,如果您需要更多信息,我已经减少了我的大学项目中的用例,我试图使用干净的架构创建解析器:github.com/antoniogamiz/manga-api/blob/master/src/modules/…
  • 你可以使用this之类的断言函数来“动态地”将接口添加到列表中,但它比使用固定列表更复杂和脆弱
  • 这些编辑使事情worse,因为它们有语法错误(大括号无效)和其他问题(new Implementation() 不是有效的() =&gt; unknown,没有名为getInstance() 的函数)。请考虑将示例代码放入 IDE 并在发布之前测试其行为。我现在必须退出,但我的问题仍然存在:您是否需要能够通过mockWithInstance()添加 接口,还是只使用现有接口的固定列表?前者在断言函数中是可能的,但后者更容易。
  • 抱歉,现在已修复。是的,我使用现有接口的固定列表。我导入它们并像 SomeInterface 一样添加它们。

标签: typescript design-patterns factory


【解决方案1】:

由于您有一个您关心支持的名称到类型映射的固定列表,因此这里的一般方法是考虑表示此映射的对象类型T,然后是任何受支持的接口名称@987654328 @,您将处理以该名称返回属性的函数...即() =&gt; T[K] 类型的函数。另一种说法是,我们将使用keyof and lookup types 来帮助为您的工厂提供类型。

对于T,我们只会使用像{"SomeInterface": SomeInterface; "Date": Date} 这样的具体类型,但是如果T 是泛型的,那么编译器会更容易处理。这是ObjectFactory maker 的可能通用实现:

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

我将您的版本重构为编译器大部分可以验证为类型安全的版本,以避免type assertions。让我们来看看吧。

makeFactory 函数在对象映射类型T 中是通用的,并采用类型为{ [K in keyof T]: () =&gt; T[K] } 的名为DEFAULT_IMPLEMENTATIONS 的参数。这是一个mapped type,其键KT 的键相同,但其属性是返回T[K] 类型值的零参数函数。你可以看到你现有的DEFAULT_IMPLEMENTATIONS 是这样的:每个属性都是一个零参数函数,返回相应接口的值。

在函数实现中,我们创建MOCK_IMPLEMENTATIONS。此变量与DEFAULT_IMPLEMENTATIONS 具有几乎相同的类型,但属性是可选(受[K in keyof T]? 中的可选修饰符? 的影响)。

函数返回工厂本身,有两种方法:

getInstance方法在接口名称类型K中是泛型的,返回值是对应接口属性T[K]类型。我通过object spread 合并DEFAULT_IMPLEMENTATIONSMOCK_IMPLEMENTATIONS 来实现这一点,并注释这个compositeInjectableDEFAULT_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

这一切都如我所料。我们通过使用所需的DEFAULT_IMPLEMENTATIONS 对象调用makeFactory 来创建ObjectFactory。这里我注释了SomeInterface 属性返回一个SomeInterface 类型的值(否则编译器会推断Implementation 可能比你想要的更具体)。

然后我们可以看到编译器允许我们调用ObjectFactory.getInstance()ObjectFactory.mockWithInstance() 并使用正确的参数并返回预期的类型,并且它也可以在运行时工作。


Playground link to code

【讨论】:

  • 首先,非常感谢您的精彩回答。我只是想知道是否有可能摆脱 SomeInterface: () => new SomeImplementation() hack。我的意思是,将可用接口声明为 SomeInterface: SomeImplementation 并在 makeFactory 函数中创建它们。
  • 您是在示例代码中编写SomeInterface: () =&gt; new Implementation() 的人,所以我不确定它到底是如何被破解的?您是否正在寻找类似this 的东西?如果不是,请详细说明,但请记住,这可能超出了本问题的范围,不适合在此处进一步讨论。
  • 这是一个 hack,因为我必须这样做才能使我的旧解决方案有效:/。我的意思只是使用 { SomeInterface: SomeImplementation} 作为 makeFactory 函数的参数。抱歉,我应该说这是问题中的一个技巧。
  • 我已经对问题进行了编辑,但这是我的错,所以请随意忽略我的请求,您已经帮了我很多忙。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2018-02-11
  • 2022-07-08
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多