Зачем нужна инверсия управления 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 шт):
Декоратор - это детали реализации
IoC/DI, в общем-то специфичные для конкретного языка. Там где я с этим всем встречался, вC#, например, были просто обычные классы, с параметрами в виде интерфейсов в конструкторе класса, фреймворк конфигурился специальный образом и дальше сам понимал, где что внедрить, без декораторов и прочего (если правильно помню). Не зацикливайтесь на деталях реализации, это не суть важно.Почему плохо создавать нужные нам экземпляры в конструкторе класса? Мы не должны быть привязаны к деталям реализации. А создать экземпляр класса далеко не всегда просто. Нужно где-то взять данные, которые ему нужны для создания, нужно знать, от каких других экземпляров классов он зависит и предварительно создать эти экземпляры в нужном порядке и передать их ему. В общем, если случай не такой тривиальный как у вас, когда просто создаётся один класс, то всё может быть гораздо сложнее. А чтобы соблюдать принцип
loose couplingнам лучше знать поменьше о других классах. Идеал - это когда мы знаем о классе только интерфейс. Инициализируется этот класс где-то снаружи, нам эти детали не нужны, а мы только дёргаем класс за известный нам интерфейс. Поменялось что-то в классе, он теперь как-то по-другому инициализируется, он зависит от каких-то других теперь классов? Да наплевать, мы этого ничего не знаем и радуемся жизни. Мы получаем в конструкторе готовый класс и вызываем нужные методы интерфейса, всё.Но дело не только в сложностях инициализации. Переходим к главному - к тестированию. И вот тут нам тоже помогает то, что благодаря
DIфреймворку мы можем думать только о том, что нас интересует. Самое главное тут то, что мы можем подменять любой интерфейс на заглушку. К примеру, мы тестируем метод, который обращается к БД (через интерфейс), и, к примеру, складывает получаемые данные и выдаёт результат. БлагодаряDIмы можем подсунуть этому классу вместо живой БД, которую нам пришлось бы для этого специально создавать, интерфейс, который выдаёт определённый нами набор данных и таким образом мы тестируем только логический функционал, который нас интересует в данный момент. Работу с БД мы, возможно, тоже тестируем, но в другом тесте, отдельно.Чем хороши отдельные тесты. Например, у вас есть класс с какой-то сложной логикой работы с сетью, БД, файлами и бог знает чем ещё. Если вы будете тестировать его как есть, ну, допустим, у вас тест не прошёл. На чём тест сломался? А бог его знает - может файловая система отвалилась, может веб-сервер недоступен, может таблица БД в дедлок попала, а может всё же с логикой у нашего класса проблема. Понять, что именно плохо работает - не всегда просто. В какой-то степени эти проблемы решаются грамотным рефакторингом, но всё-равно не всегда легко отделить логику от всего лишнего. Если же у нас есть
DI, то мы можем протестировать даже очень зависимый от всего на свете класс, просто подменив ему все не интересующие нас в данный момент интерфейсы на обманки. И тогда у нас точно не может быть никаких проблем с файловой системой, БД, интернетом, поэтому мы точно знаем, что в данный момент тестируем именно логику класса. И если этот тест сломался - это проблемы с логикой, а не с чем-то ещё. И это очень удобно. Вы или ваши коллеги внесли изменения в код, после этого вы запускаете тесты, определённые тесты падают - и вы сразу точно знаете, логика какого именно класса сломалась. И вы точно знаете, что это не проблемы с железом или с какими-то сторонними компонентами - это проблема с работой конкретного кода в чистом виде. И это прекрасно.