第三方单例的依赖倒置(IOC)

Dependency inversion (IOC) of third party singleton

为了保持模块之间的松散耦合并防止依赖循环,我很难实现控制反转。

在我的示例中,我想为第三方库 (Apollo Client) 动态注册配置。

当前实施

在我当前的实现中,存在依赖循环问题

文件夹结构

src
├── apolloClient.ts
├── modules
│   ├── messages
│   │   ├── ui.ts
│   │   └── state.ts
│   ├── users
│   │   ├── ui.ts
│   │   └── state.ts
...

apolloClient.ts

import { InMemoryCache, ApolloClient } from '@apollo/client'
import { messagesTypePolicies } from './modules/messages/state.ts'
import { usersTypePolicies } from './modules/users/state.ts'

export const client = new ApolloClient({
  cache: new InMemoryCache({
    typePolicies: {
      Query: {
        // register manually all modules
        ...messagesTypePolicies,
        ...usersTypePolicies
      }
    }
  }),
})

modules/messages/state.ts

import { client } from '../../apolloClient.ts'

export const messagesTypePolicies = {
  messagesList: {
    read() {
      return ['hello', 'yo']
    }
  }
}

export const updateMessage = () => {
  client.writeQuery(...) // Use of client, so here we have a dependency cycle issue
}

我想要什么

为了防止依赖循环和紧耦合,我希望每个模块都注册自己, 因此 apolloClient 单例不依赖于 UI 模块。

modules/messages/state.ts

import { client, registerTypePolicies } from '../../apolloClient.ts'

// Use custom function to register typePolicies on UI modules
registerTypePolicies({
  messagesList: {
    read() {
      return ['hello', 'yo']
    }
  }
})

export const updateMessage = () => {
  client.writeQuery(...) // Dependency cycle issue solved :)
}

apolloClient.ts

export const registerTypePolicies = () => {
  // What I am trying to solve using IOC pattern
  // Not sure how to dynamically update the singleton
}

我对 ApolloClient 没有太多经验,但看起来 ApolloClientInMemoryCache 的设置似乎是在您调用 new。我担心的是事后您可能无法修改它们。出于这个原因,我建议您 import 将您的模块放入客户端文件,而不是相反。

当然,您的模块中会有依赖于 client 实例的方法,但我们可以使它们成为以 client 作为参数的函数。这允许我们在创建客户端之前编写方法。

我想出了一个基于 Builder Pattern 的设计。我们将每个模块定义为具有两个属性的 objectpolicies 是策略定义的键值对象,符合 apollo 包中的 TypePolicies 类型; makeMethods 是一个函数,它接受 client 和 returns 函数的键值对象。我们使用该 returned 函数对象作为接口的通用对象,以便我们稍后可以获得该函数的确切定义。

type MethodFactory<MethodMap> = (
  client: ApolloClient<NormalizedCacheObject>
) => MethodMap;

interface Module<MethodMap> {
  policies: TypePolicies;
  makeMethods: MethodFactory<MethodMap>;
}

当我们将方法构建到 client 中时,我们会将所有策略传递给 cache。我们还将把新创建的 client 传递给 makeMethods 工厂和 return 方法,这些方法不再需要 client 因为它已经被注入了。

让我在这里暂停一下,我的代码的 policies 部分不太正确,因为我不完全理解政策应该是什么样子。所以我会把它留给你去弄清楚。 (docs link)

我们的 Builder 开始时没有任何设置。每次我们添加一个模块时,我们都会将设置与现有的 return 和一个新的 Builder 结合起来。最终我们调用 build,其中 return 是一个具有两个属性的对象:client,即 ApolloClientmethods,它是绑定到的方法的对象客户。


class Builder<MethodMap> {
  policies: TypePolicies;
  makeMethods: MethodFactory<MethodMap>;

  protected constructor(module: Module<MethodMap>) {
    this.makeMethods = module.makeMethods;
    this.policies = module.policies;
  }

  public static create(): Builder<{}> {
    return new Builder({ makeMethods: () => ({}), policies: {} });
  }

  public addModule<AddedMethods>(
    module: Module<AddedMethods>
  ): Builder<MethodMap & AddedMethods> {
    return new Builder({
      policies: {
        ...this.policies,
        ...module.policies
      },
      makeMethods: (client) => ({
        ...this.makeMethods(client),
        ...module.makeMethods(client)
      })
    });
  }

  public build(): {
    client: ApolloClient<NormalizedCacheObject>;
    methods: MethodMap;
  } {
    const client = new ApolloClient({
      cache: new InMemoryCache({
        typePolicies: this.policies
      })
    });
    const methods = this.makeMethods(client);

    return { client, methods };
  }
}

用法如下:

const messagesModule = {
  policies: {
    messagesList: {
      read() {
        return ["hello", "yo"];
      }
    } as TypePolicy // I don't know enough about TypePolicies to understand why this is needed
  },
  makeMethods: (client: ApolloClient<NormalizedCacheObject>) => ({
    updateMessage: (): void => {
      client.writeQuery({});
    }
  })
};

// some dummy data
const otherModule = {
  policies: {},
  makeMethods: (client: ApolloClient<NormalizedCacheObject>) => ({
    doSomething: (): number => {
      return 5;
    },
    somethingElse: (arg: string): void => {}
  })
};

// build through chaining
const { client, methods } = Builder.create()
  .addModule(messagesModule)
  .addModule(otherModule)
  .build();

// can call a method without any arguments
methods.updateMessage();

// methods with arguments know their correct argument type
methods.somethingElse('');

// can call any method on the client
const resolvers = client.getResolvers();

// can destructure methods from the map object
const {updateMessage, doSomething, somethingElse} = methods;

您可以从此处导出 client 并在您的整个应用程序中使用它。

您可以单独解构 methods 和导出方法,或者您可以只导出整个 methods 对象。请注意,您不能在多个模块中使用同名方法,因为它们不是嵌套的。