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

Павел Финкельштейн — Kotlin: Два года в проде и...

Павел Финкельштейн — Kotlin: Два года в проде и ни единого разрыва

Об истории знакомства с Kotlin, экспериментах с ним и применении его в production. Основные аспекты работы в enterprise с Kotlin, с какими трудностями пришлось встретиться и как их удалось решить. Про interop и про то, правда ли он настолько хорош, как кажется.

Avatar for Moscow JUG

Moscow JUG

March 13, 2019
Tweet

More Decks by Moscow JUG

Other Decks in Programming

Transcript

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

    Финкельштейн Разработчик, тимлид
  2. Отказ от гарантий • Я никак не аффилирован с JetBrains

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

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

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

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

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

    на котлине? • А риски? Этот доклад — это наш путь и опыт 11
  8. 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
  9. 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
  10. 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
  11. 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
  12. 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 Что может пойти не так?
  13. 18

  14. 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
  15. 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
  16. Spring + Kotlin. Part 1. @Transactional Проблема • Всё final

    • Прокси не создаются • Конфигурации не работают Решения • kotlin-allopen + kotlin-spring ◦ The code is a lie ⇒ сложное кодревью ◦ Конструкция хрупкая • Всюду явно писать open ◦ boilerplate 22
  17. 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
  18. 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
  19. 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
  20. Исключения в 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; }
  21. Исключения в 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; }
  22. Исключения в 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; }
  23. Исключения в 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; }
  24. Проблема • Нет checked exceptions Решения • Следить за исключениями

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

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

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

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

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

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

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

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

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

    Disclaimer: у меня нет опыта с этим Проблема: no-arg конструкторов в data-классах нет Решение 1: не использовать data-классы Абыдна 41
  34. Мы ынтырпрайз, у нас 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
  35. Мы ынтырпрайз, у нас 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
  36. Мы ынтырпрайз, у нас 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
  37. Мы ынтырпрайз, у нас 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
  38. Kotlin + JUnit. Тестирование. Part 1. Что нужно знать: •

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

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

    возвращает null • это плохо потому что на обращении к объекту Kotlin проверяет что объект — не null • anyString() тоже! • when — ключевое слово
  41. 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
  42. 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
  43. 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
  44. 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
  45. 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
  46. 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
  47. Kotlin. Тестирование. Part 3. Плюшки. Понятный нейминг fun `I am

    readable unit test name which describes what is tested`(){ assertTrue(true) } 56
  48. 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
  49. 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
  50. 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
  51. 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
  52. 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
  53. 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
  54. Spring + Kotlin. #Security public interface UserDetails extends Serializable {

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

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

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

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

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

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

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

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

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

    name: String, @Min(18) val age: Double, @CustomAnno val parents: List<Person>? ) 74 Что может пойти не так?
  64. 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 • Иногда из-за логики сигнатура типа и аннотация конфликтуют
  65. Как мы свои монады писали 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
  66. Как мы свои монады писали 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
  67. Как мы свои монады писали 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
  68. Как мы свои монады писали 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
  69. Как мы свои монады писали 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
  70. А зачем? Было CompletableFuture .supplyAsync { someCall() } .exceptionally {

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

    input → input.mapRight { process(it) } } .thenAccept { when(it) { is Left → handleError() is Right → handleResult() } } 84
  72. Именование переменных • Функции без классов, обычно получают класс FilenameKt

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

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

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