第三方单例的依赖倒置(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
没有太多经验,但看起来 ApolloClient
和 InMemoryCache
的设置似乎是在您调用 new
。我担心的是事后您可能无法修改它们。出于这个原因,我建议您 import
将您的模块放入客户端文件,而不是相反。
当然,您的模块中会有依赖于 client
实例的方法,但我们可以使它们成为以 client
作为参数的函数。这允许我们在创建客户端之前编写方法。
我想出了一个基于 Builder Pattern 的设计。我们将每个模块定义为具有两个属性的 object
: policies
是策略定义的键值对象,符合 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
,即 ApolloClient
和 methods
,它是绑定到的方法的对象客户。
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
对象。请注意,您不能在多个模块中使用同名方法,因为它们不是嵌套的。
为了保持模块之间的松散耦合并防止依赖循环,我很难实现控制反转。
在我的示例中,我想为第三方库 (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
没有太多经验,但看起来 ApolloClient
和 InMemoryCache
的设置似乎是在您调用 new
。我担心的是事后您可能无法修改它们。出于这个原因,我建议您 import
将您的模块放入客户端文件,而不是相反。
当然,您的模块中会有依赖于 client
实例的方法,但我们可以使它们成为以 client
作为参数的函数。这允许我们在创建客户端之前编写方法。
我想出了一个基于 Builder Pattern 的设计。我们将每个模块定义为具有两个属性的 object
: policies
是策略定义的键值对象,符合 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
,即 ApolloClient
和 methods
,它是绑定到的方法的对象客户。
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
对象。请注意,您不能在多个模块中使用同名方法,因为它们不是嵌套的。