Инверсия зависимостей (IOC) стороннего синглтона

#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.