Использование модулей для определения того, как должны быть построены объекты

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

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

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

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

Пример

Чтобы проиллюстрировать описанную выше ситуацию, вернемся к первому примеру части 1, где у нас было только три класса: WeatherReporter и две его зависимости, LocationManager и WeatherService. . Обе зависимости были конкретными классами. На практике это, вероятно, не так.

Предположим, вместо этого, что WeatherService - это интерфейс, и что у нас есть еще один класс, скажем, YahooWeather, который реализует этот интерфейс:

Если мы попытаемся снова скомпилировать проект, Dagger выдаст нам сообщение об ошибке, в котором говорится, что ему не удалось найти поставщика для WeatherService.

Когда класс конкретен и имеет аннотированный конструктор, Dagger может автоматически сгенерировать поставщика для этого класса. Однако, поскольку WeatherService является интерфейсом, мы должны предоставить Dagger дополнительную информацию.

Что такое модули?

На данный момент модули - это классы, которые могут создавать объекты определенных типов. Например, следующий модуль может создавать объекты WeatherService по запросу путем создания экземпляра класса YahooWeather.

Модули должны получить аннотацию @Module. Некоторые из их методов, известные как методы поставщика, получают аннотацию Provides, указывающую, что они могут предоставлять экземпляры определенного типа по запросу. Имена методов не важны: Dagger смотрит только на подписи.

С помощью этого определения модуля мы расширяем возможности Dagger по созданию объектов и удовлетворению зависимостей. Раньше в качестве зависимостей допускались только конкретные классы с аннотированными конструкторами, но теперь, с помощью этого модуля, любой класс может зависеть от нашего интерфейса WeatherService. Нам просто нужно подключить этот модуль к компоненту, который использует наша точка входа:

Теперь проект снова компилируется, и каждый WeatherReporter, созданный методом getWeatherReporter, получает экземпляр YahooWeather.

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

Как поменять местами модули

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

Предположим, например, что у нас есть другой конкретный класс, WeatherChannel,, который также реализует WeatherService. Если мы хотим использовать этот конкретный класс в нашем WeatherReporter вместо предыдущего YahooWeather, мы можем написать новый модуль WeatherChannelModule, а затем заменить предыдущий модуль этим в нашем компоненте, как показано ниже.

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

Если мы попытаемся подключить два разных модуля, которые предоставляют один и тот же тип, в компонент, Dagger выдаст ошибку компиляции, сообщая нам, что тип привязан несколько раз. Например, нам не разрешено это делать:

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

Строительство более сложных объектов

Теперь, когда мы знаем, что такое модули и как писать некоторые базовые, давайте посмотрим, как мы можем писать модули, которые создают более сложные объекты. Например, объекты, определенные третьими сторонами (чьи классы не могут быть изменены), которые имеют зависимости или требуют настройки.

Создание сторонних объектов

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

Предположим, например, что наш класс LocationManager зависит от GpsSensor. Однако этот класс был предоставлен Acme Corporation и не может быть изменен. Что еще хуже, конструктор этого класса не инициализирует его полностью. После создания экземпляра этого класса нам все еще нужно вызвать ряд методов, таких как calibrate, прежде чем объект можно будет фактически использовать. Ниже приведен исходный код для LocationManager и GpsSensor.

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

Мы не хотели бы, чтобы конструктор LocationManager вызывал метод calibrate, поскольку установка GpsSensor не входит в его обязанности. В идеале все полученные зависимости должны быть готовы к использованию. Этот экземпляр GpsSensor также может использоваться несколькими объектами, и корпорация Acme предупредила нас, что многократный вызов метода calibrate вызывает сбой.

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

Построение объектов с зависимостями

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

Предположим, например, что класс YahooWeather требует для работы WebSocket. Код для обоих этих классов показан ниже.

Теперь, когда конструктору YahooWeather требуется параметр, необходимо изменить наш YahooWeatherModule. Нам нужно каким-то образом получить экземпляр WebSocket, чтобы вызвать конструктор .

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

Строительные объекты, которые необходимо настроить

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

Продолжая наш пример, предположим, что конструктору YahooWeather также требуется ключ API. Вот модифицированный класс.

Обратите внимание, что мы удалили аннотацию Inject. Поскольку создание YahooWeather выполняется нашим модулем, Dagger не должен знать об этом конструкторе. Dagger также не знал бы, как автоматически ввести значимый параметр String (хотя это тоже можно сделать, как мы увидим в следующей публикации).

Если ключ API является константой, доступной во время компиляции где-то, скажем, в классе с именем BuildConfig, то возможное решение следующее:

Однако предположим, что ключ API доступен только во время работы. Например, это может быть аргумент командной строки. Эта информация может быть предоставлена ​​модулю, как и любая другая зависимость, через его конструктор.

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

В строках 3–4 мы взяли ключ API из аргументов командной строки и сами создали экземпляр модуля. В строках 5–7 мы попросили Dagger создать новый компонент, используя этот экземпляр нашего модуля. Вместо вызова метода create мы получили builder, передали экземпляр модуля и затем вызвали build.

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

Заключение

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

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