Как стать автором
Обновить

Ракету пустил и забыл. Или как заставить DI работать

Уровень сложностиПростой
Время на прочтение7 мин
Количество просмотров3.2K

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

Сегодня нужно написать простенький экран, который будет отображать список. Вы с огромным энтузиазмом начинаете реализовывать прекрасный список - каталог товаров магазина. Один запрос, один список. Все сделали красиво, фрагмент создался, подтянул из DI ViewModel, которая в свою очередь передала остальным слоям, чтоб загрузить данные по АПИ и закешировать их. Все эти компоненты правильно освобождаются, так как все это сделано как надо отдельным Субкомпонентом с отдельным скоупом.

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

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

Хочется, чтобы это все уничтожалось при выходе с экрана. Хранить на все приложение будет неправильно, памяти на все не хватит. Бизнес же напротив - потерпеть одну долгую загрузку может, но вот, если пользователь вышел с экрана и вернулся, нужно обязательно нужно показать тоже самое и без каких либо задержек. Сортировать и фильтровать приходится каждый раз долго и мучительно. Главное - нельзя завалить сервер запросами, может быть и такое, что пользователь часто выходит и заходит на экран с надеждой, что когда-нибудь загрузится, а приложение каждый раз сбрасывает предыдущий запрос и отправляет новый.

Не Google решение

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

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

Кажется выбранный DI фреймворк не особо облегчил ситуацию. В приложении также много зависимостей, разделенной логики, дополнительно могут быть различные реализации пересылки событий и сообщений по приложению. Может EventBus еще кто нибудь любит.

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

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

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

Тайник без ключа

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

Hidden text

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

Общаясь с ребятами, как-то неожиданно я для себя понял, что никто почти не задумается о смысле одного из основополагающих инструментов в jvm: о ссылках различных видов. Однажды меня даже прямо спросили, для чего же мы используем кроме Weak ссылки еще и Soft ссылки. Что ж после небольшого рассказа, он даже устроился к нам работать.

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

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

Ну зачем ты гугл, зачем ты так.

Решение на ладони

Добро пожаловать в Америку, ну а кто еще плывет, немного расскажем про работу jvm. Точнее про сборщик мусора.

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

  • Локальные переменные и объекты стека вызова

  • Активные треды

  • Статические переменные

  • Загруженные классы classloader'ом

  • Удерживаемые в jni локальные и глобальные переменные

Самое интересное начинается при использовании специальных ссылок. WeakReference к примеру можно использовать, когда объект особо не нужен, но его не хотелось бы потерять. Он будет хранить ссылку на объект вплоть до самого уничтожения сборщиком мусора. И никак мешать этой сборке мусора не будет. SoftReference позволяет использовать объект вплоть до конца, но не умирать за него. Если у jvm будет стоять вопрос памяти ребром, то ты как самый ответственный программист легко откажешься от любого нужного компонента, дабы не встретиться с "Out of Memory".

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

class ShowCaseViewModel : ViewModel {
    fun loadCatalog(){
        scope.launch { 
            // long request with caching. 
            // request prices
            // Collect data from few interactors
            // merge lists 
            // filter list 
            // sort list 
        }
    }
}

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

Stone, как система

Выбор положен, ты решил писать свою библиотеку DI. И даже определил принципы для нее. Прежде всего она не должна держать чужие компоненты. Очевидно, что жизненный цикл компонентов приложения определяется их держателями и теми, кто их использует. Так к примеру Activity удерживается Андройдом, View удерживается Activity, ViewModel удерживается вьшкой, и так до самого DataSource.

Hidden text

Если быть детальнее, то Activity удерживается в ActivityThread и в ApplicationThread.

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

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

class ShowCaseFragment : Fragment() {

    @Inject
    lateinit var viewModel: ShowCaseViewModel 

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        DI.inject(this)
    }

}

Неожиданно, правда. Не часто видишь, что ViewModel предоставляется без ViewProvider. Да, именно так все и должно быть. Нам не нужны больше дополнительные фабрики, провайдеры, менеджеры компонентов. Все потому, что DI это все предоставляет.

@Component
interface AppComponent {

    fun viewModelsModule(): ViewModelsModule

    fun inject(showCaseFragment: ShowCaseFragment)

}

@Module
interface ViewModelsModule {

    @Provide(cache = Provide.CacheType.Weak)
    fun provideShowCaseViewModel(): ShowCaseViewModel {
        return ShowCaseViewModel()
    }

}

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

Сложности применения

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

Первая, и самая важная - потеря состояния. Сразу переехать на новую идею не получится, даже наскоком с надеждой, что все протестируется - нет. Раньше все, что вы делали в компоненте, будем говорить о ViewModel, чудесным образом исчезнет с новым открытием экрана. Вы могли оставить там все что хотели, сейчас нет. Та же ViewModel вам может вернуться заново, и все что вы там повыставляли в переменных, все может сохраниться (а может и нет). Так что будте бдительны, теперь вам надо самостоятельно управлять ЖЦ своих компонентов и не надеятся, что за вас это сделает DI.

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

Сухой остаток

Мне кажется Kotlin Native еще не скоро завоюет рынок Android. Из мобильных операционных систем, предоставляющих нативную разработку сохранилась только одна. Windows Phone повяз в болотах. Отечественные ОС только пробиваются на свет.

У нас еще как минимум 10 лет насладится миром jvm, так давайте насладимся им сполна и не будем страдать постоянными утечками памяти. Имея мощные инструменты jit, резервного выделения памяти, сборщика мусора и возможности переиспользовать память мы на самом деле может поставить на лопатки нашего злого конкурента, так давайте сделаем это.

Заходи ко мне на проект Stone. Оставляй лайки и комментарии, и я продолжу рассказывать про DI, в работе.

Теги:
Хабы:
Рейтинг0
Комментарии21

Публикации

Истории

Работа

Java разработчик
296 вакансий

Ближайшие события

28 ноября
Конференция «TechRec: ITHR CAMPUS»
МоскваОнлайн
2 – 18 декабря
Yandex DataLens Festival 2024
МоскваОнлайн
11 – 13 декабря
Международная конференция по AI/ML «AI Journey»
МоскваОнлайн
25 – 26 апреля
IT-конференция Merge Tatarstan 2025
Казань