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

«Model Driven-конфигурация приложения на Kotlin DSL», Андрей Говоровский, Mail.ru Group

5d08ba0cd07942f2ddbf82e5b21ba5e7?s=47 FunCorp
August 03, 2019

«Model Driven-конфигурация приложения на Kotlin DSL», Андрей Говоровский, Mail.ru Group

5d08ba0cd07942f2ddbf82e5b21ba5e7?s=128

FunCorp

August 03, 2019
Tweet

Transcript

  1. Говоровский Андрей Senior Android Developer 03.08.2019 Model-driven конфигурация приложения на

    Kotlin DSL
  2. Коротко обо мне В Android разработке с 2014 года Работаю

    в Mail.ru Group в проекте мобильной почты Преподаю курс по Android разработке в Технопарке при МГТУ им. Баумана
  3. Agenda Зачем это понадобилось !3

  4. Agenda Зачем это понадобилось Kotlin DSL для описания конфигурации !4

  5. Agenda Зачем это понадобилось Kotlin DSL для описания конфигурации Генерация

    моделей, парсеров !5
  6. Agenda Зачем это понадобилось Kotlin DSL для описания конфигурации Генерация

    моделей, парсеров Как это все встроить в процесс сборки !6
  7. Конфигурация приложений Требования бизнеса !7

  8. Конфигурация приложений Требования бизнеса Включать/выключать фичи, запускать промо !8

  9. Конфигурация приложений !9

  10. Конфигурация приложений Требования бизнеса Включать/выключать фичи, запускать промо Проводить A/B

    тесты !10
  11. Конфигурация приложений !11

  12. Конфигурация приложений Требования бизнеса Включать/выключать фичи, запускать промо Проводить A/B

    тесты Менять поведение приложения, тексты, графику без выпуска новой версии !12
  13. Конфигурация приложений !13

  14. Конфигурация приложений Решение - разделять пользователей на группы и присылать

    разные варианты конфигураций !14
  15. Типичный конфиг фичи { "id": "mark_all_as_read", "image": "https://r.mradx.net/img/4A/C7B130.png", "rules": [

    { "value": "${/folders/0/unread} > 50", "type": "expression" } ], "texts": [ { "alignment": "1", "body": "Непрочитанных писем<br> во входящих: *${/ folders/0/unread}*<br>Отметить их прочитанными?", "color": "black" } ], "type": "custom", "visible-close-button": true } !15
  16. Типичный конфиг фичи { "id": "mark_all_as_read", "image": "https://r.mradx.net/img/4A/C7B130.png", "rules": [

    { "value": "${/folders/0/unread} > 50", "type": "expression" } ], "texts": [ { "alignment": "1", "body": "Непрочитанных писем<br> во входящих: *${/ folders/0/unread}*<br>Отметить их прочитанными?", "color": "black" } ], "type": "custom", "visible-close-button": true } !16 Вложенные объекты
  17. Типичный конфиг фичи { "id": "mark_all_as_read", "image": "https://r.mradx.net/img/4A/C7B130.png", "rules": [

    { "value": "${/folders/0/unread} > 50", "type": "expression" } ], "texts": [ { "alignment": "1", "body": "Непрочитанных писем<br> во входящих: *${/ folders/0/unread}*<br>Отметить их прочитанными?", "color": "black" } ], "type": "custom", "visible-close-button": true } !17 Вложенные объекты Массивы
  18. Типичный конфиг фичи { "id": "mark_all_as_read", "image": "https://r.mradx.net/img/4A/C7B130.png", "rules": [

    { "value": "${/folders/0/unread} > 50", "type": "expression" } ], "texts": [ { "alignment": "1", "body": "Непрочитанных писем<br> во входящих: *${/ folders/0/unread}*<br>Отметить их прочитанными?", "color": "black" } ], "type": "custom", "visible-close-button": true } !18 Вложенные объекты Массивы Примитивы
  19. Типичный конфиг фичи { "id": "mark_all_as_read", "image": "https://r.mradx.net/img/4A/C7B130.png", "rules": [

    { "value": "${/folders/0/unread} > 50", "type": "expression" } ], "texts": [ { "alignment": "1", "body": "Непрочитанных писем<br> во входящих: *${/ folders/0/unread}*<br>Отметить их прочитанными?", "color": "black" } ], "type": "custom", "visible-close-button": true } !19 Вложенные объекты Массивы Примитивы Enum’s
  20. Типичный конфиг фичи { "id": "mark_all_as_read", "image": "https://r.mradx.net/img/4A/C7B130.png", "rules": [

    { "value": "${/folders/0/unread} > 50", "type": "expression" } ], "texts": [ { "alignment": "1", "body": "Непрочитанных писем<br> во входящих: *${/ folders/0/unread}*<br>Отметить их прочитанными?", "color": "black" } ], "type": "custom", "visible-close-button": true } !20 Вложенные объекты Массивы Примитивы Enum’s
  21. Конфигурация приложений Как доставлять конфиг пользователям? !21

  22. Конфигурация приложений Configuration server !22

  23. Конфигурация приложений Configuration server Group 1 -> json_1 Group 2

    -> json_2 !23
  24. Конфигурация приложений {json2} Configuration server !24 Group 1 -> json_1

    Group 2 -> json_2 {state1} {state1} {json1}
  25. Получение конфига на клиенте !25

  26. Получение конфига на клиенте JSON Parsers !26

  27. Получение конфига на клиенте Config from server JSON Parsers !27

  28. Получение конфига на клиенте Config from server Previously saved config

    JSON Parsers !28
  29. Получение конфига на клиенте Config from server /assets etalon config

    Previously saved config JSON Parsers !29
  30. Получение конфига на клиенте Config from server /assets etalon config

    Previously saved config JSON Parsers class MailAppConfiguration { /* ... */ } !30
  31. Использование конфига на клиенте !31 class MailAppConfiguration { fun isFeatureAEnabled():

    Boolean { ... } fun getFeatureBConfig(): FeatureConfig { ... } }
  32. !32 val mailConfig: MailAppConfiguration fun onScreenShown() { if(mailConfig.featureConfig.isEnabled) { val

    featureConfig = mailConfig.featureConfig /* some work with featureConfig */ } else { /* ... */ } } Использование конфига на клиенте
  33. !33 class MailResources(resources: Resources) : BaseResourcesWrapper(resources) { @Throws(Resources.NotFoundException::class) override fun

    getString(id: Int): String { return if (mailConfig.isResourcesOverridden) { val result = memcache.getString(getResourceName(id), resConf) result ?: getStringFromSuper(id) } else { super.getString(id) } } } Использование конфига на клиенте
  34. Как было раньше ! !34

  35. Как было раньше ! Feature !35

  36. Как было раньше ! Feature { "config":{ "feature":true, "param":123 }

    } !36
  37. Как было раньше ! Feature { "config":{ "feature":true, "param":123 }

    } Logic Parsers Validation !37
  38. Как было раньше ! Feature { "config":{ "feature":true, "param":123 }

    } DONE Logic Parsers Validation !38
  39. Как было раньше ! Feature { "config":{ "feature":true, "param":123 }

    } Logic Parsers Validation DONE Task !39
  40. Как было раньше !! !! !40

  41. Как было раньше ! Feature ! !! !41

  42. Как было раньше ! Feature { "config":{ "feature":true, "param":123 }

    } { "config":{ "feature":true, "param":123 } } { "config":{ "feature":true, "param":123 } } { "config":{ "feature":true, "param":123 } } { "config":{ "feature":true, "param":123 } } ! !! !42
  43. Как было раньше ! Feature { "config":{ "feature":true, "param":123 }

    } { "config":{ "feature":true, "param":123 } } { "config":{ "feature":true, "param":123 } } { "config":{ "feature":true, "param":123 } } { "config":{ "feature":true, "param":123 } } ! !! ??? !43
  44. Как было раньше !44

  45. We have a problem(s) Нет единого источника конфигурации, все разбросано

    по коду и задачам !45
  46. We have a problem(s) Нет единого источника конфигурации, все разбросано

    по коду и задачам Разные источники дефолтных значений !46
  47. We have a problem(s) Нет единого источника конфигурации, все разбросано

    по коду и задачам Разные источники дефолтных значений Сложно понять, какие значения может принимать поле и в какой версии !47
  48. We have a problem(s) Нет единого источника конфигурации, все разбросано

    по коду и задачам Разные источники дефолтных значений Сложно понять, какие значения может принимать поле и в какой версии Boilerplate, каждый пишет свою логику парсинга, валидации и тп !48
  49. Time for DSL !49

  50. Что есть DSL Предметно-ориентированный язык (англ. domain- specific language, DSL

    — «язык, специфический для предметной области») — язык программирования, специализированный для конкретной области применения (в противоположность языку общего назначения, применимому к широкому спектру областей и не учитывающему особенности конкретных сфер знаний). © Wikipedia !50
  51. Что есть DSL Язык для работы с определенным набором проблем.

    !51
  52. Вы уже используете DSL !52

  53. Вы уже используете DSL !53

  54. Почему DSL? Конфигурация как domain область !54

  55. Почему DSL? Конфигурация как domain область Просто вносить изменения, создавать

    новые поля, накладывать ограничения !55
  56. Почему DSL? Конфигурация как domain область Просто вносить изменения, создавать

    новые поля, накладывать ограничения Можно описать все поля и возможные значения !56
  57. Почему DSL? Конфигурация как domain область Просто вносить изменения, создавать

    новые поля, накладывать ограничения Можно описать все поля и возможные значения Из DSL описания удобно сгенерировать парсеры с валидацией значений, документацию, примеры конфигурации и тд !57
  58. Инструменты для создания DSL !58

  59. Инструменты для создания DSL Lambda with receivers fun <T> T.apply(block:

    T.() -> Unit): T { block() return this } !59
  60. Инструменты для создания DSL Lambda with receivers fun <T> T.apply(block:

    T.() -> Unit): T { block() return this } fun createButton() = Button().apply { text = "Some text" height = 40 width = 60 } !60
  61. Инструменты для создания DSL Extension functions class T class F(val

    name: String, val type: T) !61
  62. Инструменты для создания DSL Extension functions class T class F(val

    name: String, val type: T) fun String.of(t: T) = F(name = this, type = t) !62
  63. Инструменты для создания DSL Extension functions class T class F(val

    name: String, val type: T) fun String.of(t: T) = F(name = this, type = t) val field = "var1".of(T()) !63
  64. Инструменты для создания DSL Infix functions infix fun String.of(t: T)

    = F(name = this, type = t) !64
  65. Инструменты для создания DSL Infix functions infix fun String.of(t: T)

    = F(name = this, type = t) val field = "field" of T() !65
  66. Simple DSL. Исходный JSON { "feature":{ "field1":1, "field2":"text field" }

    } !66
  67. Simple DSL. Структура конфига !67 Definition

  68. Simple DSL. Структура конфига !68 Definition Field Int

  69. Simple DSL. Структура конфига !69 Definition Field Int Field String

  70. Simple DSL. Структура конфига !70 Definition Field Int Field Definition

    Field String
  71. Simple DSL. Описание поля data class Field<N>( val name: String,

    val type: Type<N>, val default: N? = null, val description: String ) class Definition(val fields: List<Field<*>>) !71
  72. Simple DSL. Описание поля data class Field<N>( val name: String,

    val type: Type<N>, val default: N? = null, val description: String ) class Definition(val fields: List<Field<*>>) !72
  73. Simple DSL. Описание поля data class Field<N>( val name: String,

    val type: Type<N>, val default: N? = null, val description: String ) class Definition(val fields: List<Field<*>>) !73
  74. Simple DSL. Описание поля data class Field<N>( val name: String,

    val type: Type<N>, val default: N? = null, val description: String ) class Definition(val fields: List<Field<*>>) !74
  75. Simple DSL. Описание поля data class Field<N>( val name: String,

    val type: Type<N>, val default: N? = null, val description: String ) class Definition(val fields: List<Field<*>>) !75
  76. Simple DSL. Описание поля data class Field<N>( val name: String,

    val type: Type<N>, val default: N? = null, val description: String ) class Definition(val fields: List<Field<*>>) !76
  77. Simple DSL. Описание типов sealed class Type<out N> { abstract

    fun defaultValue(): N } class StringType: Type<String>() { override fun defaultValue(): String = "" } class IntegerType: Type<Int>() { override fun defaultValue(): Int = 0 } class StrictObjectType(val definition: Definition? = null): Type<Unit?>() { override fun defaultValue(): Unit? = null } !77
  78. Simple DSL. Описание типов sealed class Type<out N> { abstract

    fun defaultValue(): N } class StringType: Type<String>() { override fun defaultValue(): String = "" } class IntegerType: Type<Int>() { override fun defaultValue(): Int = 0 } class StrictObjectType(val definition: Definition? = null): Type<Unit?>() { override fun defaultValue(): Unit? = null } !78
  79. Simple DSL. Описание типов sealed class Type<out N> { abstract

    fun defaultValue(): N } class StringType: Type<String>() { override fun defaultValue(): String = "" } class StrictObjectType(val definition: Definition? = null): Type<Unit?>() { override fun defaultValue(): Unit? = null } !79
  80. Simple DSL. Компоновка infix fun <N> String.of(type: Type<N>) = Field(this,

    type) !80
  81. Simple DSL. Компоновка infix fun <N> String.of(type: Type<N>) = Field(this,

    type) infix fun <N> Field<N>.withDefault(defaultValue: N?): Field<N> = copy(default = defaultValue) !81
  82. Simple DSL. Компоновка infix fun <N> String.of(type: Type<N>) = Field(this,

    type) infix fun <N> Field<N>.withDefault(defaultValue: N?): Field<N> = copy(default = defaultValue) infix fun Field<Any?>.restrictedBy(definition: Definition): Field<Any?> = copy(type = StrictObjectType(definition)) !82
  83. Simple DSL. Компоновка infix fun <N> String.of(type: Type<N>) = Field(this,

    type) infix fun <N> Field<N>.withDefault(defaultValue: N?): Field<N> = copy(default = defaultValue) infix fun Field<Any?>.restrictedBy(definition: Definition): Field<Any?> = copy(type = StrictObjectType(definition)) !83
  84. Simple DSL. Использование val config = definition( "feature" of StrictObjectType()

    withDescription "Some cool feature" restrictedBy definition( "field2" of IntegerType() withDescription "Show count" withDefault 1 withAllowedRange 1..10, "field1" of StringType() withDefault "title1" withDescription "Text field" withAllowedValues listOf("title1", "title2") ) ) !84 Config.kt
  85. Simple DSL. Использование !85 private fun collectAllFieldsRecursively(definition: Definition): List<Field<*>> {

    return definition.fields .flatMap { val type = it.type listOf(it) + if (type is StrictObjectType) { collectAllFieldsRecursively(type.definition) } else if (type is CompositeType && type.subtype is StrictObjectType) { collectAllFieldsRecursively((type.subtype.definition) } else { emptyList() } } }
  86. Simple DSL. Использование Config.kt !86 Config.kt

  87. Simple DSL. Использование Config.kt class FeatureJSONParser interface FeatureConfig class FeatureConfigImpl

    !87 Config.kt
  88. Simple DSL. Использование Config.kt class FeatureJSONParser interface FeatureConfig class FeatureConfigImpl

    !88 Config.kt
  89. Simple DSL. Использование interface FeatureConfig { String getField1(); Integer getField2();

    } !89
  90. Simple DSL. Использование public class FeatureConfigImpl implements Feature { private

    String mField1 = "Window title"; private Integer mField2 = 1; void setField1(String field1) { mField1 = field1; } void setField2(Integer field2) { mField2 = field2; } @Override public String getField1() { return mField1; } @Override public Integer getField2() { return mField2; } } !90
  91. Simple DSL. Использование class FeatureJsonParser { public FeatureConfigImpl parse(JSONObject json)

    throws JSONException { FeatureConfigImpl obj = new FeatureConfigImpl(); if (json.has("feature")) { String value; try { value = json.getString("field1"); obj.setField1(value); } catch (JSONException e) { /* send analytics */ } } return obj; } } !91
  92. Simple DSL. Documentation !92

  93. Настройки разработчика Так же генерируются из модели !93

  94. Настройки разработчика Так же генерируются из модели Возможность посмотреть все

    доступные фичи !94
  95. Настройки разработчика Так же генерируются из модели Возможность посмотреть все

    доступные фичи Изменять поведение приложения в рантайме !95
  96. Nowadays ! !96

  97. Nowadays ! Feature Config.kt !97

  98. Nowadays ! Feature Config.kt Gradle Task Parsers Validation DTOConfig Documentation

    !98
  99. Nowadays ! Feature Config.kt Gradle Task Parsers Validation DTOConfig Documentation

    Logic !99
  100. Nowadays ! Feature Config.kt Gradle Task Parsers Validation DTOConfig Documentation

    Logic !100
  101. Nowadays ! Feature Config.kt Gradle Task Parsers Validation DTOConfig Documentation

    Logic !101 "
  102. Встраивание в процесс сборки !102

  103. Встраивание в процесс сборки 1. Помещаем код генератора в buildSrc

    !103
  104. Встраивание в процесс сборки 1. Помещаем код генератора в buildSrc

    2. Cоздаем Gradle-таск !104
  105. Встраивание в процесс сборки 1. Помещаем код генератора в buildSrc

    2. Cоздаем Gradle-таск 3. Запускаем таск в нужной фазе процесса сборки !105
  106. Встраивание в процесс сборки open class ConfigGenerationTask : DefaultTask() @InputFile

    !106
  107. Встраивание в процесс сборки open class ConfigGenerationTask : DefaultTask() {

    @InputFile val configurationDefinition: File = project.rootProject.file("Config.kt") @OutputDirectory private fun getOutputDirName(): File { return File(destDir + "/" + packageName.replace('.', '/')) } !107
  108. Встраивание в процесс сборки open class ConfigGenerationTask : DefaultTask() {

    @InputFile val configurationDefinition: File = project.rootProject.file("Config.kt") @OutputDirectory private fun getOutputDirName(): File { return File(destDir + "/" + packageName.replace('.', '/')) } @TaskAction fun executeTask() { ConfigGenerator(/* ..... */).generate() } } !108
  109. Встраивание в процесс сборки open class ConfigGenerationTask : DefaultTask() {

    @InputFile val configurationDefinition: File = project.rootProject.file("Config.kt") @OutputDirectory private fun getOutputDirName(): File { return File(destDir + "/" + packageName.replace('.', '/')) } @TaskAction fun executeTask() { ConfigGenerator(/* ..... */).generate() } } !109
  110. Встраивание в процесс сборки task configGenTask(type: ConfigGenerationTask) { packageName =

    "ru.mail.mailapp" jsonParserClassName = "DTOConfigurationJsonParser" configurationClassName = "DTOConfigurationImpl" destDir = "${project.buildDir.toString()}/generated/source/modelConfig" android.sourceSets.main.java.srcDirs += destDir } !110
  111. Встраивание в процесс сборки task configGenTask(type: ConfigGenerationTask) { packageName =

    "ru.mail.mailapp" jsonParserClassName = "DTOConfigurationJsonParser" configurationClassName = "DTOConfigurationImpl" destDir = "${project.buildDir.toString()}/generated/source/modelConfig" android.sourceSets.main.java.srcDirs += destDir } !111
  112. Встраивание в процесс сборки task configGenTask(type: ConfigGenerationTask) { packageName =

    "ru.mail.mailapp" jsonParserClassName = "DTOConfigurationJsonParser" configurationClassName = "DTOConfigurationImpl" destDir = "${project.buildDir.toString()}/generated/source/modelConfig" android.sourceSets.main.java.srcDirs += destDir } !112
  113. Встраивание в процесс сборки task configGenTask(type: ConfigGenerationTask) { packageName =

    "ru.mail.mailapp" jsonParserClassName = "DTOConfigurationJsonParser" configurationClassName = "DTOConfigurationImpl" destDir = "${project.buildDir.toString()}/generated/source/modelConfig" android.sourceSets.main.java.srcDirs += destDir } !113
  114. Встраивание в процесс сборки task configGenTask(type: ConfigGenerationTask) { packageName =

    "ru.mail.mailapp" jsonParserClassName = "DTOConfigurationJsonParser" configurationClassName = "DTOConfigurationImpl" destDir = "${project.buildDir.toString()}/generated/source/modelConfig" android.sourceSets.main.java.srcDirs += destDir } !114
  115. Встраивание в процесс сборки task configGenTask(type: ConfigGenerationTask) { packageName =

    "ru.mail.mailapp" jsonParserClassName = "DTOConfigurationJsonParser" configurationClassName = "DTOConfigurationImpl" destDir = "${project.buildDir.toString()}/generated/source/modelConfig" android.sourceSets.main.java.srcDirs += destDir } !115
  116. Встраивание в процесс сборки !116 Initialization

  117. Встраивание в процесс сборки !117 Initialization Configuration

  118. Встраивание в процесс сборки !118 Initialization Configuration DAG of Tasks

  119. Встраивание в процесс сборки !119 Initialization Configuration Execution DAG of

    Tasks
  120. Встраивание в процесс сборки !120 Initialization Configuration Execution DAG of

    Tasks Run config generation task
  121. Встраивание в процесс сборки project.afterEvaluate { /* .... */ preBuild.dependsOn(configGenTask)

    /* .... */ } !121
  122. Встраивание в процесс сборки project.afterEvaluate { /* .... */ preBuild.dependsOn(configGenTask)

    /* .... */ } !122
  123. Встраивание в процесс сборки Получили инкрементальный таск. Повторная генерация при

    изменении Config.kt или при удалении /build директории !123
  124. Выводы !124

  125. !125

  126. Profit 1. Описали конфигурацию через DSL 2. Получили единый источник

    информации о фичах 3. Сделали удобные настройки разработчика для тестирования 4. Опробовали Kotlin в проекте, остались довольны :) !126
  127. Talk is cheap, show me the code https://github.com/govorovsky/configen

  128. Спасибо за внимание! @govorovskij @govorovsky

  129. None