Внедрение зависимостей в Swift: объяснение и примеры кода

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

Многие принципы разработки программного обеспечения, такие как DRY и SOLID имеют одну общую черту: они делают ваш код более понятным и модульным . Вместо того, чтобы создавать одну большую кучу кода, вы делите код на модули, которые взаимодействуют друг с другом. Внедрение зависимостей также помогает повысить модульность вашего кода.

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

Работа с зависимостями

Начнем с выяснения того, что такое зависимость. Зависимость — это объект, от которого зависит другой код. Давайте посмотрим на следующий пример:

protocol Propulsion {
    func move()
}
 
class Vehicle {
    var engine: Propulsion
 
    init() {
        engine = RaceCarEngine()
    }
 
    func forward() {
        engine.move()
    }
}

Представьте следующий сценарий:

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

Протокол Propulsion определяет одну функцию: move(). Когда класс хочет соответствовать протоколу, он должен реализовать эту функцию.

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

Когда экземпляр Vehicle будет инициализирован с помощью функции init(), экземпляр RaceCarEngine присваивается свойству engine. Затем, когда вызывается функция forward(), автомобиль вызывает функцию move() для объекта engine.

Протокол Propulsion определяет правила для двигателя, а внутри класса Vehicle двигатель инициализируется и используется для вызова функции move().

Вот код для автомобильного двигателя:

class RaceCarEngine: Propulsion {
    func move() {
        print("Врууум!")
    }
}

В классе RaceCarEngine мы реализум протокол Propulsion, а также пишем реализацию для требуемой функции move().

var car = Vehicle()
car.forward() // Врууум!

Мы определяем переменную car типа Vehicle и вызываем функцию forward() для движения автомобиля.

Зависимость — это класс RaceCarEngine внутри функции init() класса Vehicle. Класс Vehicle тесно связан с классом RaceCarEngine, потому что класс Vehicle непосредственно ссылается на класс RaceCarEngine в своем инициализаторе. Мы напрямую вызываем класс RaceCarEngine из Vehicle, поэтому класс Vehicle теперь зависит от функции RaceCarEngine. Это и есть зависимость.

Внедрение зависимостей в Swift

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

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

Вернемся к классу Vehicle из предыдущего раздела. Как мы можем использовать внедрение зависимостей для улучшения кода?

class Vehicle {
    var engine: Propulsion
 
    init(engine: Propulsion) {
        self.engine = engine
    }
 
    func forward() {
        engine.move()
    }
}

Это небольшое изменение, но оно имеет решающее значение. Во-первых, обратите внимание, что класс Vehicle теперь вообще не упоминает класс RaceCarEngine.

Вместо жесткого кодирования объекта RaceCarEngine в функцию init() Vehicle, мы добавиляем параметр в инициализатор. Параметр вызывает engine типа Propulsion, и он используется для установки свойства engine Vehicle при инициализации.

Вот как все работает:

let fastEngine = RaceCarEngine()
 
var car = Vehicle(engine: fastEngine)
car.forward() // Врууум!

В приведенном коде мы сначала создаем экземпляр RaceCarEngine. Затем инициализируем экземпляр Vehicle.

Объект fastEngine вводится в объект Vehicle с внешней стороны. Это и есть внедрение зависимости! Оба класса по-прежнему зависят друг от друга, но они больше не связаны друг с другом. Они модульные. Вы можете использовать один класс без другого.

Давайте посмотрим на еще один пример:

class RocketEngine: Propulsion {
    func move() {
        print("3-2-1... Старт!... Вперед!")
    }
}

Да, это ракетный двигатель. Так же, как RaceCarEngine, класс RocketEngine соответствует протоколу Propulsion.

Вы можете переключить двигатель гоночного автомобиля в своем автомобиле на ракетный двигатель:

let rocket = RocketEngine()
 
var car = Vehicle(engine: rocket)
car.forward() // 3-2-1... Старт!... Вперед!

В приведенном выше коде мы создали экземпляр RocketEngine. Когда Vehicle инициализируется, мы используем объект rocket с помощью инициализатора engine.

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

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

Вам всегда нужен протокол для внедрения зависимостей? Не обязательно Вы можете использовать одну из многих других функций Swift, таких как создание подклассов, дженериков и непрозрачных типов.

Когда использовать внедрение зависимости?

Внедрение зависимостей полезно в следующих сценариях:

  • Вы хотите изменить реализацию кода, к которому у вас нет доступа.
  • Вы хотите «смоделировать» ваш код во время разработки.
  • Вы хотите протестировать свой код.

Код, к которому вы не можете получить доступ

Вы работаете с кодом, к которому у вас нет доступа. Подумайте о фреймворках iOS или сторонней библиотеке.

Для этого у Cocoa Touch SDK есть удобное решение: делегирование. Делегирование — это шаблон проектирования, который позволяет классу передать некоторые из своих обязанностей экземпляру другого класса.

Во многих классах Cocoa Touch вы можете назначить свой собственный объект делегатом. Затем классы фреймворка вызывают функции для ваших объектов-делегатов. Поскольку у вас есть контроль над объектами-делегатами, вы можете изменить поведение классов фрейворка, не изменяя их код напрямую.

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

Рассмотрим следующий сценарий:

  • Вы работаете с другим разработчиком iOS. Другой разработчик отвечает за создание WebAPI класса, от которого вы зависите.
  • Вы согласны с тем, что WebAPI необходимо реализовать функцию getItems(), так что вы придумали протокол под названием API, который включает эту функцию.
  • Пока другой разработчик не закончит создание WebAPI, вы создаете свой собственный API — заглушку с именем FakeAPI. Данный API возвращает простой список элементов.
  • Вы используете внедрение зависимостей в своем коде, чтобы иметь возможность работать с любым объектом, который соответствует API.
  • Когда реальное WebAPI готово, вы переключаете классы и вводите нужный объект в свой код. Вам нужно всего лишь изменить 1-2 строчки кода.

Mock объекты

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

Предположим, у нас есть два типа двигателей:

  • Фактический ракетный двигатель.
  • Диагностический ракетный двигатель.

Диагностический ракетный двигатель отправляет данные во внешнюю службу. Когда он запустится, он отправит сообщение «Я запустился» в службу регистрации. Ракетный двигатель выполняет больше вычислений для генерации диагностических данных, поэтому он более ресурсоемкий.

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

Заглушки и mock-объекты похожи, но они разные. Заглушка может быть функцией или классом, которая имеет правильную подпись, но ничего не делает. Mock-объект является функцией или классом, который имеет имитацию реализации, как правило, гораздо более простую, чем реальная функция или класс.

  • Вы используете заглушки, чтобы быстро установить или изменить зависимость. Они ничего не делают, поэтому идея состоит в том, чтобы заменить их позже на настоящую реализацию.
  • Вы используете mock-объекты для замены зависимости компонентом, который является фальшивым или имеет реализацию, отличную от реальной.

Распространенным сценарием mock-объектов является генерация диагностических данных. Например, у вас есть класс Log, который собирает данные регистрации в текстовом файле при разработке приложения. В процессе разработки вы не хотите ничего регистрировать в текстовом файле — или вообще что-либо регистрировать — поэтому вы заменяете компонент Log mock-объектом.

Модульное тестирование (Unit Testing)

Модульное тестирование — это практика разработки программного обеспечения, которая проверяет вывод кода на основании заданного ввода и сравнивает его с заранее заданным значением. Когда выходные данные соответствуют предопределенному значению, тест считается успешным. Вы знаете, что реализация, модуль, который вы тестируете, соответствует ожиданиям.

Сравните это с простым калькулятором. Вы проверяете, правильно ли работает функция сложения, вычисляя 1 + 1. Когда результат получается 2, вы знаете, что он работает, а когда результат другой, вы знаете, что функция не работает.

Это полезно, если вы собираетесь изменить реализацию некоторого кода. Сначала вы разрабатываете тест и запускаете его в соответствии с вашим текущим кодом, чтобы убедиться в правильности теста. Затем вы меняете проверенный код, который называется Unit Under Test и снова его запускаете.

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

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

При модульном тестировании вы обычно заменяете компоненты кода на mock-объекты или заглушки. Представьте, что вы хотите проверить WebAPI. Вместо загрузки элементов из интернета код сетевых запросов заменяется более простыми альтернативами, которые считывают те же данные из локального файла. Затем вы можете проверить полученные данные.

Подходы для внедрения зависимости

Вы можете внедрить зависимости в Swift двумя способами:

  1. Внедрение инициализатора: предоставление зависимости через инициализатор init().
  2. Внедрение свойства: обеспечить зависимость через свойство (или setter).

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

  • Рекомендуется использовать внедрение инициализатора для зависимости, которая не изменяется в течение времени жизни зависимого объекта.
  • Когда зависимость может измениться в течение времени жизни зависимого объекта или когда он в процессе инициализации становится nil, лучше использовать внедрение свойства.

Внедрение инициализатора

Ранее мы использовали внедрение инициализатора для предоставления объекта RocketEngine экземпляру класса Vehicle:

let car = Vehicle(engine: rocketEngine)

В приведенном коде мы используем функцию инициализатора Vehicle(engine:), чтобы обеспечить зависимость для экземпляра Vehicle.

Внедрение свойства

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

var car = Vehicle()
car.engine = rocketEngine

В приведенном выше коде мы передаем объект rocketEngine в экземпляр Vehicle с помощью свойства.

Внедрение метода

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

car.setEngine(rocketEngine)

Внедрение метода гораздо менее распространено в Swift, чем внедрение свойства или инициализатора. Сеттер-функции как выше обычно не используются. Вместо этого мы обычно используем свойство для непосредственного назначения объекта.

Читайте также:
Добавить комментарий

Ваш адрес email не будет опубликован.