Всем привет. На связи Сергей Окатов. Я руковожу отделом разработки в компании Datana, а также являюсь руководителем курсов Kotlin Backend Developer и Kotlin Developer. Basic в OTUS.
В компании я отвечаю за работу команды разработчиков. Команда небольшая - всего 6 разрабов, но за последний год с небольшим мы с нуля разработали и внедрили пять проектов. Причем это были не детские проектики, а вполне промышленные проекты, которые сейчас начинают свою работу на металлургическом заводе и интегрированы со сталеплавильными установками*. Много это или мало? Чаще всего, от запуска проекта до его внедрения проходит примерно год-два. А тут средняя скорость разработки получается примерно проект за два-три месяца.
Сразу скажу, что выдержать такой темп было нелегко и для достижения поставленной задачи применялась целая серия инструментов, архитектурных и организационных приемов. Но на чем бы я хотел остановиться в текущей заметке - это языки разработки.
Мы в команде применяем для бэкенд-разработки Kotlin и Python. Эти два языка - антиподы друг друга. От правильного распределения задач, возложенных на них, зависит ключевой момент: уложимся ли мы в сроки или контракт будет сорван.
Возьмем Python. Это очень старый язык, который заметно старше даже Java, не говоря уже о Kotlin. Сам в себе он содержит следы смены эпох, и на эти следы постоянно натыкаешься. За всю свою 31-летнюю историю он не пользовался особой популярностью, решая лишь какие-то нишевые задачи. Взрывной рост его популярности случился лишь в последние лет 10 и связан был с прорывом в машинном обучении. Благодаря этому росту Python прорвался во многие смежные экосистемы, например, обработка видео.
Kotlin, напротив - возник лишь 10 лет назад, но гигантскими темпами набирает популярность. В себя он вобрал все лучшие практики опробованные на данный момент, активно поглощает в себя экосистему Java за счет интероперабельности с JVM, а также начал это делать с экосистемами JavaScript и C/C++ за счет интероперабельности с ними в Kotlin Multiplatform. Да, Kotlin явно демонстрирует амбиции в выдавливании Python из темы машинного обучения, но пока говорить о каких-то успехах в этом направлении очень рано.
При выборе задач для Питона и Котлина я выделил три ключевые особенности, которые в итоге и определили архитектуру разработанных систем.
Этот код отлично работает вплоть до последней 9-й строки. На ней он отдает вот такую ошибку:
Понятно, что все подобные ошибки легко решаются и исправляются. Проблема в другом: пока мы не добавим print_hi(12) и не запустим соответствующий участок кода, мы не узнаем, что здесь есть ошибка. Предусматривать подобные расклады приходится самому разработчику и никакой помощи от языка здесь ожидать не приходится.
Да, в 3-й версии Питона появились средства явного указания типов, т.е. авторы языка сделали большой шаг в сторону статической типизации, но на текущий момент эти новые фичи слабо поддерживаются как сообществом, так и интерпретатор довольно лоялен к нарушению типизации (по сравнению со строгими языками).
Вторая проблема динамической типизации - это подсказки IDE. Если явно не задан тип объекта, то и отследить его текущий тип очень часто невозможно. Раз тип неизвестен, то и подсказать доступные методы для переменной тоже невозможно. В таком случае, IDE уже не работает как помощник разработчика, а просто служит текстовым редактором.
Далеко не всегда динамическая типизация создает какие-то проблемы. Если программа небольшая и разработчик способен отследить все изменения типов, то работать с динамическими типами гораздо проще, чем явно описывать все классы и типы.
Но, если ваша программа разрастается до больших размеров, то тут уже IDE часто сдается, перестает помогать и уходит много времени на выяснение типов и доступных методов. Также возникают постоянные проблемы с типами, которые никак не отследить и можно выявить только с помощью модульного тестирования. Модульное тестирование - это нужная и полезная вещь, но динамическая типизация предъявляет повышенные требования к нему и требует большего покрытия тестами кода. И чем больше проект, тем будут больше издержки на отладку и поддержку.
Ну и еще одно обстоятельство характеризует различия в статической и динамической типизации. Если у вас функция принимает аргумент только одного типа, то и обработка этого типа очень простая. А вот если ваша функция начинает принимать разные типы, то под каждый тип необходимо будет описать собственную обработку и предусмотреть гораздо больше вариантов.
Для оценки могу привести грубый пример. Предположим, у нас код построен на динамической типизации. В программе используется целое, строка и два класса. Также возможен еще и None (Null). Если мы сделаем функцию с 10 аргументами, то в самом общем (возможно параноидальном) случае нам нужно будет учитывать влияние аргументов друг на друга и мы должны будем предусмотреть количество вариантов: 10 аргументов, по 5 вариантов каждый, итого 10 в 5-й степени = 100 000. Очевидно, что никто не будет делать такое количество проверок, а непредусмотренные варианты окажутся просто потенциальными багами.
Что предлагает Kotlin - статическую типизацию. При создании переменной или аргумента мы явно фиксируем ее тип. Неявные преобразования запрещены, поэтому мы всегда знаем какой набор функций над этой переменной можно выполнить и какой набор методов у нее можно вызвать. Т.е. в любой самой большой и сложной программе мы всегда знаем что можно сделать с любой переменной, а IDE всегда готова дать подсказки по доступным операциям. Более того, если мы ошибемся и попытаемся вместо строки передать целое, то не то что компилятор, даже IDE подсветит нам ошибку.
При статической типизации приходится каждый раз инвестировать не малые ресурсы в создание структуры классов, но в качестве вознаграждения будет высокая скорость работы с кодом за счет подсказок, сокращения возможных ручных проверок и снижения количества скрытых ошибок, выявляемых только в рантайме.
Отдельно хочется упомянуть Java. Она тоже обладает статической типизацией, но мы выбрали Kotlin. Все дело в том, что, если забыть про большое количество сахара, Kotlin предоставляет еще и большее количество таких вот проверок. Например, нет в Java настолько развитой работы с Null. Также Kotlin более строг при преобразованиях перечислимых (Enum), требуя явно рассматривать все возможные варианты, либо использовать else. Более чувствителен Kotlin к мутабельности переменных и пр. Да, все эти строгости и проверки требуют повышенного внимания и квалификации разработчика, но позволяют прямо во время написания кода выявлять огромное количество вариантов, ветвления логики, что избавляет нас от большей части ошибок еще даже до компиляции, не говоря уж о выкате в прод.
Итого, динамическая типизация более выигрышна для небольших проектов, а статическая больше подходит для крупных, качественных, корпоративных проектов, нагруженных логикой.
Но и в мире JVM можно кое-что сделать для повышения времени старта. Например, очень сильно может помочь отказ от Spring Framework в пользу более современных разработок. Например, в Ktor не используются прокси-классы и вообще рефлексия. Это позволяет стартовать приложению в 5-10 раз быстрее, чем приложению на Spring.
Видео-подсистему с gstreamer и opencv разрабатывать на Java/Kotlin тоже можно, но будет это заметно труднее, чем на Python.
С другой стороны, например, для интеграций есть изумительный Java-фреймворк Apache Camel, который покрывает более 90% всех потребностей в интеграциях с внешними системами. Но при этом, например, эмулятор контроллеров Siemens S7 написан на Python и ничего подобного в JVM стеке не сделать. Так что микросервисная архитектура и сочетание языков нередко - это не блажь, а суровая необходимость.
Но далеко не всегда сторонние библиотеки и фреймворки могут диктовать выбор языка. Например, в бизнес-логике, где нет особых интеграций и специфических библиотек, мы вольны выбирать любой язык, который нам удобнее.
Адаптеры - Kotlin
Это микросервисы, которые отвечают за интеграцию с внешними системами. Их обязанность - принять данные, провалидировать, преобразовать во внутренние форматы системы и отправить дальше.
Фреймворк Apache Camel и статическая типизация Kotlin в данном случае обеспечивают максимальную скорость разработки с учетом довольно сложных структур данных, с которыми приходится работать.
Видеоподсистема и Машинное обучение - Python
Как ни парадоксально, ни то, ни другое практически не содержит сложной бизнес-логики. Проблему сложности некоторых типов данных мы решаем сторонними средствами, включая элементы статической типизации Python, OpenAPI и пр. Практически все микросервисы в этом классе достаточно простые.
Логические микросервисы - Kotlin
Задача логических микросервисов - обеспечить выполнение бизнес-логики. Интеграции в этом случае совершенно не важны, сторонние библиотеки практически не используются. Но что есть в этих компонентах - это большие и сложные структуры данных, большое количество бизнес-операций, обработка условий, машины состояний и многие другие элементы бизнес-логики.
И вот в этих условиях лучшим образом показывает себя именно Kotlin. Да, формирование статических типов и классов требует значительного времени. Но формирование этих структур - это и есть та самая бизнес-логика, которую мы должны разработать. Формируя статические типы, мы отсекаем тысячи вариантов других возможных вариантов, которые в Python нам бы пришлось проверять вручную и с помощью модульных тестов. И когда структура классов сформирована, Kotlin даже на этапе набора текста программы обеспечивает подсказки IDE. Затем на этапе компиляции автоматически выявляются ошибки и несогласованности. Например, если у нас изменится API, то мы обнаружим ошибки не в рантайме на продуктовой площадке, а еще на этапе компиляции программ.
И именно благодаря Kotlin большая часть ошибок, которые мы исправляли в ходе тестирования и внедрения, носила именно бизнесовый характер, а не системный типа фатального падения программы из-за Null Pointer Exception. Честно говоря, за весь период работы NPE мы практически не видели.
Вспомогательные сервисные скрипты - Python
В любом проекте нередко возникают различные сервисные задачи, будь то переименование тысячи файлов или разовая обработка каких-то данных. Эти скрипты никогда не идут на продуктовые сервера, но они очень помогают делать рутинную работу. В подобных скриптах нет нужды предусматривать тысячи логических вариантов, они компактные, а типы и переменные из-за этого наглядны. Питон для таких скриптов подходит идеально.
В компании я отвечаю за работу команды разработчиков. Команда небольшая - всего 6 разрабов, но за последний год с небольшим мы с нуля разработали и внедрили пять проектов. Причем это были не детские проектики, а вполне промышленные проекты, которые сейчас начинают свою работу на металлургическом заводе и интегрированы со сталеплавильными установками*. Много это или мало? Чаще всего, от запуска проекта до его внедрения проходит примерно год-два. А тут средняя скорость разработки получается примерно проект за два-три месяца.
Сразу скажу, что выдержать такой темп было нелегко и для достижения поставленной задачи применялась целая серия инструментов, архитектурных и организационных приемов. Но на чем бы я хотел остановиться в текущей заметке - это языки разработки.
Мы в команде применяем для бэкенд-разработки Kotlin и Python. Эти два языка - антиподы друг друга. От правильного распределения задач, возложенных на них, зависит ключевой момент: уложимся ли мы в сроки или контракт будет сорван.
Почему вообще два языка, а не один?
Как я уже указал выше, эти два языка - антиподы друг друга. У меня вообще не укладывается в голове как они могут конкурировать друг с другом в каких-то командах. Заточены Kotlin и Python под совершенно разные задачи.Возьмем Python. Это очень старый язык, который заметно старше даже Java, не говоря уже о Kotlin. Сам в себе он содержит следы смены эпох, и на эти следы постоянно натыкаешься. За всю свою 31-летнюю историю он не пользовался особой популярностью, решая лишь какие-то нишевые задачи. Взрывной рост его популярности случился лишь в последние лет 10 и связан был с прорывом в машинном обучении. Благодаря этому росту Python прорвался во многие смежные экосистемы, например, обработка видео.
Kotlin, напротив - возник лишь 10 лет назад, но гигантскими темпами набирает популярность. В себя он вобрал все лучшие практики опробованные на данный момент, активно поглощает в себя экосистему Java за счет интероперабельности с JVM, а также начал это делать с экосистемами JavaScript и C/C++ за счет интероперабельности с ними в Kotlin Multiplatform. Да, Kotlin явно демонстрирует амбиции в выдавливании Python из темы машинного обучения, но пока говорить о каких-то успехах в этом направлении очень рано.
При выборе задач для Питона и Котлина я выделил три ключевые особенности, которые в итоге и определили архитектуру разработанных систем.
- Типизация. Python характеризуется динамической типизацией, т.е., создав переменную x, ей в любой момент можно присвоить хоть целое, хоть строку, хоть любой объект. Kotlin же весь построен на парадигме статической типизации. Т.е., объявив переменную x с типом String, мы уже не сможем ей присвоить ничего кроме строки.
- Время старта. Не смотря на то, что Python при старте выполняет компиляцию, программы на нем стартуют гораздо быстрее, чем JVM.
- Экосистема. Kotlin, конечно - это очень молодой язык, но он вырос на базе экосистемы JVM. А JVM и Python - это довольно старые и развитые экосистемы. Большую часть инструментов поддерживают обе. Тем не менее, всегда существуют нюансы. В некоторых проектах больше поддерживается Python и меньше JVM, в других - наоборот. Поэтому, каждый раз приходится взвешивать все “за” и “против”, делая выбор в пользу того или иного языка на каждом новом микросервисе.
Типизация
Возьмем простую программу:Этот код отлично работает вплоть до последней 9-й строки. На ней он отдает вот такую ошибку:
Понятно, что все подобные ошибки легко решаются и исправляются. Проблема в другом: пока мы не добавим print_hi(12) и не запустим соответствующий участок кода, мы не узнаем, что здесь есть ошибка. Предусматривать подобные расклады приходится самому разработчику и никакой помощи от языка здесь ожидать не приходится.
Да, в 3-й версии Питона появились средства явного указания типов, т.е. авторы языка сделали большой шаг в сторону статической типизации, но на текущий момент эти новые фичи слабо поддерживаются как сообществом, так и интерпретатор довольно лоялен к нарушению типизации (по сравнению со строгими языками).
Вторая проблема динамической типизации - это подсказки IDE. Если явно не задан тип объекта, то и отследить его текущий тип очень часто невозможно. Раз тип неизвестен, то и подсказать доступные методы для переменной тоже невозможно. В таком случае, IDE уже не работает как помощник разработчика, а просто служит текстовым редактором.
Далеко не всегда динамическая типизация создает какие-то проблемы. Если программа небольшая и разработчик способен отследить все изменения типов, то работать с динамическими типами гораздо проще, чем явно описывать все классы и типы.
Но, если ваша программа разрастается до больших размеров, то тут уже IDE часто сдается, перестает помогать и уходит много времени на выяснение типов и доступных методов. Также возникают постоянные проблемы с типами, которые никак не отследить и можно выявить только с помощью модульного тестирования. Модульное тестирование - это нужная и полезная вещь, но динамическая типизация предъявляет повышенные требования к нему и требует большего покрытия тестами кода. И чем больше проект, тем будут больше издержки на отладку и поддержку.
Ну и еще одно обстоятельство характеризует различия в статической и динамической типизации. Если у вас функция принимает аргумент только одного типа, то и обработка этого типа очень простая. А вот если ваша функция начинает принимать разные типы, то под каждый тип необходимо будет описать собственную обработку и предусмотреть гораздо больше вариантов.
Для оценки могу привести грубый пример. Предположим, у нас код построен на динамической типизации. В программе используется целое, строка и два класса. Также возможен еще и None (Null). Если мы сделаем функцию с 10 аргументами, то в самом общем (возможно параноидальном) случае нам нужно будет учитывать влияние аргументов друг на друга и мы должны будем предусмотреть количество вариантов: 10 аргументов, по 5 вариантов каждый, итого 10 в 5-й степени = 100 000. Очевидно, что никто не будет делать такое количество проверок, а непредусмотренные варианты окажутся просто потенциальными багами.
Что предлагает Kotlin - статическую типизацию. При создании переменной или аргумента мы явно фиксируем ее тип. Неявные преобразования запрещены, поэтому мы всегда знаем какой набор функций над этой переменной можно выполнить и какой набор методов у нее можно вызвать. Т.е. в любой самой большой и сложной программе мы всегда знаем что можно сделать с любой переменной, а IDE всегда готова дать подсказки по доступным операциям. Более того, если мы ошибемся и попытаемся вместо строки передать целое, то не то что компилятор, даже IDE подсветит нам ошибку.
При статической типизации приходится каждый раз инвестировать не малые ресурсы в создание структуры классов, но в качестве вознаграждения будет высокая скорость работы с кодом за счет подсказок, сокращения возможных ручных проверок и снижения количества скрытых ошибок, выявляемых только в рантайме.
Отдельно хочется упомянуть Java. Она тоже обладает статической типизацией, но мы выбрали Kotlin. Все дело в том, что, если забыть про большое количество сахара, Kotlin предоставляет еще и большее количество таких вот проверок. Например, нет в Java настолько развитой работы с Null. Также Kotlin более строг при преобразованиях перечислимых (Enum), требуя явно рассматривать все возможные варианты, либо использовать else. Более чувствителен Kotlin к мутабельности переменных и пр. Да, все эти строгости и проверки требуют повышенного внимания и квалификации разработчика, но позволяют прямо во время написания кода выявлять огромное количество вариантов, ветвления логики, что избавляет нас от большей части ошибок еще даже до компиляции, не говоря уж о выкате в прод.
Итого, динамическая типизация более выигрышна для небольших проектов, а статическая больше подходит для крупных, качественных, корпоративных проектов, нагруженных логикой.
Время старта
Далеко не всегда время старта является критическим. Чаще всего, микросервис один раз запускается и успешно функционирует месяцами. Но все-таки иногда время старта важно. Например, когда нагрузка на систему неожиданно возросла и планировщик принял решение поднять еще один под. Холодный старт в этом случае сколько займет времени? Одну секунду или минуту? Питон, очевидно, в этом плане явно лидирует.Но и в мире JVM можно кое-что сделать для повышения времени старта. Например, очень сильно может помочь отказ от Spring Framework в пользу более современных разработок. Например, в Ktor не используются прокси-классы и вообще рефлексия. Это позволяет стартовать приложению в 5-10 раз быстрее, чем приложению на Spring.
Экосистема
Как я уже указал выше, есть масса нюансов в каждой экосистеме. Например, Java-обертки для PyTorch или Tensorflow все-таки не так развиты как их Python-версии. Да и найти инженера по машинному обучению/Data scientist-а, готового освоить Java/Kotlin не так просто. Большинству DS-ов просто удобнее работать на Питоне и вряд ли стоит нам - разработчикам системы - с этим что-то делать.Видео-подсистему с gstreamer и opencv разрабатывать на Java/Kotlin тоже можно, но будет это заметно труднее, чем на Python.
С другой стороны, например, для интеграций есть изумительный Java-фреймворк Apache Camel, который покрывает более 90% всех потребностей в интеграциях с внешними системами. Но при этом, например, эмулятор контроллеров Siemens S7 написан на Python и ничего подобного в JVM стеке не сделать. Так что микросервисная архитектура и сочетание языков нередко - это не блажь, а суровая необходимость.
Но далеко не всегда сторонние библиотеки и фреймворки могут диктовать выбор языка. Например, в бизнес-логике, где нет особых интеграций и специфических библиотек, мы вольны выбирать любой язык, который нам удобнее.
Распределение обязанностей
В итоге, у нас сформировалось такое разделение языков по компонентам систем.Адаптеры - Kotlin
Это микросервисы, которые отвечают за интеграцию с внешними системами. Их обязанность - принять данные, провалидировать, преобразовать во внутренние форматы системы и отправить дальше.
Фреймворк Apache Camel и статическая типизация Kotlin в данном случае обеспечивают максимальную скорость разработки с учетом довольно сложных структур данных, с которыми приходится работать.
Видеоподсистема и Машинное обучение - Python
Как ни парадоксально, ни то, ни другое практически не содержит сложной бизнес-логики. Проблему сложности некоторых типов данных мы решаем сторонними средствами, включая элементы статической типизации Python, OpenAPI и пр. Практически все микросервисы в этом классе достаточно простые.
Логические микросервисы - Kotlin
Задача логических микросервисов - обеспечить выполнение бизнес-логики. Интеграции в этом случае совершенно не важны, сторонние библиотеки практически не используются. Но что есть в этих компонентах - это большие и сложные структуры данных, большое количество бизнес-операций, обработка условий, машины состояний и многие другие элементы бизнес-логики.
И вот в этих условиях лучшим образом показывает себя именно Kotlin. Да, формирование статических типов и классов требует значительного времени. Но формирование этих структур - это и есть та самая бизнес-логика, которую мы должны разработать. Формируя статические типы, мы отсекаем тысячи вариантов других возможных вариантов, которые в Python нам бы пришлось проверять вручную и с помощью модульных тестов. И когда структура классов сформирована, Kotlin даже на этапе набора текста программы обеспечивает подсказки IDE. Затем на этапе компиляции автоматически выявляются ошибки и несогласованности. Например, если у нас изменится API, то мы обнаружим ошибки не в рантайме на продуктовой площадке, а еще на этапе компиляции программ.
И именно благодаря Kotlin большая часть ошибок, которые мы исправляли в ходе тестирования и внедрения, носила именно бизнесовый характер, а не системный типа фатального падения программы из-за Null Pointer Exception. Честно говоря, за весь период работы NPE мы практически не видели.
Вспомогательные сервисные скрипты - Python
В любом проекте нередко возникают различные сервисные задачи, будь то переименование тысячи файлов или разовая обработка каких-то данных. Эти скрипты никогда не идут на продуктовые сервера, но они очень помогают делать рутинную работу. В подобных скриптах нет нужды предусматривать тысячи логических вариантов, они компактные, а типы и переменные из-за этого наглядны. Питон для таких скриптов подходит идеально.
Итоги
JVM и Python - это две наиболее популярные экосистемы в бэкенд-разработке на сегодня. Знаю, что во многих компаниях эти два инструмента конкурируют друг с другом, что выражается в подходах “Все на Python” или “Все на Java”, но я считаю, что конкуренция здесь неуместна. Эти два языка очень разные и занимают совершенно разные рыночные ниши. Python больше подходит для прототипирования, а Kotlin - для крупной разработки со сложной и нагруженной бизнес-логикой. Сочетание преимуществ каждого из языков дает сочетание высоких темпов разработки и высокого качества кода.Kotlin и Python в одном проекте
Всем привет. На связи Сергей Окатов. Я руковожу отделом разработки в компании Datana , а также являюсь руководителем курсов Kotlin Backend Developer и Kotlin Developer. Basic в OTUS. В компании я...
habr.com