Pro Yearly is on sale from $80 to $50! »

CodeFest 2019. Павел Финкельштейн (Lamoda) — Kotlin: 2 года в проде и ни единого разрыва

16b6c87229eaf58768d25ed7b2bbbf52?s=47 CodeFest
April 05, 2019

CodeFest 2019. Павел Финкельштейн (Lamoda) — Kotlin: 2 года в проде и ни единого разрыва

Это доклад про то, как я попробовал Kotlin, как затащил его в продакшн и с какими сложностями мы столкнулись.

Основные темы:
Транзакции
Работа с БД
Исключения
Тестирование
Коллекции
Интеропабилити с джавой
Аннотации
Архитектура
Немного про ФП

16b6c87229eaf58768d25ed7b2bbbf52?s=128

CodeFest

April 05, 2019
Tweet

Transcript

  1. Kotlin: Два года в проде и ни единого разрыва Паша

    Финкельштейн Разработчик, тимлид
  2. О докладе Кишки нормального доклада 2

  3. О докладе Кишки доклада курильщика 3

  4. Не Android 4

  5. Не для тех, кто уже 5

  6. Отказ от гарантий • Я никак не аффилирован с JetBrains

    (пока) • Я не отвечаю за то что вам захочется попробовать • Так же как и за то, что вам не захочется • Я знаю не всё, а значит могу ошибаться или неправильно понимать то, что другие люди понимают правильно или просто там не ошибаются. • Но я стараюсь 6
  7. Несколько слов о себе? • 12 лет в IT •

    10 лет в разработке • Почти всё время — на JVM • Учусь по референсам и на ошибках • Экспериментирую • Попробовал всякое от Java до Ceylon и Frege module Hello where greeting friend = "Hello, " ++ friend ++ "!" main args = do println (greeting "World") 7
  8. Intro: каким был мой первый раз • Дока была в

    конфлюэнсе • Аннотаций были без собак • Перегруженные конструкторы? Не, не слышал. • И я даже что-то предлагал :) 8 • Котлин был ещё чайником
  9. А реальный опыт? • Маленький стартап • Маленькие сервисы •

    Много сущностей, которые меняются • Надоел бойлерплейт и костыли 9 VS Код здорового разработчика тут data class Person (//… @Getter @Setter @EqualsAndHashCode @AllArgsConstructor @RequiredArgsConstructor public class Person{ //…
  10. А ещё? • null-safety • Автоматический вывод типов • Очень

    хорошую совместимость с Java • Идеальное делегирование • Нормальный вызов функций без всякого apply() • extension-методы 10 fun Iterable<Int>.sum() = reduce{a, b → a + b} Это уже есть в стандартной библиотеке
  11. Зачем я вообще всё это говорю? • Бэк можно писать

    на котлине? • А риски? Этот доклад — это наш путь и опыт 11
  12. Дазы Банных 12

  13. Spring + Kotlin. Part 1. @Transactional 13 @Transactional class MySmartService(repo1:

    Repository1, repo2: Repository2) { fun complex(datum: String): Result { val interim = repo1.save(datum) return repo2.destroy(someOp(interim)) } fun someOp(input: String) = if (notLucky(input)) throw Exception("Sorry, bro") else input
  14. Spring + Kotlin. Part 1. @Transactional 14 @Transactional class MySmartService(repo1:

    Repository1, repo2: Repository2) { fun complex(datum: String): Result { val interim = repo1.save(datum) return repo2.destroy(someOp(interim)) } fun someOp(input: String) = if (notLucky(input)) throw Exception("Sorry, bro") else input
  15. Spring + Kotlin. Part 1. @Transactional 15 @Transactional class MySmartService(repo1:

    Repository1, repo2: Repository2) { fun complex(datum: String): Result { val interim = repo1.save(datum) return repo2.destroy(someOp(interim)) } fun someOp(input: String) = if (notLucky(input)) throw Exception("Sorry, bro") else input
  16. Spring + Kotlin. Part 1. @Transactional 16 @Transactional class MySmartService(repo1:

    Repository1, repo2: Repository2) { fun complex(datum: String): Result { val interim = repo1.save(datum) return repo2.destroy(someOp(interim)) } fun someOp(input: String) = if (notLucky(input)) throw Exception("Sorry, bro") else input
  17. Spring + Kotlin. Part 1. @Transactional 17 @Transactional class MySmartService(repo1:

    Repository1, repo2: Repository2) { fun complex(datum: String): Result { val interim = repo1.save(datum) return repo2.destroy(someOp(interim)) } fun someOp(input: String) = if (notLucky(input)) throw Exception("Sorry, bro") else input Что может пойти не так?
  18. 18

  19. Как работают транзакции 19 https://www.baeldung.com/spring-aop-vs-aspectj

  20. Spring + Kotlin. Part 1. @Transactional 20 @Transactional class MySmartService(repo1:

    Repository1, repo2: Repository2) { fun complex(datum: String): Result { val interim = repo1.save(datum) return repo2.destroy(someOp(interim)) } fun someOp(input: String) = if (notLucky(input)) throw Exception("Sorry, bro") else input
  21. Spring + Kotlin. Part 1. @Transactional 21 @Transactional open class

    MySmartService(repo1: Repository1, repo2: Repository2) { open fun complex(datum: String): Result { val interim = repo1.save(datum) return repo2.destroy(someOp(interim)) } fun someOp(input: String) = if (notLucky(input)) throw Exception("Sorry, bro") else input
  22. Spring + Kotlin. Part 1. @Transactional Проблема • Всё final

    • Прокси не создаются • Конфигурации не работают Решения • kotlin-allopen + kotlin-spring ◦ The code is a lie ⇒ сложное кодревью ◦ Конструкция хрупкая • Всюду явно писать open ◦ boilerplate 22
  23. 23 @Transactional open class MySmartService(repo1: Repository1, repo2: Repository2) { open

    fun complex(datum: String): Result { val interim = repo1.save(datum) return repo2.destroy(someOp(interim)) } fun someOp(input: String) = if (notLucky(input)) throw Exception("Sorry, bro") else input Spring + Kotlin. Part 2. @Transactional
  24. 24 @Transactional open class MySmartService(repo1: Repository1, repo2: Repository2) { open

    fun complex(datum: String): Result { val interim = repo1.save(datum) return repo2.destroy(someOp(interim)) } fun someOp(input: String) = if (notLucky(input)) throw Exception("Sorry, bro") else input Что может пойти не так? Spring + Kotlin. Part 2. @Transactional
  25. 25 @Transactional open class MySmartService(repo1: Repository1, repo2: Repository2) { open

    fun complex(datum: String): Result { val interim = repo1.save(datum) return repo2.destroy(someOp(interim)) } fun someOp(input: String) = if (notLucky(input)) throw Exception("Sorry, bro") else input Spring + Kotlin. Part 2. @Transactional
  26. 26 Collateral Damage

  27. Исключения в Kotlin Понятный подход: throw RuntimeException(Exception()) 27 Но нет!

    public static RuntimeException sneakyThrow(Throwable t) { if (t == null) throw new NullPointerException("t"); return Lombok.<RuntimeException>sneakyThrow0(t); } @SuppressWarnings("unchecked") private static <T extends Throwable> T sneakyThrow0(Throwable t) throws T { throw (T)t; }
  28. Исключения в Kotlin 28 public static RuntimeException sneakyThrow(Throwable t) {

    if (t == null) throw new NullPointerException("t"); return Lombok.<RuntimeException>sneakyThrow0(t); } @SuppressWarnings("unchecked") private static <T extends Throwable> T sneakyThrow0(Throwable t) throws T { throw (T)t; }
  29. Исключения в Kotlin 29 public static RuntimeException sneakyThrow(Throwable t) {

    if (t == null) throw new NullPointerException("t"); return Lombok.<RuntimeException>sneakyThrow0(t); } @SuppressWarnings("unchecked") private static <T extends Throwable> T sneakyThrow0(Throwable t) throws T { throw (T)t; }
  30. Исключения в Kotlin 30 public static RuntimeException sneakyThrow(Throwable t) {

    if (t == null) throw new NullPointerException("t"); return Lombok.<RuntimeException>sneakyThrow0(t); } @SuppressWarnings("unchecked") private static <T extends Throwable> T sneakyThrow0(Throwable t) throws T { throw (T)t; }
  31. Проблема • Нет checked exceptions Решения • Следить за исключениями

    ◦ Не создавать checked exceptions 31 Spring + Kotlin. Part 2. @Transactional
  32. Как inline нам на ногу наступил 32 @Transactional open class

    SomeService(val repo: SomeRepo){
  33. Как inline нам на ногу наступил 33 @Transactional open class

    SomeService(val repo: SomeRepo){ open fun someFunc(mapper: (DTO)->Long): List<Long> = repo .someAction() .map{mapper(it)}
  34. Как inline нам на ногу наступил 34 @Transactional open class

    SomeService(val repo: SomeRepo){ open fun someFunc(mapper: (DTO)->Long): List<Long> = repo .someAction() .map{mapper(it)} Но есть же inline!
  35. Как inline нам на ногу наступил 35 @Transactional open class

    SomeService(val repo: SomeRepo){ open inline fun someFunc(mapper: (DTO)->Long): List<Long> = repo .someAction() .map{mapper(it)}
  36. 36

  37. Как inline нам на ногу наступил 37 @Transactional open class

    SomeService(val repo: SomeRepo){ open inline fun someFunc(mapper: (DTO)->Long): List<Long> = repo .someAction() .map{mapper(it)} public
  38. Проблема • inline инлайнится даже когда не надо Решения •

    Все зависимости надо делать приватными • Думать когда пользуешься inline 38 Как инлайн нам на ногу наступил
  39. Работа с БД. JOOQ. Бонусы • Получение работает даже красивее

    чем в Java: record[FIELD] vs record.get(FIELD) • Маппинг в data классы из коробки • Лямбды! 39
  40. Работа с БД. JOOQ. Минус • Алиасы надо писать как

    ◦ val b = Book("b") ◦ val b = BOOK.`as`("b") Вместо привычного Book b = BOOK.as(“b”) 40
  41. Мы ынтырпрайз, у нас JPA, а не ваши хипстерские штучки

    Disclaimer: у меня нет опыта с этим Проблема: no-arg конструкторов в data-классах нет Решение 1: не использовать data-классы Абыдна 41
  42. Мы ынтырпрайз, у нас JPA, а не ваши хипстерские штучки

    <artifactId>kotlin-maven-plugin</artifactId> <groupId>org.jetbrains.kotlin</groupId> <configuration> <compilerPlugins> <plugin>jpa</plugin> </compilerPlugins> </configuration> <dependencies> <dependency> <groupId>org.jetbrains.kotlin</groupId> <artifactId>kotlin-maven-noarg</artifactId> </dependency> </dependencies> 42 Решение 2
  43. Мы ынтырпрайз, у нас JPA, а не ваши хипстерские штучки

    <artifactId>kotlin-maven-plugin</artifactId> <groupId>org.jetbrains.kotlin</groupId> <configuration> <compilerPlugins> <plugin>jpa</plugin> </compilerPlugins> </configuration> <dependencies> <dependency> <groupId>org.jetbrains.kotlin</groupId> <artifactId>kotlin-maven-noarg</artifactId> </dependency> </dependencies> 43
  44. Мы ынтырпрайз, у нас JPA, а не ваши хипстерские штучки

    <artifactId>kotlin-maven-plugin</artifactId> <groupId>org.jetbrains.kotlin</groupId> <configuration> <compilerPlugins> <plugin>jpa</plugin> </compilerPlugins> </configuration> <dependencies> <dependency> <groupId>org.jetbrains.kotlin</groupId> <artifactId>kotlin-maven-noarg</artifactId> </dependency> </dependencies> 44
  45. Мы ынтырпрайз, у нас JPA, а не ваши хипстерские штучки

    <artifactId>kotlin-maven-plugin</artifactId> <groupId>org.jetbrains.kotlin</groupId> <configuration> <compilerPlugins> <plugin>jpa</plugin> </compilerPlugins> </configuration> <dependencies> <dependency> <groupId>org.jetbrains.kotlin</groupId> <artifactId>kotlin-maven-noarg</artifactId> </dependency> </dependencies> 45
  46. Тесты 46

  47. Kotlin + JUnit. Тестирование. Part 1. Что нужно знать: •

    @BeforeAll и @AfterAll нужно объявлять в компаньоне • Для Parametrized тестов ◦ ClassRule, MethodRule и @Parameter должен быть @JvmField 47
  48. Kotlin + JUnit. Тестирование. Part 1. companion object { @JvmStatic

    @BeforeAll fun setup() {} @ClassRule @JvmField val SPRING_CLASS_RULE = SpringClassRule() } @Rule @JvmField var springMethodRule = SpringMethodRule() 48
  49. Kotlin + JUnit. Тестирование. Part 2. Mockito. 49 • anyObject()

    возвращает null • это плохо потому что на обращении к объекту Kotlin проверяет что объект — не null • anyString() тоже! • when — ключевое слово
  50. Kotlin + JUnit. Тестирование. Part 2. Mockito. import org.mockito.Mockito.`when` as

    on inline fun <reified T> kAnyObject(): T = kAnyObject(T::class.java) inline fun kAnyObject(t: KClass<T>): T = Mockito.any(t) 50
  51. Kotlin + JUnit. Тестирование. Part 2. Mockito. import org.mockito.Mockito.`when` as

    on inline fun <reified T> kAnyObject(): T = kAnyObject(T::class.java) inline fun kAnyObject(t: KClass<T>): T = Mockito.any(t) 51
  52. Kotlin. Тестирование. Part 3. Плюшки. Красивый mockito @Test fun doAction_doesSomething(){

    val mock = mock<MyClass> { on { getText() } doReturn "text" } val classUnderTest = ClassUnderTest(mock) classUnderTest.doAction() verify(mock).doSomething(any()) } 52 Mockito-kotlin
  53. Kotlin. Тестирование. Part 3. Плюшки. Красивый mockito @Test fun doAction_doesSomething(){

    val mock = mock<MyClass> { on { getText() } doReturn "text" } val classUnderTest = ClassUnderTest(mock) classUnderTest.doAction() verify(mock).doSomething(any()) } 53 Mockito-kotlin
  54. Kotlin. Тестирование. Part 3. Плюшки. Красивый mockito @Test fun doAction_doesSomething(){

    val mock = mock<MyClass> { on { getText() } doReturn "text" } val classUnderTest = ClassUnderTest(mock) classUnderTest.doAction() verify(mock).doSomething(any()) } 54 Mockito-kotlin
  55. Kotlin. Тестирование. Part 3. Плюшки. Красивый mockito @Test fun doAction_doesSomething(){

    val mock = mock<MyClass> { on { getText() } doReturn "text" } val classUnderTest = ClassUnderTest(mock) classUnderTest.doAction() verify(mock).doSomething(any()) } 55 Mockito-kotlin
  56. Kotlin. Тестирование. Part 3. Плюшки. Понятный нейминг fun `I am

    readable unit test name which describes what is tested`(){ assertTrue(true) } 56
  57. Kotlin. Тестирование. Part 3. Плюшки. BDD object CalculatorSpec: Spek({ describe("A

    calculator") { val calculator by memoized { Calculator() } describe("addition") { it("returns the sum of its arguments") { assertThat(3, calculator.add(1, 2)) }}}}) 57 Spek
  58. Kotlin. Тестирование. Part 3. Плюшки. BDD object CalculatorSpec: Spek({ describe("A

    calculator") { val calculator by memoized { Calculator() } describe("addition") { it("returns the sum of its arguments") { assertThat(3, calculator.add(1, 2)) }}}}) 58 Spek
  59. Kotlin. Тестирование. Part 3. Плюшки. BDD object CalculatorSpec: Spek({ describe("A

    calculator") { val calculator by memoized { Calculator() } describe("addition") { it("returns the sum of its arguments") { assertThat(3, calculator.add(1, 2)) }}}}) 59 Spek
  60. Kotlin. Тестирование. Part 3. Плюшки. BDD object CalculatorSpec: Spek({ describe("A

    calculator") { val calculator by memoized { Calculator() } describe("addition") { it("returns the sum of its arguments") { assertThat(3, calculator.add(1, 2)) }}}}) 60 Spek
  61. Kotlin. Тестирование. Part 3. Плюшки. BDD object CalculatorSpec: Spek({ describe("A

    calculator") { val calculator by memoized { Calculator() } describe("addition") { it("returns the sum of its arguments") { assertThat(3, calculator.add(1, 2)) }}}}) 61 Spek
  62. Kotlin. Тестирование. Part 3. Плюшки. BDD object CalculatorSpec: Spek({ describe("A

    calculator") { val calculator by memoized { Calculator() } describe("addition") { it("returns the sum of its arguments") { assertThat(3, calculator.add(1, 2)) }}}}) 62 Spek
  63. Kotlin. Тестирование. Part 3. Плюшки. Matchers 23.should.equal(23) "Kotlin".should.not.contain("Scala") listOf(1,2,3).should.have.size.above(1) 63

    Expekt
  64. Особенности языка 64

  65. Spring + Kotlin. #Security public interface UserDetails extends Serializable {

    String getPassword(); String getUsername(); • В интерфейсе объявлен геттер • В Kotlin их нет • И оверрайдить геттеры нельзя 65
  66. Spring + Kotlin. #Security data class User( private val username:

    String, private val pass: String ): UserDetails { override fun getUsername() = username override fun getPassword() = pass } 66
  67. To run{} or not to run{} class Controller { fun

    apiCall(arg: String): ComplexBusinessDTO { val interim = myService.call(arg) return postProcess(interim) } } 67
  68. To run{} or not to run{} class Controller { fun

    apiCall(arg: String): ComplexBusinessDTO { val interim = myService.call(arg) return postProcess(interim) } } 68 Но ведь есть run{}!
  69. To run{} or not to run{} class Controller { fun

    apiCall(arg: String) = run { val interim = myService.call(arg) postProcess(interim) } } 69
  70. To run{} or not to run{} class Controller { fun

    apiCall(arg: String) = run { val interim = myService.call(arg) postProcess(interim) } } 70 Не делайте так!
  71. Пару слов об интеропе • Всё очень хорошо • Java

    2 Kotlin работает достаточно плохо ◦ Вообще, наверное, не используйте • Котлин не умеет raw-types public class BrowserWebDriverContainer<SELF extends BrowserWebDriverContainer<SELF>> {...} BrowserWebDriverContainer<Nothing>() // Не работают красивые билдеры class KBrowserWebDriverContainer(): // Лапшекод :( BrowserWebDriverContainer<KBrowserWebDriverContainer>() 71
  72. Работа с коллекциями • Стримы тоже — нужна библиотека kotlinx-support-jdk8

    ◦ Работают даже лучше за счёт методов toList() и тд • Стримы не нужны — есть asSequence() ◦ Который работает даже на массивах • Больше функциональных методов ◦ Например, zip 72
  73. Mein kampf с аннотациями data class Person ( @NotNull val

    name: String, @Min(18) val age: Int, @CustomAnno val parents: List<Person>? ) 73 Что может пойти не так?
  74. Mein kampf с аннотациями data class Person ( @NotNull val

    name: String, @Min(18) val age: Double, @CustomAnno val parents: List<Person>? ) 74 Что может пойти не так?
  75. Mein kampf с аннотациями data class Person ( @field:NotNull val

    name: String, @field:Min(18) val age: Double, @field:CustomAnno val parents: List<Person>? ) 75 • Сразу непонятно, но аннотации садятся на аргументы конструктора ◦ О которых hibernate-validator не знает • Нас спасает ключевое слово field • И jackson-module-kotlin • Иногда из-за логики сигнатура типа и аннотация конфликтуют
  76. Функциональщина 76

  77. Как мы свои монады писали sealed class Either<out LEFT, out

    RIGHT> { class Left<LEFT>(private val left: LEFT) : Either<LEFT, Nothing>() { override fun <R> mapLeft(trans: (LEFT) -> R) = Left(trans(left)) override fun <R> mapRight(trans: (Nothing) -> R) = this } class Right<RIGHT>(private val right: RIGHT) : Either<Nothing, RIGHT>() { override fun <R> mapLeft(trans: (Nothing) -> R) = this override fun <R> mapRight(trans: (RIGHT) -> R) = Right(trans(right)) } abstract fun <R> mapLeft(trans: (LEFT) -> R): Either<R, RIGHT> abstract fun <R> mapRight(trans: (RIGHT) -> R): Either<LEFT, R> } 77
  78. Как мы свои монады писали sealed class Either<out LEFT, out

    RIGHT> { class Left<LEFT>(private val left: LEFT) : Either<LEFT, Nothing>() { override fun <R> mapLeft(trans: (LEFT) -> R) = Left(trans(left)) override fun <R> mapRight(trans: (Nothing) -> R) = this } class Right<RIGHT>(private val right: RIGHT) : Either<Nothing, RIGHT>() { override fun <R> mapLeft(trans: (Nothing) -> R) = this override fun <R> mapRight(trans: (RIGHT) -> R) = Right(trans(right)) } abstract fun <R> mapLeft(trans: (LEFT) -> R): Either<R, RIGHT> abstract fun <R> mapRight(trans: (RIGHT) -> R): Either<LEFT, R> } 78
  79. Как мы свои монады писали sealed class Either<out LEFT, out

    RIGHT> { class Left<LEFT>(private val left: LEFT) : Either<LEFT, Nothing>() { override fun <R> mapLeft(trans: (LEFT) -> R) = Left(trans(left)) override fun <R> mapRight(trans: (Nothing) -> R) = this } class Right<RIGHT>(private val right: RIGHT) : Either<Nothing, RIGHT>() { override fun <R> mapLeft(trans: (Nothing) -> R) = this override fun <R> mapRight(trans: (RIGHT) -> R) = Right(trans(right)) } abstract fun <R> mapLeft(trans: (LEFT) -> R): Either<R, RIGHT> abstract fun <R> mapRight(trans: (RIGHT) -> R): Either<LEFT, R> } 79
  80. Как мы свои монады писали sealed class Either<out LEFT, out

    RIGHT> { class Left<LEFT>(private val left: LEFT) : Either<LEFT, Nothing>() { override fun <R> mapLeft(trans: (LEFT) -> R) = Left(trans(left)) override fun <R> mapRight(trans: (Nothing) -> R) = this } class Right<RIGHT>(private val right: RIGHT) : Either<Nothing, RIGHT>() { override fun <R> mapLeft(trans: (Nothing) -> R) = this override fun <R> mapRight(trans: (RIGHT) -> R) = Right(trans(right)) } abstract fun <R> mapLeft(trans: (LEFT) -> R): Either<R, RIGHT> abstract fun <R> mapRight(trans: (RIGHT) -> R): Either<LEFT, R> } 80
  81. Как мы свои монады писали sealed class Either<out LEFT, out

    RIGHT> { class Left<LEFT>(private val left: LEFT) : Either<LEFT, Nothing>() { override fun <R> mapLeft(trans: (LEFT) -> R) = Left(trans(left)) override fun <R> mapRight(trans: (Nothing) -> R) = this } class Right<RIGHT>(private val right: RIGHT) : Either<Nothing, RIGHT>() { override fun <R> mapLeft(trans: (Nothing) -> R) = this override fun <R> mapRight(trans: (RIGHT) -> R) = Right(trans(right)) } abstract fun <R> mapLeft(trans: (LEFT) -> R): Either<R, RIGHT> abstract fun <R> mapRight(trans: (RIGHT) -> R): Either<LEFT, R> } 81
  82. А зачем? Ради цепочек асинхронных операций 82

  83. А зачем? Было CompletableFuture .supplyAsync { someCall() } .exceptionally {

    /* What am I supposed to do here? Blow up? */ } .thenApply { input → process(input) } .thenAccept { result → println(result) } 83
  84. А зачем? Стало CompletableFuture .supplyAsync { eitherTry(someSyncCall()) } .thenApply {

    input → input.mapRight { process(it) } } .thenAccept { when(it) { is Left → handleError() is Right → handleResult() } } 84
  85. В программировании есть две проблемы: инвалидация кешей и именование переменных

    85
  86. Именование переменных • Функции без классов, обычно получают класс FilenameKt

    ◦ Например если функции лежат в Utils.kt – из джавы они будут выглядеть как UtilsKt.functionName() • Иногда нам надо чтобы функция именовалась в джаве иначе • @JvmName to the rescue! 86
  87. Outro 87

  88. Как мы раскатали практику на полкомпании • Количество кода уменьшилось

    • Покрывать его тестами стало гораздо проще • Если весь код написан на Kotlin — то как правило можно не думать о null • Множество библиотек с очень приятным синтаксисом делает код красивее • Этого оказалось достаточно чтобы распространить практику 88
  89. Outro Фатальных проблем нет Те которые есть — решаются, в

    основном без костылей Экосистема огромна Писать приятно, кода становится меньше Рекомендую :) 89
  90. Спасибо за внимание! Остались вопросы? 90