[Разработка веб-сайтов, JavaScript, Программирование, Совершенный код] Погружение во внедрение зависимостей (DI), или как взломать Матрицу

Автор Сообщение
news_bot ®

Стаж: 6 лет 3 месяца
Сообщений: 27286

Создавать темы news_bot ® написал(а)
03-Июн-2021 18:32

Давным-давно в далекой Галактике, когда сестры Вачовски еще были братьями, искусственный разум в лице Архитектора поработил человечество и создал Матрицу… Всем привет, это снова Максим Кравец из Holyweb, и сегодня я хочу поговорить про Dependency Injection, то есть про внедрение зависимостей, или просто DI. Зачем? Возможно, просто хочется почувствовать себя Морфеусом, произнеся сакраментальное: «Я не могу объяснить тебе, что такое DI, я могу лишь показать тебе правду».  
Постановка задачи
Вот. Взгляни на этих птиц. Существует программа, чтобы ими управлять. Другие программы управляют деревьями и ветром, рассветом и закатом. Программы совершенствуются. Все они выполняют свою собственную часть работы.Пифия
Фабула, надеюсь, всем известна —  есть Матрица, к ней подключены люди. Люди пытаются освободиться, им мешают Агенты. Главный вопрос —  кто победит? Но это будет в конце фильма, а мы с вами пока в самом начале. Так что давайте поставим себя на место Архитектора и подумаем, как нам создать Матрицу?Что есть программы? Те самые, которые управляют птицами, деревьями, ветром.Да просто всем нам знакомые классы, у каждого из которых есть свои поля и методы, обеспечивающие реализацию возложенной на этот класс задачи.Что нам нужно обеспечить для функционирования Матрицы? Механизм внедрения, или (внимание, рояль в кустах), инжекции (Injection) функционала классов, отвечающих за всю вышеперечисленную флору, фауну и прочие природные явления, внутрь Матрицы. Подождем, пока грузчики установят в кустах очередной музыкальный инструмент, и зададимся вопросом: а что произойдет с Матрицей после того, как мы в нее инжектируем нужный нам функционал? Все правильно — у нее появятся зависимости (Dependency) от внешних по отношению к ней классов.Пазл сложился? С одной стороны — да. Dependency Injection — это всего лишь механизм внедрения в класс зависимости от другого класса. С другой — что это за механизм, для чего он нужен и когда его стоит использовать? Первым делом, посмотрим на цитату в начале текста и обратим внимание на предложение: «Программы совершенствуются». То есть — переписываются. Изменяются. Что это означает для нас? Работа нашей Матрицы не должна зависеть от конкретной реализации класса зависимости. Кажется, ерунда какая-то — зависимость на то и зависимость, чтобы от нее зависеть! А теперь следите за руками. Мы внедряем в Матрицу не конкретную реализацию зависимости, а абстрактный контракт, и реализуем механизм предоставления конкретной реализации, соответствующей этому контракту! Остались сущие пустяки — понять, как же это все реализовать.Внедрение зависимости в чистом видеОставим романтикам рассветы и закаты, птичек и цветочки. Мы, человеки, должны вырваться из под гнета ИИ вообще и Архитектора в частности. Так что будем разбираться с реализацией DI и параллельно — освобождаться из Матрицы. Первая итерация. Создадим класс matrix, непосредственно в нем создадим агента по имени Смит, определим его силу. Там же, внутри Матрицы, создадим и претендента, задав его силу, после чего посмотрим, кто победит, вызвав метод whoWin():
class Matrix {
  agent = {
    name: 'Smith',
    damage: 10000,
  };
  human = {
    name: 'Cypher',
    damage: 100,
  };
  whoWin(): string {
    const result = this.agent.damage > this.human.damage
      ? this.agent.name
      : this.human.name;
    return result;
  }
}
const matrixV1 = new Matrix();
console.log(‘Побеждает ’, matrixV1.whoWin());
Да, Сайфер не самый приятный персонаж, да еще и хотел вернуться в Матрицу, так что на роль первого проигравшего подходит идеально.
Побеждает  Smith
Архитектор, конечно, антигерой в рамках повествования, но идея заставить его вручную внести каждого подключенного к Матрице в базовый класс, а потом еще отслеживать рождаемость-смертность и поддерживать код в актуальном состоянии — сродни путевке в ад. Тем более, что физически люди находятся в реальном мире, а в Матрицу проецируется только их образ. Хотите — называйте его аватаром. Мы программисты, нам ближе ссылка или инстанс. Перепишем наш код.
class Human {
  name;
  damage;
  constructor(name, damage) {
    this.name = name;
    this.damage = damage;
  }
  get name(): string {
    return this.name;
  }
  get damage(): number {
    return this.damage;
  }
}
class Matrix {
  agent = {
    name: 'Smith',
    damage: 10000,
  };
human;
  constructor(challenger) {
    this.human = challenger;
  }
  whoWin(): string {
    const result = this.agent.damage > this.human.damage
      ? this.agent.name
      : this.human.name;
    return result;
  }
Мы добавили класс Human, принимающий на вход конструктора имя и силу человека, и возвращающий их в соответствующих методах. Также мы внесли изменения в наш класс Матрицы — теперь информацию о претенденте на победу он получает через конструктор. Давайте проверим, сможет ли Тринити победить Агента Смита?
const Trinity = new Human('Trinity', 500);
const matrixV1 = new Matrix(Trinity);
console.log('Побеждает ', matrixV1.whoWin());
Увы, Тринити «всего лишь человек» (с), и ее сила по определению не может быть больше, чем у агента, так что итог закономерен.
Побеждает  Smith
Но стоп! Давайте посмотрим, что случилось с Матрицей? А случилось то, что класс Matrix и результаты его работы стал зависеть от класса Human! И нашему оператору, отправляющему Тринити в Матрицу, достаточно немного изменить код, чтобы обеспечить победу человечества!
class Human {

  get damage(): number {
    return this.damage * 1000;
  }
}
...Пьем шампанское и расходимся по домам? Чем плох подход выше? Тем, что класс Matrix ждет от зависимости challenger, передаваемой в конструктор, наличие метода damage, поскольку именно к нему мы обращаемся в коде. Но об этом знает Архитектор, создавший Матрицу, а не наш оператор! В примере — мы можем угадать. А если не знать заранее название метода? Может быть, надо было написать не damage, а power? Или strength? Инверсия зависимостейЗнакомьтесь! Dependency inversion principle, принцип инверсии зависимостей (DIP). Название, кстати, нередко сокращают, убирая слово «принцип» , и тогда остается только Dependency inversion (DI), что вносит путаницу в мысли новичков.Принцип инверсии зависимостей имеет несколько трактовок, мы приведем лишь две:
  • Модули верхних уровней не должны зависеть от модулей нижних уровней. Оба типа модулей должны зависеть от абстракций.
  • Абстракции не должны зависеть от деталей. Детали должны зависеть от абстракций.
Зачем он нам понадобился? Для того, чтобы обеспечить механизм соблюдения контракта для зависимости, о необходимости которого мы говорили при постановке задачи, и отсутствие которого в нашей реализации не дало нам пока наполнить бокалы шампанским.Давайте внедрим в наш класс Matrix некий абстрактный класс AbstractHuman, а конкретную реализацию в виде класса Human — попросим имплементировать эту абстракцию:
abstract class AbstractHuman {
  abstract get name(): string;
  abstract get damage(): number;
}
class Human implements AbstractHuman{
  name;
  damage;
  constructor(name, damage) {
    this.name = name;
    this.damage = damage;
  }
  get name(): string {
    return this.name;
  }
  get damage(): number {
    return this.damage;
  }
}
class Matrix {
  agent = {
    name: 'Smith',
    damage: 10000,
  };
human;
  constructor(challenger: AbstractHuman) {
    this.human = challenger;
  }
  whoWin(): string {
    const result = this.agent.damage > this.human.damage
      ? this.agent.name
      : this.human.name;
    return result;
  }
}
const Morpheus = new Human('Morpheus', 900);
const matrixV2 = new Matrix(Morpheus);
console.log('Побеждает ', matrixV2.whoWin());
Морфеуса жалко, но все же он — не избранный.
Побеждает  Smith
Вторая версия Матрицы пока что выигрывает, но что получилось на текущий момент? Класс Matrix больше не зависит от конкретной реализации класса Human — задачу номер один мы выполнили. Класс Human отныне точно знает, какие методы с какими именами в нем должны присутствовать — пока «контракт» в виде абстрактного класса AbstractHuman не будет полностью реализован (имплементирован) в конкретной реализации, мы будем получать ошибку. Задача номер два также выполнена.Порадуемся за архитектора, кстати! В новой реализации он может создать отдельный класс для мужчин, отдельный для женщин, если понадобится — сделать отдельный класс для брюнеток и отдельный для блондинок… Согласитесь, таким модульным кодом легче управлять.
В бою с Морфеусом побеждает  Smith
В бою с Тринити побеждает  Smith
Думаю, вы уже догадались, что должен сделать наш оператор, чтобы Нео все же победил. Напишем еще один класс для Избранного, слегка отредактировав его силу:
...
class TheOne implements AbstractHuman{
  name;
  damage;
  constructor(name, damage) {
    this.name = name;
    this.damage = damage;
  }
  get name(): string {
    return this.name;
  }
  get damage(): number {
    return this.damage * 1000;
  }
}

const Neo = new TheOne('Neo, 500);
const matrixV5 = new Matrix(Neo);
Свершилось!
В бою с Нео побеждает  Нео
Инверсия управления Давайте посмотрим, кто управляет кодом? В нашем примере мы сами пишем и класс Matrix, и класс Human, сами создаем инстансы и задаем все параметры. Мы управляем нашим кодом. Захотели — внесли изменения и обеспечили победу Тринити. Увы, по условиям мира, придуманного Вачовски, мы можем лишь вклиниваться в работу Матрицы, добавляя свои кусочки программного кода. Матрица управляет не только фантомами внутри себя, но и тем, как с ней работать извне!Возможно, авторы трилогии увлекались программированием, потому что ситуация целиком и полностью списана с реальности и даже имеет свое название — Inversion of Control (IoC). Когда программист работает в фреймворке, он тоже пишет только часть кода, отдельные модули (классы, в которые внедряются зависимости) или сервисы (классы, которые внедряются в модули как зависимости). Причем какие именно (порой вплоть до правил именования файлов) — решает фреймворк. Кстати, уже использованный нами выше DIP (принцип инверсии зависимостей) — одно из проявлений механизма IoC.К-контейнер н-нада? Последний шаг —  передача управления разрешением зависимостей. Кому и какой инстанс предоставить, использовать singleton или multiton — также решается не программистом (оператором), а фреймворком (Матрицей). Вариантов решения задачи множество, но все они сводятся к одной идее.
  • на верхнем уровне приложения создается глобальный объект,
  • в этом объекте регистрируется абстрактный интерфейс и класс, который его имплементирует,
  • модуль запрашивает необходимый ему интерфейс (абстрактный класс),
  • глобальный объект находит класс, имплементирующий данный интерфейс, при необходимости создает инстанс и передает его в модуль.
Конкретные реализации у каждого фреймворка свои: где-то используется Локатор сервисов/служб (Service Locator), где-то Контейнер DI, чаще называемый IoC Container. Но на уровне базовой функциональности отличия между подходами стираются до неразличимости.У нас есть класс, который мы планируем внедрить (сервис). Мы сообщаем фреймворку о том, что этот класс нужно отправить в контейнер. Наиболее наглядно это происходит в Angular —  мы просто вешаем декоратор Injectable.
@Injectable()
export class SomeService {}
Декоратор добавит к классу набор метаданных и зарегистрирует его в IoC контейнере. Когда нам понадобится инстанс SomeService, фреймворк обратится к контейнеру, найдет уже существующий или создаст новый инстанс сервиса и вернет его нам. Крестики-нолики, а точнее — плюсы и минусыПлюсы очевидны —  вместо монолитного приложения мы работаем с набором отдельных сервисов и модулей, не связанных друг с другом напрямую. Мы не завязаны на конкретную реализацию класса, более того, мы можем подменять их при необходимости просто через конфигурацию. За счет того, что большая часть «технического» кода отныне спрятана в недрах фреймворка, наш код становится компактнее, чище. Его легче рефакторить, тестировать, проводить отладку.Минусы —  написание рабочего кода требует понимания логики работы фреймворка, иначе проект превращается в набор «черных ящиков» с наклейками «я реализую такой-то интерфейс». Кроме того, за любое удобство в программировании приходится платить производительностью. В конечном итоге все всё равно сводится к обычному инстанцированию с помощью new, а дополнительные «обертки», реализующие за нас эту логику, требуют и дополнительных ресурсов.Вместо заключения, или как это использовать практически?Окей, если необходимость добавления промежуточного слоя в виде «контракта» более-менее очевидна, то где на практике нам может пригодиться IoC?  Кейс 1 — тестирование.
  • У вас есть модуль, который отвечает за оформление покупки в интернет-магазине. 
  • Функционал списания средств мы вынесем в отдельный сервис и внедрим его через DI. Этот сервис будет обращаться к реальному эквайрингу банка Х.
  • Нам нужно протестировать работу модуля в целом, но мы не готовы совершать реальную покупку при каждом тесте.
  • Решение — напишем моковый сервис, имплементирующий тот же контракт, что и «боевой», и для теста — через IoC будем вызывать моковую реализацию. 
Кейс 2 — расширение функционала.
  • Модуль — прежний, оформление покупки в интернет-магазине.
  • Поступает задача — добавить возможность оплаты не только в банке Х, но и в банке Y. 
  • Мы пишем еще один платежный сервис, реализующий взаимодействие с банком Y и имплементирующий тот же контракт, что и сервис банка X.
  • Плюсы — мы можем подменять банк, мы можем давать пользователю выбор, в какой банк платить. При этом мы никак не меняем наш основной модуль.
Кейс 3 — управление на уровне инфраструктуры.
  • Модуль — прежний.
  • Для production — работаем с «боевым» сервисом платежей.
  • Для разработки — подгружаем моковый вариант, симулирующий списание средств.
  • Для тестового окружения — пишем третий сервис, который будет симулировать списание средств, а заодно вести расширенный лог состояния.
Надеюсь, этот краткий список примеров вас убедил в том, что вопроса, использовать или не использовать DI, в современной разработке не стоит. Однозначно использовать. А значит —  надо понимать, как это работает. Надеюсь, мне удалось не только помочь Нео в его битве со Смитом, но и вам в понимании, как устроен и работает DI. Если есть вопросы или дополнения по теме, буду рад продолжить общение в комментариях. Напишите, о чем рассказать в следующей статье? А если хотите познакомиться с нами ближе, я всегда на связи в Телеграме @maximkravec
===========
Источник:
habr.com
===========

Похожие новости: Теги для поиска: #_razrabotka_vebsajtov (Разработка веб-сайтов), #_javascript, #_programmirovanie (Программирование), #_sovershennyj_kod (Совершенный код), #_di, #_dependency, #_dependency_injection, #_javascript, #_design_patterns, #_dip, #_razrabotka_vebsajtov (
Разработка веб-сайтов
)
, #_javascript, #_programmirovanie (
Программирование
)
, #_sovershennyj_kod (
Совершенный код
)
Профиль  ЛС 
Показать сообщения:     

Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы

Текущее время: 17-Май 08:51
Часовой пояс: UTC + 5