Всем привет, меня зовут Алексей, и я отвечаю за разработку android-приложений в Константе. У нас в компании есть несколько проектов с большим набором функций, часть из которых присутствует во всех (или, по крайней мере, во многих) разделах интерфейса приложения. Речь идет об авторизации (регистрация + вход), добавлении товаров в корзину, информации о балансе пользователя, уведомлениях о новых входящих сообщениях и т.д. В этой статье я расскажу, как наша команда воспользовалась одной фичей языка Kotlin в своих корыстных целях :) Вы увидите, что существует жизнь без наследования, и что любая задача может иметь несколько решений. В первую очередь статья может быть полезна начинающим разработчикам, которые уже познакомились с базовым принципами объектно-ориентированного программирования, такими как абстракция, инкапсуляция, наследование и полиморфизм, а также владеют основными библиотеками и инструментами, актуальными для современной Android-разработки (Android Navigation Components, Hilt, RecyclerView). Моя цель – показать вам, что существуют другие возможные приёмы и паттерны, а также объяснить почему любая задача может быть решена разными способами. Пример будет основан на паттерне MVVM и достаточно упрощен, чтобы сконцентрироваться на организации кода, связанного со сквозной логикой приложения. В частности, RecyclerView заменён на ScrollView + Linearlayout, все интеракторы являются моками намеренно. Давайте представим, что нам нужно отображать несколько экранов, таких как: каталог товаров, детальная информация по товару, новости, акции или что-либо еще. На каждом из этих экранов по задумке дизайнеров должна быть доступна корзина. Для начала добавим эту функциональность на экран каталога товаров: Когда мы добавим эту функциональность на экран детальной информации по товару из каталога, быстро станет очевидным, что столкнёмся с дублированием кода. Думаю, все прекрасно понимают, что дублирование кода — это плохо, наследование же способно эту проблему решить. Первое, что приходит в голову — создать базовый класс вьюмодели, в котором можно описать общую для этих двух экранов логику: Сделав такую заготовку, мы действительно избавимся от дублирования кода во вьюмоделях и упростим добавление функциональности корзины на новые экраны: Всё выглядит прекрасно, но у нас осталось дублирование кода в слое представления (во фрагментах). Почему бы не пойти тем же путём и не сделать базовый фрагмент: В целом уже всё работает, но в данной реализации есть некоторые проблемы. Базовый фрагмент надеется, что наследники будут иметь в верстке FloatingActionButton, причем именно с идентификатором bucket_fab, иначе всё молча перестанет работать, но и это еще не все сложности. Теперь давайте представим, что продуктологи/дизайнеры/заказчики решили добавить кнопки входа на все ключевые экраны в том случае, когда пользователь не авторизован. Следуя нашей прошлой логике, нужно делать базовые абстрактные BaseAuthControlsFragment и BaseAuthControlsViewModel: Теперь нужно унаследовать эти классы на тех экранах, на которых нам нужны кнопки. К сожалению, в Kotlin (как в и Java) недоступно множественное наследование и придётся делать каскадное наследование, чтобы получить обе функциональности на одном экране. Абсолютно непонятно, какой из этих фрагментов/вьюмоделей должен быть выше в иерархии наследования. Как всегда вопросов больше, чем ответов. К счастью, в ООП есть другие механизмы для построения классов и связей между ними. На ряду с наследованием существует ассоциация. Она, в свою очередь, бывает двух видов: Композиция — вариант ассоциации, при которой часть целого не может существовать вне главного объекта, объект А полностью управляет временем жизни объекта B. Агрегация — часть целого имеет своё время жизни, объект A получает ссылку на объект B извне и использует его. Попробуем применить агрегацию для совместного использования BaseCartViewModel и BaseAuthControlsViewModel У нас получилось использовать корзину и авторизацию в рамках одной вьюмодели, но всё еще много болейрплейт-кода. Вспоминаем, что создатели языка Kotlin уже решили эту проблему, добавив делегаты в язык. Определим интерфейсы этих двух функциональностей: Теперь вьюмодели наших экранов могут стать более чистыми: При этом мы всегда можем переопределить любой метод и добавить логи, аналитику и т.д., если это понадобится: Кажется, все проблемы во вьюмоделях решены. Вернёмся к фрагментам: отдельные куски кода, обрабатывающие данные от делегатов вьюмоделей, тоже можно вынести в отдельные классы и подключать с помощью композиции в нужные фрагменты — в этом случае фрагмент будет полностью управлять жизненным циклом делегата, его существование отдельно от фрагмента бессмысленно: Итоговый код примера доступен на github Такой способ организации кода может быть применён не только для переиспользования общей логики между несколькими экранами, но также для декомпозиции больших вьюмоделей, содержащих много частей, каждая из которых имеет свой алгоритм работы и зависимости. При таком подходе можно написать тесты отдельно на каждый компонент вьюмодели. Надеюсь, что наш опыт поможет новым и существующим проектам стать более чистыми и поддерживаемыми, а значит — стать лучше. Спасибо за внимание!