#javascript #typescript #design-patterns #singleton #inversion-of-control
#javascript #typescript #шаблоны проектирования #синглтон #инверсия контроля
Вопрос:
У меня возникают трудности с реализацией инверсии управления, чтобы сохранить слабую связь между моими модулями, а также предотвратить цикл зависимостей.
В моем примере я хочу динамически зарегистрировать конфигурацию для сторонней библиотеки (клиент Apollo).
Текущая реализация
В моей текущей реализации у меня есть проблемы с циклом зависимостей
Структура папок
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
}
}
}),
})
модули / сообщения / 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
синглтон не зависел от модулей пользовательского интерфейса.
модули / сообщения / 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
}
Комментарии:
1. Я недостаточно знаком с Аполлоном, чтобы ответить на этот вопрос должным образом. Похоже, что все конфигурации для
ApolloClient
иInMemoryCache
создаются путем передачи конфигураций при вызовеnew
, поэтому я не уверен, сможете ли вы изменить их постфактум. Возможно, вы захотите пойти в другом направлении, где сообщения не зависят от клиента. Вы можете сделатьupdateMessage
be функциейclient
и передать ее клиенту при ее вызове.
Ответ №1:
У меня нет большого опыта работы с ApolloClient
, но похоже , что настройки для ApolloClient
и InMemoryCache
создаются путем передачи конфигураций при вызове new
. Меня беспокоит то, что вы, возможно, не сможете изменить их постфактум. По этой причине я предлагаю вам import
поместить ваши модули в клиентский файл, а не наоборот.
Конечно, в ваших модулях будут методы, которые зависят от client
экземпляра, но мы можем сделать их функциями, которые принимают в client
качестве аргумента. Это позволяет нам писать методы до того, как клиент был создан.
Я придумал дизайн, основанный на шаблоне Builder. Мы определяем каждый модуль как an object
с двумя свойствами: policies
является объектом типа ключ-значение определений политики, соответствующим TypePolicies
типу из пакета apollo; makeMethods
является функцией, которая принимает client
и возвращает объект типа ключ-значение функций. Мы используем этот возвращаемый объект functions в качестве общего для интерфейса, чтобы позже мы могли получить точные определения для функции.
type MethodFactory<MethodMap> = (
client: ApolloClient<NormalizedCacheObject>
) => MethodMap;
interface Module<MethodMap> {
policies: TypePolicies;
makeMethods: MethodFactory<MethodMap>;
}
Когда мы встроим методы в a client
, мы передадим все политики в cache
. Мы также передадим вновь созданный client
на makeMethods
фабрику и вернем методы, которые больше не требуют client
, потому что он уже введен.
Позвольте мне сделать паузу здесь, чтобы сказать, что что-то не совсем правильно с policies
частью моего кода, поскольку я не до конца понимаю, как должна выглядеть политика. Так что я оставлю это на ваше усмотрение. (ссылка на документы)
Наш Builder
запуск без настроек. Каждый раз, когда мы добавляем модуль, мы объединяем настройки с существующими и возвращаем новый Builder
. В конечном итоге мы вызываем build
which возвращает объект с двумя свойствами: 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 amp; 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
объект. Обратите внимание, что у вас не может быть методов с одинаковыми именами в нескольких модулях, потому что они не являются вложенными.
Комментарии:
1. Спасибо за ваш четкий ответ! Наконец, я получаю что-то действительно похожее на ваше решение. Как вы уже сказали, идея состоит в том, чтобы экспортировать класс «builder», а не обрабатывать создание экземпляра клиентского класса Apollo.