##// END OF EJS Templates
Added tag v1.2.13 for changeset 0c74a0572161
Added tag v1.2.13 for changeset 0c74a0572161

File last commit:

r31:b8fd9db91f14 propose observables
r66:f14dd702c9ac v1.2.14 default
Show More
di.md
204 lines | 11.3 KiB | text/x-minidsrc | MarkdownLexer

SOLID и контейнеры

Рассмотрим простой пример того, как может быть построено приложения и почему приходится прибегать к такому сложному и непонятному решению как контейнеры.

Допустим у нас есть приложение, которое работает с комментариями к товару, и мы решили разделить логику и работу с источником данных, разнеся ее между контроллером и контекстом данных и тут возникает вопрос, как они должны взаимодействовать друг с другом.

// 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 - набор принципов ООП при разработки программного обеспечения

  • Single responsibility
  • Open-closed
  • Liskov substitution
  • Interface segregation
  • Dependency inversion

Использование данных принципов позволяет писать код, которыей будет обладать

  • повоторной используемостью
  • тестируемостью
  • читаемостью
  • поддаваться доработкам

IoC Контейнеры являются инструментом для построения ПО согласно принциам SOLID, как клей связывают друг с другом компоненты.

Контейнеры предоставляют стандарный механизм для описания и конструирования объектов, т.е. это некоторый объект в котором сосредоточена информация о сруктуре приложения и именно он должен создавать объекты и устанавливать между ними связи. Контейнер похож на шаблоны абстрактная фабрика (Abstract Factory) и строитель (Builder).

Для построения графа объектов контейнеру требуется конфигурация, которая состоит из набора дескрипторов сервисов, а также зависимостями между ними.

Сервисы регистрируются в контейнере, каждый сервис имеет свое имя, позволяющее получить его у контейнера.

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

Вложенные контейнеры

Контейнеры могут создаваться на основе уже существующих, так называемые дочерние контейнеры, они получают все сервисы описанные в родительском контейнере.

При изменении конфигурации дочернего контейнера - родительский контейнер останется без изменений. Использование дочерних контейнеров позволяет оптимизировать конфигурирование и дальнейшую работу с сервисами.

Примером такой оптимизации может служить веб-приложение, в котором загружается контейнер для всего приложения, а для каждого запроса создается свой дочерний контейнер, который донастраивается контроллером запроса. По окончанию выполнения запроса дочерний контейнр уничтожается, освобождая ресурсы.

Активация сервисов

Активация - процесс, когда контейнер в ответ на запрос выдает экземпляр сервиса. При обработке запроса на получение сервиса контейнер создает контекст активации, всю работу по получению экземпляра сервиса выполняет дескриптор, которому передается контекст активации.

  1. создается контекст активации,
  2. ищется запись декриптора сервиса в контейнере,
  3. дескриптору передается контекст активации,
  4. дескриптор возвращает экземпляр сервиса.

В процессе создания экземпляра сервиса, дескриптор может использовать контекст активации для обращения к контейнеру, текущим сервисам, а также может использовать его для активации других дескрипторов.

Типы активации сервисов

Тип активации относится к сервисам, в качестве которых были зарегистрированы либо типы, либо фабричные методы.

container.configure({
    foo: {
        $type: 'my/ServiceClass',
        activation: 'container'
    },
    bar: {
        $factory: () => {
            return new ServiceClass();
        },
        activation: 'context'
    }
});

call

Тип активации по-умолчанию, создается каждый раз, когда сервис запрашивается.

context

Экземпляр будет создан только один раз в рамках текущего контекста активации, т.е. при разрешении зависимостей будет использоваться все время один и тотже экземплар.

container

Будет создан только один экземпляр для контейнера, где сервис зарегистрирован. Созданный экземпляр сервиса будет автоматически очищен при освобождении контейнера.

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, но позволяет описать сервисы в родительском контейнере, а управлять временем жизни экземпляров этих сервисов при помощи дочерних контейнеров. Такой подход позволяет оптимизировать конфигурацию контейнеров.

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