Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Властелин модулей. Ульяновск

Властелин модулей. Ульяновск

8f1a6a9f39a0f45343eeff3bafa0bb15?s=128

Alexander Blinov

April 27, 2019
Tweet

Transcript

  1. M ODULES L ORD OF THE THE 2019

  2. Александр БЛИНОВ

  3. 1 Обзор проблем 2 Картина “TO BE” 3 Переход от

    “AS IS” к “TO BE” 4 Подведение итогов Содержание bit.ly/2XHDcHK Слайды
  4. Думайте своей головой и никому не доверяйте Disclaimer Photo by

    jesse orrico on Unsplash
  5. Обзор проблем и решений

  6. Изоляция фичей Touched by His Noodly Appendage, a parody of

    Michelangelo's The Creation of Adam, is an iconic image of the Flying Spaghetti Monster[1] by Arne Niklas Jansson.
  7. Время сборки The Persistence of Memory Painting by Salvador Dali

  8. Photo by Mohit Tomar on Unsplash Картина “TO BE”

  9. Разбиение на фиче-модули 1 В фиче доступны все необходимые зависимости

    2 Легко отделить и тиражировать фичу для экспериментов 3 Наличие механизм навигации (диплинки / апплинки) 4 Низкая связность фичей Ожидания от подхода Стабильная работа системы 5
  10. Bussines vs Core Business Core •Логическая законченность •Максимальная независимость •Четкие

    внешние зависимости Решает конкретную бизнес задачу Практически не относится к бизнес логике приложения • Авторизация • Профиль • Резюме • Список вакансий • Base UI • List & Pagination • Metrics & Analytics • Network
  11. Слои модулей Feature 1 Feature 2 Feature 3 Feature 4

    App 1 App 2 Core Feature 2 Core Feature 3 Core Features Business Features App Core Feature 1
  12. External Deps Internal implementation Feature API Устройство business feature

  13. External Internal API External Internal API External Internal API Взаимодействие

    через Mediator External Deps Internal implementation Feature API ?
  14. External Internal API External Internal API External Internal API Взаимодействие

    через Mediator External Deps Internal implementation Feature API APP Mediator Deps
  15. НЕЛЬЗЯ ПРОСТО ЗАПИЛИТЬ МОДУЛИ ТАК Кадр из трилогии “Властелин колец”

  16. Пример работы Feature Position

  17. External Internal API APP Mediator External Internal API Deps MainSearchScreen

    PositionScreen Инициализация
  18. External Internal API APP Mediator External Internal API Deps interface

    PositionDependencies { fun getCurrentPosition(): String fun setCurrentPosition(position: String) }
  19. External Internal API APP Mediator External Internal API Deps interface

    FeatureApi { fun getPositionFragment(): Fragment }
  20. External Internal API APP Mediator External Internal API Deps interface

    FeatureApi { fun getPositionFragment(): Fragment } interface FeatureApi { fun getPositionFragment(): Fragment //Допустимые внешние зависимости fun getSomeIntent(): SomeIntent fun getSomeInteractor(): SomeInteractor fun getSomeRepository(): SomeRepository // … }
  21. External Internal API APP Mediator External Internal API Deps class

    PositionComponent(positionDependencies: PositionDependencies) { companion object { private const val ROOT_SCOPE = “${BuildConfig.APPLICATION_ID}_POSITION” } init { Toothpick.openScopes(APP_ROOT_SCOPE, APP_SCOPE, ROOT_SCOPE) .installModules(DependenciesModule(positionDependencies)) } fun destroyComponent() { Toothpick.closeScope(ROOT_SCOPE) } val api: FeatureApi = FeatureApiIml(ROOT_SCOPE) }
  22. External Internal API APP Mediator External Internal API Deps class

    PositionComponent(positionDependencies: PositionDependencies) { companion object { private const val ROOT_SCOPE = “${BuildConfig.APPLICATION_ID}_POSITION” } init { Toothpick.openScopes(APP_ROOT_SCOPE, APP_SCOPE, ROOT_SCOPE) .installModules(DependenciesModule(positionDependencies)) } fun destroyComponent() { Toothpick.closeScope(ROOT_SCOPE) } val api: FeatureApi = FeatureApiIml(ROOT_SCOPE) }
  23. External Internal API APP Mediator External Internal API Deps class

    PositionComponent(positionDependencies: PositionDependencies) { companion object { private const val ROOT_SCOPE = “${BuildConfig.APPLICATION_ID}_POSITION” } init { Toothpick.openScopes(APP_ROOT_SCOPE, APP_SCOPE, ROOT_SCOPE) .installModules(DependenciesModule(positionDependencies)) } fun destroyComponent() { Toothpick.closeScope(scopeName) } val api: FeatureApi = FeatureApiIml(ROOT_SCOPE) }
  24. PositionMediator PositionComponent API External Internal Stub API External Internal API

    APP Mediator External Internal API Deps MainSearchScreenMediator Component Holder MainSearchScreenComponent API External Stub API Internal
  25. PositionMediator PositionComponent API External Internal Stub API PositionComponent API External

    Internal External Internal API APP Mediator External Internal API Deps MainSearchScreenMediator Component Holder MainSearchScreenComponent API External Stub API Internal
  26. PositionMediator PositionComponent API External Internal Component Holder Stub API PositionComponent

    API External Internal External Internal API APP Mediator External Internal API Deps MainSearchScreenMediator Component Holder MainSearchScreenComponent API External Stub API Internal
  27. External Internal API APP Mediator External Internal API Deps class

    PositionMediator(private val mediatorManager : MediatorManager) { private val componentHolder: SingleComponentHolder<PositionComponent, PositionDependencies> = SingleComponentHolder { deps -> PositionComponent(deps) } private fun provideComponent(): PositionComponent { if(!componentHolder.hasComponent()){ componentHolder.initComponent(object : PositionDependencies { override fun getCurrentPosition(): String { mediatorManager.mainSearchMediator.apiStub.positionInteractor.getPosition() } override fun setCurrentPosition(p: String) { mediatorManager.mainSearchMediator.apiStub.positionInteractor.setPosition(p) } }) } return componentHolder.provideComponent(deps) } val apiStub = object : FeatureApi { override fun getPositionFragment(): Fragment { return provideComponent().api.getPositionFragment() } } // ... }
  28. External Internal API APP Mediator External Internal API Deps class

    PositionMediator(private val mediatorManager : MediatorManager) { private val componentHolder: SingleComponentHolder<PositionComponent, PositionDependencies> = SingleComponentHolder { deps -> PositionComponent(deps) } private fun provideComponent(): PositionComponent { if(!componentHolder.hasComponent()){ componentHolder.initComponent(object : PositionDependencies { override fun getCurrentPosition(): String { mediatorManager.mainSearchMediator.apiStub.positionInteractor.getPosition() } override fun setCurrentPosition(p: String) { mediatorManager.mainSearchMediator.apiStub.positionInteractor.setPosition(p) } }) } return componentHolder.provideComponent(deps) } val apiStub = object : FeatureApi { override fun getPositionFragment(): Fragment { return provideComponent().api.getPositionFragment() } } // ... }
  29. External Internal API APP Mediator External Internal API Deps class

    PositionMediator(private val mediatorManager : MediatorManager) { private val componentHolder: SingleComponentHolder<PositionComponent, PositionDependencies> = SingleComponentHolder { deps -> PositionComponent(deps) } private fun provideComponent(): PositionComponent { if(!componentHolder.hasComponent()){ componentHolder.initComponent(object : PositionDependencies { override fun getCurrentPosition(): String { mediatorManager.mainSearchMediator.apiStub.positionInteractor.getPosition() } override fun setCurrentPosition(p: String) { mediatorManager.mainSearchMediator.apiStub.positionInteractor.setPosition(p) } }) } return componentHolder.provideComponent(deps) } val apiStub = object : FeatureApi { override fun getPositionFragment(): Fragment { return provideComponent().api.getPositionFragment() } } // ... }
  30. External Internal API APP Mediator External Internal API Deps class

    PositionMediator(private val mediatorManager : MediatorManager) { private val componentHolder: SingleComponentHolder<PositionComponent, PositionDependencies> = SingleComponentHolder { deps -> PositionComponent(deps) } private fun provideComponent(): PositionComponent { if(!componentHolder.hasComponent()){ componentHolder.initComponent(object : PositionDependencies { override fun getCurrentPosition(): String { mediatorManager.mainSearchMediator.apiStub.positionInteractor.getPosition() } override fun setCurrentPosition(p: String) { mediatorManager.mainSearchMediator.apiStub.positionInteractor.setPosition(p) } }) } return componentHolder.provideComponent(deps) } val apiStub = object : FeatureApi { override fun getPositionFragment(): Fragment { return provideComponent().api.getPositionFragment() } } // ... }
  31. External Internal API APP Mediator External Internal API Deps class

    PositionMediator(private val mediatorManager : MediatorManager) { private val componentHolder: SingleComponentHolder<PositionComponent, PositionDependencies> = SingleComponentHolder { deps -> PositionComponent(deps) } private fun provideComponent(): PositionComponent { if(!componentHolder.hasComponent()){ componentHolder.initComponent(object : PositionDependencies { override fun getCurrentPosition(): String { mediatorManager.mainSearchMediator.apiStub.positionInteractor.getPosition() } override fun setCurrentPosition(p: String) { mediatorManager.mainSearchMediator.apiStub.positionInteractor.setPosition(p) } }) } return componentHolder.provideComponent(deps) } val apiStub = object : FeatureApi { override fun getPositionFragment(): Fragment { return provideComponent().api.getPositionFragment() } } // ... }
  32. External Internal API APP Mediator External Internal API Deps class

    PositionMediator(private val mediatorManager : MediatorManager) { private val componentHolder: SingleComponentHolder<PositionComponent, PositionDependencies> = SingleComponentHolder { deps -> PositionComponent(deps) } private fun provideComponent(): PositionComponent { if(!componentHolder.hasComponent()){ componentHolder.initComponent(object : PositionDependencies { override fun getCurrentPosition(): String { mediatorManager.mainSearchMediator.apiStub.positionInteractor.getPosition() } override fun setCurrentPosition(p: String) { mediatorManager.mainSearchMediator.apiStub.positionInteractor.setPosition(p) } }) } return componentHolder.provideComponent(deps) } val apiStub = object : FeatureApi { override fun getPositionFragment(): Fragment { return provideComponent().api.getPositionFragment() } } // ... }
  33. class PositionMediator(private val mediatorManager : MediatorManager) { private val componentHolder:

    SingleComponentHolder<PositionComponent, PositionDependencies> = SingleComponentHolder { deps -> PositionComponent(deps) } private fun provideComponent(): PositionComponent { if(!componentHolder.hasComponent()){ componentHolder.initComponent(object : PositionDependencies { override fun getCurrentPosition(): String { mediatorManager.mainSearchMediator.apiStub.positionInteractor.getPosition() } override fun setCurrentPosition(p: String) { mediatorManager.mainSearchMediator.apiStub.positionInteractor.setPosition(p) } }) } return componentHolder.provideComponent(deps) } val apiStub = object : FeatureApi { override fun getPositionFragment(): Fragment { return provideComponent().api.getPositionFragment() } } // ... } PositionMediator External Internal API APP Mediator External Internal API Deps Component Holder PositionComponent API External Stub API Internal apiStub SingleComponentHolder PositionComponent PositionDependencies
  34. Dependency Injection Photo by Hush Naidoo on Unsplash

  35. Какой Dependency Injection Фреймворк Подойдет лучше всего? Photo by Aziz

    Acharki on Unsplash
  36. External Internal API APP Mediator External Internal API Deps Устройство

    DI External Deps Internal implementation Feature API Common Deps
  37. Feature Position Some Else Feature AppRootScope RootPositionScope AppScope RootSomeElseScope SomeElsePositionScope

    AbcScope GhiScope JklScope DefScope Глубина скоупов зависимостей External Deps Common Deps
  38. class AutosearchFragment : AutosearchView { @InjectPresenter lateinit var presenter: AutosearchPresenter

    @ProvidePresenter fun providePresenter(): AutosearchPresenter { return AutosearchDI.openAutosearchScope().getInstance(AutosearchPresenter::class.java) } DI внутри модуля
  39. Скоупы зависимостей в модулях AutosearchDI Other App part Instal Dependencies

    Open Scope Инициализация & хранение 1 0 AutosearchFragment
  40. Скоупы зависимостей в модулях AutosearchDI AutosearchFragment Kill static Open Scope

    Переоткрытие System 1 0
  41. Скоупы зависимостей в модулях AutosearchDI AutosearchFragment Kill static Open Scope

    Переоткрытие System Force Initializer 1 Need Autosearch Deps 2 0
  42. Скоупы зависимостей в модулях AutosearchDI Other App part AutosearchFragment Kill

    static Open Scope Переоткрытие System Force Initializer 1 Need Autosearch Deps 2 Init Autosearch Deps 3 0
  43. Скоупы зависимостей в модулях AutosearchDI Other App part AutosearchFragment Kill

    static Open Scope Переоткрытие System Force Initializer 1 Need Autosearch Deps 2 Init Autosearch Deps 3 0 4 Instal Dependencies
  44. Photo by Alexander Andrews on Unsplash Навигация в приложении

  45. Внутри модуля Между модулями Старина Cicerone Smart Router A B

    State Machine C Навигация Через Dependencies Средства Android A B
  46. Внутри модуля Между модулями Старина Cicerone Smart Router A B

    State Machine C Навигация Через Dependencies Средства Android A B goo.gl/Tnqw2a goo.gl/tDmzEF
  47. External Internal API APP Mediator External Internal API Deps interface

    PositionDependencies { fun getCurrentPosition(): String fun setCurrentPosition(position: String) fun onClosePositionScreen() } interface PositionDependencies { fun getCurrentPosition(): String fun setCurrentPosition(position: String) fun onClosePositionScreen() } Закрытие фичи
  48. interface OnBackPressedListener { /** * @return true, если команда была

    обработана */ fun onBackPressed(): Boolean } interface OnBackPressable { fun addOnBackPressedListener(listener: OnBackPressedListener) fun removeOnBackPressedListener(listener: OnBackPressedListener) } BaseFragment : Fragment(), OnBackPressable, OnBackPressedListener{} Кнопка Back Обработка нажатия
  49. public class Resume { private Collection<SpecializationDto> specializations; private ProfessionalExperienceDto professionalExperience;

    private Set<ResumeCertificateDto> certificates; private AdditionalEducationDto additionalEducation; private BusinessTripReadiness businessTripsReadiness; private ContactInformationDto contactInformation; private RecommendationInfoDto recommendationInfo; private List<UserImageDto> userPortfolio; private EducationHistoryDto educationHistory; private RelocationInfoDto relocationInfo; private ResumeLanguageDto languages; private WorkExperience workExperience; private Set<Employment> employments; private EducationLevel education; private LocalDateTime creationTime; private LocalDateTime lastChangeTime; private Set<Schedule> schedules; private RoadTimeType roadTime; private ResumeStatus resumeStatus; private UserImageDto userImage; private AreaDto area; private AccessDto access; private LocalDate birthdayDate; private AreaDto[] citizenship; private AreaDto[] workTickets; private MoneyDto desirableCompensation; private MetroDto nearestMetro; private Gender gender; private String[] keySkills; private String hash; private String title; private String aboutMe; private int id; private int siteId; private boolean hasVehicle; private String[] driverLicense; //… } + Большие объекты
  50. public class Resume { private Collection<SpecializationDto> specializations; private ProfessionalExperienceDto professionalExperience;

    private Set<ResumeCertificateDto> certificates; private AdditionalEducationDto additionalEducation; private BusinessTripReadiness businessTripsReadiness; private ContactInformationDto contactInformation; private RecommendationInfoDto recommendationInfo; private List<UserImageDto> userPortfolio; private EducationHistoryDto educationHistory; private RelocationInfoDto relocationInfo; private ResumeLanguageDto languages; private WorkExperience workExperience; private Set<Employment> employments; private EducationLevel education; private LocalDateTime creationTime; private LocalDateTime lastChangeTime; private Set<Schedule> schedules; private RoadTimeType roadTime; private ResumeStatus resumeStatus; private UserImageDto userImage; private AreaDto area; private AccessDto access; private LocalDate birthdayDate; private AreaDto[] citizenship; private AreaDto[] workTickets; private MoneyDto desirableCompensation; private MetroDto nearestMetro; private Gender gender; private String[] keySkills; private String hash; private String title; private String aboutMe; private int id; private int siteId; private boolean hasVehicle; private String[] driverLicense; //… } Core Applicant Entity Employer Entity Common Entity + Большие объекты
  51. Предметная область Applicant Entity Employer Entity Common Entity Общий функционал

    Фиче-модули User Job Position Push Suggestion University … Base UI List Processing Pagination Network Remote Config Custom Views View#1 View#2 View#3 … … Applicant Application Russia Azerbaijan Belarus Employer Application Russia Belarus Итоговая картина
  52. Кадр из трилогии “Властелин колец” План перехода

  53. 1 Команда 3 человека, которая увеличится до 10 2 2

    приложения на одной кодовой базе в (!) одном модуле 3 Multi-Activity подход и монолитные Activity и Fragment 4 Dagger 2 и пухлый ApplicationScope 5 Отсутствие Event Bus, наличие синглтонов 6 End2end тесты основных сценариев на голом Espresso Контекст
  54. overflow.io graphviz.org Карта экранов сценариев Анализ зависимостей Анализ слабых мест

    ШАГ №1
  55. Выбивание костылей должно регулярно тестироваться Тесты — это магия Реанимация

    UI тестов ШАГ №2 1 Marathon 2 Kakao Docker 3 4 Kubernetes
  56. Application Applicant code Employer code Common code Кодовая база Иерархия

    Flavors Разделение проектов ШАГ №3
  57. Application Applicant code Employer code Common code Кодовая база Иерархия

    Applicant code Employer code Common code Кодовая база Applicant Employer Common Иерархия Flavors Modules Разделение проектов ШАГ №3
  58. DI фреймворки в модулях могут быть разные Замена Dagger2 на

    Toothpick Подготовка DI ШАГ №4 Замена JSR 330 DI фреймворка в монолите неделима Tothpick позволяет делать хаки при переходе
  59. 1 Выносить Core фичи легко и весело 2 Core представляет

    иерархию модулей Вынос Core модулей ШАГ №5
  60. goo.gl/8QwRp4 Разные Activity должны связывать только сущности, сохраняемые системой Слезаем

    с Multi Activity ШАГ №6
  61. 1 Замена транзакции фрагментов на Cicerone 2 Замена Activity на

    Activity + Fragment Делаем механизм для роутинга (Smart Router) 3 Не занимайтесь внедрением MVP / MVVM / MVI на этом шаге Заменяем Activity + Fragment на Fragment 4 Слезаем с Multi Activity ШАГ №6
  62. Feature 1 Feature 2 Feature 3 Feature 4 App 2

    Core Feature 2 Core Feature 3 Core Features Business Features App Core Feature 1 App 1 Отпочкование фичей ШАГ №7 App 1 Feature 5 Feature 6 Feature 7 Feature 8 Feature 9 Feature ∞
  63. class PositionComponent(positionDependencies: PositionDependencies) { companion object { private const val

    ROOT_SCOPE = “${BuildConfig.APPLICATION_ID}_POSITION” private val counter = AtomicInteger() } private val scopeName = ROOT_SCOPE + counter.incrementAndGet() init { Toothpick.openScopes(APP_ROOT_SCOPE, APP_SCOPE, scopeName) .installModules(DependenciesModule(positionDependencies)) } fun destroyComponent() { Toothpick.closeScope(scopeName) } val api: FeatureApi = FeatureApiIml(scopeName) } Переинициализация зависимостей PositionMediator Component Holder PositionComponent External Stub API API Internal
  64. class PositionComponent(positionDependencies: PositionDependencies) { companion object { private const val

    ROOT_SCOPE = “${BuildConfig.APPLICATION_ID}_POSITION” private val counter = AtomicInteger() } private val scopeName = ROOT_SCOPE + counter.incrementAndGet() init { Toothpick.openScopes(APP_ROOT_SCOPE, APP_SCOPE, scopeName) .installModules(DependenciesModule(positionDependencies)) } fun destroyComponent() { Toothpick.closeScope(scopeName) } val api: FeatureApi = FeatureApiIml(scopeName) } PositionMediator Component Holder PositionComponent External Stub API API Internal
  65. class PositionComponent(positionDependencies: PositionDependencies) { companion object { private const val

    ROOT_SCOPE = “${BuildConfig.APPLICATION_ID}_POSITION” private val counter = AtomicInteger() } private val scopeName = ROOT_SCOPE + counter.incrementAndGet() init { Toothpick.openScopes(APP_ROOT_SCOPE, APP_SCOPE, scopeName) .installModules(DependenciesModule(positionDependencies)) } fun destroyComponent() { Toothpick.closeScope(scopeName) } val api: FeatureApi = FeatureApiIml(scopeName) } PositionMediator Component Holder PositionComponent External Stub API API Internal
  66. class PositionScreenMediator() { private val componentHolder: MultiComponentHolder<PositionComponent, PositionDependencies> = MultiComponentHolder

    { deps -> PositionComponent(deps) } fun initComponent(key: String, deps: PositionDependencies) { componentHolder.initComponent(key, deps) } fun provideApiStub(key: String): FeatureApi { return componentHolder.provideComponent(key).api } // ... } PositionMediator Component Holder PositionComponent External Stub API API Internal
  67. class PositionScreenMediator() { private val componentHolder: MultiComponentHolder<PositionComponent, PositionDependencies> = MultiComponentHolder

    { deps -> PositionComponent(deps) } fun initComponent(key: String, deps: PositionDependencies) { componentHolder.initComponent(key, deps) } fun provideApiStub(key: String): FeatureApi { return componentHolder.provideComponent(key).api } // ... } PositionMediator Component Holder PositionComponent External Stub API API Internal
  68. class PositionScreenMediator() { private val componentHolder: MultiComponentHolder<PositionComponent, PositionDependencies> = MultiComponentHolder

    { deps -> PositionComponent(deps) } fun initComponent(key: String, deps: PositionDependencies) { componentHolder.initComponent(key, deps) } fun provideApiStub(key: String): FeatureApi { return componentHolder.provideComponent(key).api } // ... } PositionMediator Component Holder PositionComponent External Stub API API Internal
  69. PositionMediator Component Holder PositionComponent External Stub API API Internal class

    PositionScreenMediator() { private val componentHolder: MultiComponentHolder<PositionComponent, PositionDependencies> = MultiComponentHolder { deps -> PositionComponent(deps) } fun initComponent(key: String, deps: PositionDependencies) { componentHolder.initComponent(key, deps) } fun provideApiStub(key: String): FeatureApi { return componentHolder.provideComponent(key).api } // ... } Initializer
  70. Mediator Mediator Mediator Mediator Mediator Mediator Mediator Mediator Формирование медиаторов

    ШАГ №8 Неиерархичная связь
  71. Mediator Mediator Mediator Mediator Mediator Mediator Mediator Mediator Формирование медиаторов

    ШАГ №8 Древовидная связь
  72. Feature 6 Feature 5 Feature 1 Feature 2 Feature 3

    Feature 4 App 1 App 2 Core Feature 2 Core Feature 3 Core Features Business Features App Core Feature 1 Mediator Формирование медиаторов ШАГ №8 Иерархичная связь
  73. Формирование медиаторов Отпочкование фичей Слезаем с Multi Activity Вынос Core

    модулей Подготовка DI Разделение проектов Реанимация UI тестов Анализ слабых мест №8 №7 №1 №2 №3 №4 №5 №6 Photo by Sven Mieke on Unsplash План перехода на модульную архитектуру
  74. Мысли и инсайды Кадр из трилогии “Властелин колец”

  75. Мысли и инсайды Создание тестовых App модулей для фичи Описание

    архитектуры в вики / статье для понимания Проведение экспериментов на Console проекте Время сборки и борьба с IDE
  76. gradlew headhunter-applicant:assembleHhruDebug / headhunter-employer:assembleHhruDebug / runAllUnitTests

  77. Подведение итогов

  78. Подход решает поставленные задачи Концепция сложная и подойдет для зрелых

    проектов Выводы Экспериментируйте. В экспериментах рождается истина
  79. Ч Т О Е Щ Ё

  80. Mobius 2018 Академия Яндекса habr.com Mobius 2018 Mobius 2018 Mobius

    2019
  81. Выводы Подход решает поставленные задачи Концепция сложная и подойдет для

    зрелых проектов Экспериментируйте. В экспериментах рождается истина Слайды Обзор проблем Картина “TO BE” Подведение итогов Переход от “AS IS” к “TO BE” О чем мы говорили
  82. БЛИНОВ Александр @xanderblinov @xanderblinov