Зачем нужна инверсия управления IoC?

Бегло изучил и даже применил в своём учебном проекте инверсию управления, на TypeScript-e, с использованием пакета inversife. Но так и не понял зачем нужна инверсия управления, если и без неё можно написать рабочий код.

Если я правильно понял, нижеследующие термины, являющиеся частью концепции IoC значат следующее:

  • контейнер зависимостей - это объект, в котором хранятся экземпляры классов, которые используются внутри других классов;

  • декоратор - это по сути функция, которая с помощью специального синтаксиса (@inject) позволяет из контейнера зависимостей выдернуть нужный экземпляр класса и передать его в конструктор другого класса;

  • IoC (инверсия управления) - это принцип ООП позволяющий передать управление внедрением зависимостей, сторонним программам. Суть в том что если до этого мы вручную создавали экземпляры классов и передавали их конструктору, то при IoC, сторонняя библиотека(inversify) это делает за нас используя контейнер зависимостей и декораторы.

Чтобы было лучше понятно, какого рода у меня сомнения, поясню на примере. Есть некий класс ConfigService который зависит от другого класса LoggerService. И мы этот класс передаём через декоратор @inject:

@injectable()
export default class ConfigService implements ConfigInterface {
  private logger: LoggerInterface;

  constructor(@inject(Component.LoggerInterface) logger: LoggerInterface) {
    this.logger = logger;
  // далее какая-то внутренняя логика...
}

А можем сделать вот так:

export default class ConfigService implements ConfigInterface {
  private logger: LoggerInterface;

  constructor(logger: LoggerInterface) {
    this.logger = logger;
  // далее какая-то внутренняя логика...
}

И при вызове это класса передать ему экземпляр логера: new ConfigService (new LoggerService())

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


Ответы (1 шт):

Автор решения: CrazyElf
  1. Декоратор - это детали реализации IoC/DI, в общем-то специфичные для конкретного языка. Там где я с этим всем встречался, в C#, например, были просто обычные классы, с параметрами в виде интерфейсов в конструкторе класса, фреймворк конфигурился специальный образом и дальше сам понимал, где что внедрить, без декораторов и прочего (если правильно помню). Не зацикливайтесь на деталях реализации, это не суть важно.

  2. Почему плохо создавать нужные нам экземпляры в конструкторе класса? Мы не должны быть привязаны к деталям реализации. А создать экземпляр класса далеко не всегда просто. Нужно где-то взять данные, которые ему нужны для создания, нужно знать, от каких других экземпляров классов он зависит и предварительно создать эти экземпляры в нужном порядке и передать их ему. В общем, если случай не такой тривиальный как у вас, когда просто создаётся один класс, то всё может быть гораздо сложнее. А чтобы соблюдать принцип loose coupling нам лучше знать поменьше о других классах. Идеал - это когда мы знаем о классе только интерфейс. Инициализируется этот класс где-то снаружи, нам эти детали не нужны, а мы только дёргаем класс за известный нам интерфейс. Поменялось что-то в классе, он теперь как-то по-другому инициализируется, он зависит от каких-то других теперь классов? Да наплевать, мы этого ничего не знаем и радуемся жизни. Мы получаем в конструкторе готовый класс и вызываем нужные методы интерфейса, всё.

  3. Но дело не только в сложностях инициализации. Переходим к главному - к тестированию. И вот тут нам тоже помогает то, что благодаря DI фреймворку мы можем думать только о том, что нас интересует. Самое главное тут то, что мы можем подменять любой интерфейс на заглушку. К примеру, мы тестируем метод, который обращается к БД (через интерфейс), и, к примеру, складывает получаемые данные и выдаёт результат. Благодаря DI мы можем подсунуть этому классу вместо живой БД, которую нам пришлось бы для этого специально создавать, интерфейс, который выдаёт определённый нами набор данных и таким образом мы тестируем только логический функционал, который нас интересует в данный момент. Работу с БД мы, возможно, тоже тестируем, но в другом тесте, отдельно.

  4. Чем хороши отдельные тесты. Например, у вас есть класс с какой-то сложной логикой работы с сетью, БД, файлами и бог знает чем ещё. Если вы будете тестировать его как есть, ну, допустим, у вас тест не прошёл. На чём тест сломался? А бог его знает - может файловая система отвалилась, может веб-сервер недоступен, может таблица БД в дедлок попала, а может всё же с логикой у нашего класса проблема. Понять, что именно плохо работает - не всегда просто. В какой-то степени эти проблемы решаются грамотным рефакторингом, но всё-равно не всегда легко отделить логику от всего лишнего. Если же у нас есть DI, то мы можем протестировать даже очень зависимый от всего на свете класс, просто подменив ему все не интересующие нас в данный момент интерфейсы на обманки. И тогда у нас точно не может быть никаких проблем с файловой системой, БД, интернетом, поэтому мы точно знаем, что в данный момент тестируем именно логику класса. И если этот тест сломался - это проблемы с логикой, а не с чем-то ещё. И это очень удобно. Вы или ваши коллеги внесли изменения в код, после этого вы запускаете тесты, определённые тесты падают - и вы сразу точно знаете, логика какого именно класса сломалась. И вы точно знаете, что это не проблемы с железом или с какими-то сторонними компонентами - это проблема с работой конкретного кода в чистом виде. И это прекрасно.

→ Ссылка