di.md
204 lines
| 11.3 KiB
| text/x-minidsrc
|
MarkdownLexer
|
|
r31 | # SOLID и контейнеры | |
| Рассмотрим простой пример того, как может быть построено приложения и почему приходится прибегать к такому сложному и непонятному решению как контейнеры. | |||
| Допустим у нас есть приложение, которое работает с комментариями к товару, и мы решили разделить логику и работу с источником данных, разнеся ее между контроллером и контекстом данных и тут возникает вопрос, как они должны взаимодействовать друг с другом. | |||
| ```ts | |||
| // BAD | |||
| class CommentsController { | |||
| _db : DataContext, | |||
| constructor(connectionString: string) { | |||
| // explicit dependency | |||
| this._db = new PgSqlDataContext(connectionString); | |||
| } | |||
| //... | |||
| } | |||
| // GOOD | |||
| class CommmentsController { | |||
| _db : DataContext, | |||
| constructor(dataContext: DataContext) { | |||
| // a dependency is passed through the constructor | |||
| this._db = dataContext; | |||
| } | |||
| //... | |||
| } | |||
| // the container will do all the work | |||
| // the container knows which DataContext to create | |||
| let commentsController = container.getService<CommmentsController>("commmentsController"); | |||
| //... | |||
| commmentsController.createComment({ text: "Hello, unfair world!" }); | |||
| ``` | |||
| Для ответа на вопрос, что такое хорошо и что такое - плохо существует *SOLID* - набор принципов ООП при разработки программного обеспечения | |||
| - **S**ingle responsibility | |||
| - **O**pen-closed | |||
| - **L**iskov substitution | |||
| - **I**nterface segregation | |||
| - **D**ependency inversion | |||
| Использование данных принципов позволяет писать код, которыей будет обладать | |||
| - повоторной используемостью | |||
| - тестируемостью | |||
| - читаемостью | |||
| - поддаваться доработкам | |||
| *IoC* Контейнеры являются инструментом для построения ПО согласно принциам SOLID, как клей связывают друг с другом компоненты. | |||
| Контейнеры предоставляют стандарный механизм для описания и конструирования объектов, т.е. это некоторый объект в котором сосредоточена информация о сруктуре приложения и именно он должен создавать объекты и устанавливать между ними связи. Контейнер похож на шаблоны *абстрактная фабрика (Abstract Factory)* и *строитель (Builder)*. | |||
| Для построения графа объектов контейнеру требуется конфигурация, которая состоит из набора дескрипторов сервисов, а также зависимостями между ними. | |||
| Сервисы регистрируются в контейнере, каждый сервис имеет свое имя, позволяющее получить его у контейнера. | |||
| ```js | |||
| await container.configure({ | |||
| db: { | |||
| $type : 'my/app/PgSqlDataContext', | |||
| params: { | |||
| host: 'localhost', | |||
| port: 5432 | |||
| } | |||
| }, | |||
| commmentsController: { | |||
| $type: 'my/app/CommmentsController', | |||
| params: {$dependency: 'db'} | |||
| } | |||
| }); | |||
| ``` | |||
| В приведенном примере в контейнере объявляются два сервиса, один для работы с базой, воторой - для работы с комментариями. Между сервисами устанавливается зависимость, при создании `commmentsController` ему в контроллер передается созданный экземпляр контекста данных. | |||
| Приложению остается только получить нужный сервис у контейнера и воспользоваться последним. | |||
| > **Правило:** объекты не должны знать про контейнер! | |||
| Очень важно соблюдать данное правило, поскольку если внутри класса будет использоваться контейнер, то он может запрашивать любые сервисы в любое время, что сильно усложнит отслеживание зависимостей. Исключением могут быть только объекты отвечающие за жизненный цикл приложения, которые создают и конфигурируют контейнер и не участвуют в бизнес-логике. | |||
| Конфигурация контейнера является асинхронной операцией, поскольку может привести к загрузке модулей, где находятся объявления типов. Полностью сконфигурированный контейнер позволяет получать сервисы уже синхронно, что упрощает работу с ним и не тратит дополнительные ресурсы на использование асинхронных операций. | |||
| ## Конфигурация | |||
| Контейнер представляет собой словарь дескрипторов содержащих информацию о сервисах, в роли сервисов можно зарегистрировать: | |||
| - типы | |||
| - фабричные методы | |||
| - существующие объекты и простые занчения | |||
| подробоное описание конфигурации контейнера [di-config.md](di-config.md) | |||
| ## Вложенные контейнеры | |||
| Контейнеры могут создаваться на основе уже существующих, так называемые дочерние контейнеры, они получают все сервисы описанные в родительском контейнере. | |||
| При изменении конфигурации дочернего контейнера - родительский контейнер останется без изменений. Использование дочерних контейнеров позволяет оптимизировать конфигурирование и дальнейшую работу с сервисами. | |||
| Примером такой оптимизации может служить веб-приложение, в котором загружается контейнер для всего приложения, а для каждого запроса создается свой дочерний контейнер, который донастраивается контроллером запроса. По окончанию выполнения запроса дочерний контейнр уничтожается, освобождая ресурсы. | |||
| ## Активация сервисов | |||
| Активация - процесс, когда контейнер в ответ на запрос выдает экземпляр сервиса. При обработке запроса на получение сервиса контейнер создает контекст активации, всю работу по получению экземпляра сервиса выполняет дескриптор, которому передается контекст активации. | |||
| 1. создается контекст активации, | |||
| 2. ищется запись декриптора сервиса в контейнере, | |||
| 3. дескриптору передается контекст активации, | |||
| 4. дескриптор возвращает экземпляр сервиса. | |||
| В процессе создания экземпляра сервиса, дескриптор может использовать контекст активации для обращения к контейнеру, текущим сервисам, а также может использовать его для активации других дескрипторов. | |||
| ### Типы активации сервисов | |||
| Тип активации относится к сервисам, в качестве которых были зарегистрированы либо типы, либо фабричные методы. | |||
| ```ts | |||
| container.configure({ | |||
| foo: { | |||
| $type: 'my/ServiceClass', | |||
| activation: 'container' | |||
| }, | |||
| bar: { | |||
| $factory: () => { | |||
| return new ServiceClass(); | |||
| }, | |||
| activation: 'context' | |||
| } | |||
| }); | |||
| ``` | |||
| #### call | |||
| Тип активации по-умолчанию, создается каждый раз, когда сервис запрашивается. | |||
| #### context | |||
| Экземпляр будет создан только один раз в рамках текущего контекста активации, т.е. при разрешении зависимостей будет использоваться все время один и тотже экземплар. | |||
| #### container | |||
| Будет создан только один экземпляр для контейнера, где сервис зарегистрирован. Созданный экземпляр сервиса будет автоматически очищен при освобождении контейнера. | |||
| ```ts | |||
| container.cofigure({ | |||
| db: { | |||
| $type : 'my/app/PgSqlDataContext', | |||
| activation: 'container', | |||
| params: { | |||
| host: 'localhost', | |||
| port: 5432 | |||
| } | |||
| } | |||
| }) | |||
| let db = container.getService<DataContext>('db'); | |||
| let db2 = childContainer.getService<DataContext>('db'); | |||
| //db === db2 | |||
| container.dispose(); // will dispose db | |||
| ``` | |||
| #### hierarchy | |||
| Будет создан только один экземпляр для контейнера, который создал контекст активации. Созданный экземпляр червиса будет автоматически очищен при освобождении контейнера. | |||
| Данный вариант похож на тип активации `container`, но позволяет описать сервисы в родительском контейнере, а управлять временем жизни экземпляров этих сервисов при помощи дочерних контейнеров. Такой подход позволяет оптимизировать конфигурацию контейнеров. | |||
| ```ts | |||
| container.cofigure({ | |||
| db: { | |||
| $type : 'my/app/PgSqlDataContext', | |||
| activation: 'hierarchy', | |||
| params: { | |||
| host: 'localhost', | |||
| port: 5432 | |||
| } | |||
| } | |||
| }) | |||
| let db = container.getService<DataContext>('db'); | |||
| let db2 = childContainer.getService<DataContext>('db'); | |||
| //db !== db2 | |||
| childContainer.dispose(); // will dispose db2, db will be left intact | |||
| ``` |
