
Издательство Sprint book представляет третье издание книги «Kotlin. Паттерны проектирования и лучшие практики» от Алексея Сошина — опытного архитектора ПО и эксперта в Kotlin. Это руководство станет незаменимым помощником для разработчиков, которые хотят не только изучить классические и современные паттерны проектирования, но и научиться применять их в реальных проектах на Kotlin.
Книга охватывает все ключевые аспекты языка, начиная с базового синтаксиса и заканчивая продвинутыми концепциями, такими как структурированная конкурентность, контекстные приемники и реактивное программирование. Особое внимание уделено актуальным обновлениям Kotlin, включая версии 1.6 и 2.0, а также популярным библиотекам, таким как Arrow, Ktor и Vert.x.
Об авторе
Алексей Сошин — архитектор программного обеспечения. Работает в айти уже более 18 лет. Начал изучать язык Kotlin после выхода его бета-версии и с тех пор остается его ярым приверженцем. Выступает на конференциях, является автором множества публикаций и создателем видеокурса Pragmatic System Design.
О научных редакторах русского издания
Евгений Войнов — тимлид бэкенд-разработчиков на Java и Kotlin в «Яндекс.Маркет». Имеет более 15 лет опыта разработки и поддержки систем для коммерческого и государственного секторов. Ментор разработчиков, преподаватель учебных программ для студентов, разработчик внутренних экзаменов по Java.
Сергей Задорожный — руководитель Platform Engineering и Enabling Team в банке «Центр-Инвест». Разрабатывал бэкенд на Java/Kotlin, сейчас внедряет DevOps-практики. Ведет Telegram-канал IT Friday.
Вячеслав Иванов — техлид Platform Engineering и Enabling Team в банке «Центр-Инвест». Разрабатывал на Java/Kotlin. Сейчас занимается внедрением DevOps-практик. Любит Kotlin и архитектуру.
Анатолий Нечай-Гумен — техлид команды бэкенд-разработки в банке «Центр-Инвест». Разрабатывает бэкенд на Kotlin/Java.
Сергей Задорожный — руководитель Platform Engineering и Enabling Team в банке «Центр-Инвест». Разрабатывал бэкенд на Java/Kotlin, сейчас внедряет DevOps-практики. Ведет Telegram-канал IT Friday.
Вячеслав Иванов — техлид Platform Engineering и Enabling Team в банке «Центр-Инвест». Разрабатывал на Java/Kotlin. Сейчас занимается внедрением DevOps-практик. Любит Kotlin и архитектуру.
Анатолий Нечай-Гумен — техлид команды бэкенд-разработки в банке «Центр-Инвест». Разрабатывает бэкенд на Kotlin/Java.
Почему для Kotlin так важны паттерны проектирования важны?
При разработке программного обеспечения (ПО) часто возникает ряд проблем, решить которые призваны паттерны проектирования. Основанные на коллективном опыте множества разработчиков, эти паттерны предоставляют лучшие практики и проверенные решения для конкретных повторяющихся задач проектирования ПО, которые можно адаптировать к различным ситуациям. Благодаря паттернам разработчики получают набор терминов, облегчающих общение, сотрудничество и сопровождение кода. Умение распознавать и правильно использовать паттерны остается критически важным навыком для профессионального разработчика.
Kotlin — универсальный язык, который охватывает множество парадигм программирования. Он был разработан компанией JetBrains, которая известна созданием широко используемых интегрированных сред разработки.
Kotlin, будучи современным языком, естественным образом интегрирует многие из этих паттернов прямо в свой синтаксис, что делает их применение более естественным. Однако умение распознавать и правильно использовать паттерны остается критически важным навыком для профессионального разработчика.
Экосистема Kotlin постоянно развивается, что и привело к публикации третьего обновленного издания. Оно посвящено нововведениям, появившимся в Kotlin вплоть до версии 2.0, и описывает некоторые из наиболее интересных функций, такие как контекстные приемники (context receivers) и важные библиотеки наподобие Arrow.
Основная цель этой книги — познакомить читателя с классическими паттернами проектирования, независимо от того, слышит ли он о них впервые или пытается реализовать их с помощью Kotlin, имея опыт использования их в других языках.
Содержание книги с подробной информацией о главах
Глава 1: Знакомство с Kotlin
В первой главе автор знакомит читателя с базовым синтаксисом Kotlin, уделяя особое внимание фундаментальным концепциям и идиомам языка. Вместо сухого перечисления возможностей книга фокусируется на тех аспектах, которые важны для понимания паттернов проектирования. Это позволяет подготовить читателя к более сложным темам, которые рассматриваются в последующих главах.
Глава 2: Порождающие паттерны
Здесь разбираются классические порождающие паттерны, которые уже встроены в Kotlin, а также те, которые требуют ручной реализации. Автор объясняет, как эффективно управлять созданием объектов, делая код более гибким и удобным для сопровождения.
Глава 3: Структурные паттерны
Структурные паттерны помогают расширять функциональность объектов. В этой главе показано, как применять классические структурные паттерны в Kotlin для написания более надежного, модульного и адаптируемого кода.
Глава 4: Поведенческие паттерны
Поведенческие паттерны определяют, как объекты взаимодействуют друг с другом. В этой главе рассматривается, как объект может проявлять различное поведение в зависимости от ситуации, как объекты могут взаимодействовать, не имея знаний друг о друге, а также вы познакомитесь с простыми способами итерирования сложных структур. Эти паттерны позволяют создавать более гибкий и пригодный для повторного использования код, который легко поддерживать на протяжении длительного времени.
Глава 5: Введение в функциональное программирование
В этой главе автор объясняет ключевые принципы функционального программирования, не углубляясь в новый синтаксис: неизменяемость данных и использование функций в качестве объектов первого класса. В книге идет упор на важности их роли в создании более компактного, модульного и удобного для сопровождения кода.
Глава 6: Потоки и корутины
Потоки традиционно используются для обеспечения конкурентного выполнения кода в современных программах, однако Kotlin предлагает корутины как более эффективный вариант. В этой главе рассматриваются преимущества использования корутин, способы их реализации, а также структурированная конкурентность, которая делает асинхронные операции более безопасными и эффективными.
Глава 7: Управление потоком данных
Здесь обсуждаются функции высшего порядка, каналы и потоки, которые обеспечивают конкурентные и реактивные решения, задействующие функции высшего порядка.
Глава 8: Конкурентные паттерны
Эта глава посвящена наиболее широко используемым паттернам проектирования, которые помогают одновременно управлять несколькими задачами. А также содержится информация о том, как корутины синхронизируют свое выполнение, чтобы избежать состояния гонки и гарантировать потоковую безопасность.
Глава 9: Идиомы и антипаттерны
Автор разбирает более и менее оптимальные практики написания кода на Kotlin, а также то, какой стиль написания кода предпочтителен, какие паттерны кодирования не рекомендуется использовать. В этой главе рекомендации для создания легкого для чтения и сопровождения кода, а также разбор типичных ошибок при его написании, которые можно избежать.
Глава 10: Практическое функциональное программирование с Arrow
Фреймворк Arrow позволяет kotlin-разработчикам получить единообразный и идиоматический опыт функционального программирования, делая его доступным для всех. В этой главе показано на практике, как использовать его для создания более компактного, выразительного и удобного в сопровождении кода.
Глава 11: Конкурентные микросервисы с Ktor
На практическом примере создания микросервиса с помощью фреймворка Ktor автор демонстрирует применение всех изученных концепций.
Глава 12: Реактивные микросервисы с Vert.x
В завершающей главе рассматривается альтернативный подход к разработке микросервисов с использованием фреймворка Vert.x, основанного на реактивных паттернах проектирования.
В первой главе автор знакомит читателя с базовым синтаксисом Kotlin, уделяя особое внимание фундаментальным концепциям и идиомам языка. Вместо сухого перечисления возможностей книга фокусируется на тех аспектах, которые важны для понимания паттернов проектирования. Это позволяет подготовить читателя к более сложным темам, которые рассматриваются в последующих главах.
Глава 2: Порождающие паттерны
Здесь разбираются классические порождающие паттерны, которые уже встроены в Kotlin, а также те, которые требуют ручной реализации. Автор объясняет, как эффективно управлять созданием объектов, делая код более гибким и удобным для сопровождения.
Глава 3: Структурные паттерны
Структурные паттерны помогают расширять функциональность объектов. В этой главе показано, как применять классические структурные паттерны в Kotlin для написания более надежного, модульного и адаптируемого кода.
Глава 4: Поведенческие паттерны
Поведенческие паттерны определяют, как объекты взаимодействуют друг с другом. В этой главе рассматривается, как объект может проявлять различное поведение в зависимости от ситуации, как объекты могут взаимодействовать, не имея знаний друг о друге, а также вы познакомитесь с простыми способами итерирования сложных структур. Эти паттерны позволяют создавать более гибкий и пригодный для повторного использования код, который легко поддерживать на протяжении длительного времени.
Глава 5: Введение в функциональное программирование
В этой главе автор объясняет ключевые принципы функционального программирования, не углубляясь в новый синтаксис: неизменяемость данных и использование функций в качестве объектов первого класса. В книге идет упор на важности их роли в создании более компактного, модульного и удобного для сопровождения кода.
Глава 6: Потоки и корутины
Потоки традиционно используются для обеспечения конкурентного выполнения кода в современных программах, однако Kotlin предлагает корутины как более эффективный вариант. В этой главе рассматриваются преимущества использования корутин, способы их реализации, а также структурированная конкурентность, которая делает асинхронные операции более безопасными и эффективными.
Глава 7: Управление потоком данных
Здесь обсуждаются функции высшего порядка, каналы и потоки, которые обеспечивают конкурентные и реактивные решения, задействующие функции высшего порядка.
Глава 8: Конкурентные паттерны
Эта глава посвящена наиболее широко используемым паттернам проектирования, которые помогают одновременно управлять несколькими задачами. А также содержится информация о том, как корутины синхронизируют свое выполнение, чтобы избежать состояния гонки и гарантировать потоковую безопасность.
Глава 9: Идиомы и антипаттерны
Автор разбирает более и менее оптимальные практики написания кода на Kotlin, а также то, какой стиль написания кода предпочтителен, какие паттерны кодирования не рекомендуется использовать. В этой главе рекомендации для создания легкого для чтения и сопровождения кода, а также разбор типичных ошибок при его написании, которые можно избежать.
Глава 10: Практическое функциональное программирование с Arrow
Фреймворк Arrow позволяет kotlin-разработчикам получить единообразный и идиоматический опыт функционального программирования, делая его доступным для всех. В этой главе показано на практике, как использовать его для создания более компактного, выразительного и удобного в сопровождении кода.
Глава 11: Конкурентные микросервисы с Ktor
На практическом примере создания микросервиса с помощью фреймворка Ktor автор демонстрирует применение всех изученных концепций.
Глава 12: Реактивные микросервисы с Vert.x
В завершающей главе рассматривается альтернативный подход к разработке микросервисов с использованием фреймворка Vert.x, основанного на реактивных паттернах проектирования.
Почему стоит обратить внимание на эту книгу?
- С «Kotlin. Паттерны проектирования и лучшие практики» вы познакомитесь с классическими паттернами проектирования, а также освоите их применение в Kotlin на практике.
- Узнаете, как эффективно масштабировать приложения, используя реактивные и конкурентные паттерны проектирования.
- Книга объяснит передовые практики Kotlin, включая работу с его новейшими возможностями, чтобы ваш код оставался актуальным и соответствовал последним тенденциям языка. Исследуйте его новые возможности.
- Вы глубоко погрузитесь в принципы функционального программирования и научитесь применять их в Kotlin.
- Наконец, овладеете искусством написания идиоматического кода на Kotlin, а также узнаете, какие паттерны на самом деле могут оказаться антипаттернами — это убережет вас от типичных ошибок и неэффективных решений.
Предлагаем познакомиться с отрывком из книги об управлении потоком данных.
В этой главе вы познакомитесь с функциями, улучшающими процессы обработки данных и взаимодействия между различными корутинами. Эта глава посвящена таким важным инструментам Kotlin для обеспечения конкурентности, как каналы и асинхронные потоки. Помимо всего прочего, мы рассмотрим функции высшего порядка для работы с коллекциями, отметив сходство их API с API каналов и асинхронных потоков.
Опираясь на фундамент, заложенный при обсуждении парадигмы функционального программирования, в этой главе мы поговорим о применении небольших, многократно используемых и составных функций. Они помогают писать выразительный код, который дает четкое описание того, чего мы хотим достичь, а не конкретных шагов, необходимых для этого.
Изучив эту главу, вы получите знания и инструменты, позволяющие обеспечить
эффективное взаимодействие между различными корутинами и оптимизировать
процесс обработки данных.
Принципы реактивного программирования
Мы начнем главу с обсуждения принципов реактивного программирования. Эта концепция лежит в основе потоковой передачи данных и поможет вам понять материал текущей главы. Реактивное программирование, вырастающее из функционального, позволяет структурировать логику в виде серии операций, выполняемых над потоком данных. Основные постулаты этого подхода изложены в «Манифесте реактивных систем» (Reactive Manifesto).
Согласно этому манифесту, реактивная программа должна обладать такими четырьмя ключевыми качествами, как:
- отзывчивость;
- устойчивость;
- гибкость (эластичность);
- способность обмениваться сообщениями.
Отзывчивость
Количество времени, которое вы готовы потратить на ожидание ответа оператора, зависит от ваших обстоятельств, например от того, насколько срочным является ваш вопрос, и от степени вашей занятости. Если у вас мало времени, то вы, скорее всего, повесите трубку раньше, особенно если не знаете, сколько еще вам предстоит слушать музыку.
Эта ситуация демонстрирует работу неотзывчивой системы. Похожие сценарии проигрываются и в цифровой сфере: например, веб-запрос может ожидать в очереди, пока сервер обрабатывает другие запросы, оставляя вас в неведении относительно того, когда же ваш запрос будет наконец обработан.
И наоборот, отзывчивая система может предусматривать периодические обновления во время ожидания, информируя вас о месте вашего запроса в очереди или даже оценивая количество оставшегося времени ожидания. В обоих рассмотренных примерах вам пришлось ждать, однако вторая система по крайней мере сообщила вам какую-то информацию.
Располагая этими сведениями, вы можете принять взвешенное решение о том, стоит ли продолжать ждать или лучше переключиться на другое занятие. Именно в этом и заключается суть принципа отзывчивости — одного из ключевых постулатов, изложенных в «Манифесте реактивных систем».
Устойчивость
Суть принципа устойчивости заключается в том, насколько хорошо система способна справляться со сбоями, восстанавливаться после них и продолжать работу. Звонок, сброшенный после долгого ожидания, — прекрасный пример системы, не обладающей достаточной устойчивостью.
В «Манифесте реактивных систем» изложены следующие стратегии создания устойчивых систем.
- Делегирование. Если представитель службы поддержки не может решить вашу проблему, то система перенаправляет вас к тому, кто может это сделать. Такое делегирование гарантирует, что, если одна часть системы не сможет удовлетворить потребность клиента, это сможет сделать другая.
- Репликация. Система, управляющая работой кол-центра, может быть спроектирована таким образом, чтобы изменять численность сотрудников в зависимости от рабочей нагрузки. Если в очереди оказывается много звонящих, то она может подключить дополнительных представителей службы поддержки для обработки этих звонков. Такое поведение является проявлением «гибкости» (эластичности), о которой мы поговорим чуть позже.
- Сдерживание и изоляция. Автоответчик, позволяющий звонящему оставить свой номер телефона для получения обратного звонка, служит двум целям.
— Сдерживание. Заказ обратного звонка избавляет вас от необходимости иметь дело с существующими ограничениями системы, такими как нехватка свободных представителей.
— Изоляция. Если в системе возникли проблемы, например связанные с работой телефонной линии, то вы будете защищены и изолированы от этих системных проблем, поскольку компания сможет связаться с вами позднее.
Гибкость
Гибкость (эластичность) — способность системы динамически регулировать количество используемых ресурсов в зависимости от изменения спроса или рабочей нагрузки. Возможность увеличивать или уменьшать количество выделенных ресурсов по мере необходимости позволяет системе поддерживать оптимальный уровень производительности и удобства для пользователей, а также минимизировать затраты. По сути, благодаря эластичности системы вы можете платить только за те ресурсы, которые необходимы в каждый конкретный момент, вместо того чтобы постоянно нести затраты, характерные для периодов пиковой нагрузки.
В нашем сценарии внезапный всплеск звонков был обусловлен массовым сбоем в работе интернет-провайдера, вызванным тем, что крот повредил кабель. Гибкая система способна масштабироваться, например, путем привлечения дополнительных представителей службы поддержки для обработки возросшего количества звонков. Это гарантирует своевременное решение проблем клиентов. Когда проблема будет устранена и количество звонков вернется к нормальному уровню, система сможет снова масштабироваться, позволив освободившимся сотрудникам вернуться к другим задачам.
Гибкость тесно связана с масштабируемостью. Чтобы быть масштабируемой, система кол-центра должна иметь инфраструктуру, предусматривающую возможность увеличения количества представителей службы поддержки, например дополнительные телефоны и рабочие станции. Если телефонов оказывается меньше, чем сотрудников, то данный фактор становится узким местом, ограничивающим способность системы эффективно обрабатывать возросшее количество
звонков.
Таким образом, гибкая (эластичная) система характеризуется не только наличием дополнительных ресурсов, но и возможностью динамически задействовать и освобождать эти ресурсы, обеспечивая их эффективное использование при сохранении высокого уровня обслуживания.
Способность обмениваться сообщениями
Концепция обмена сообщениями является базовой для создания реактивных систем. Архитектура, основанная на обмене сообщениями, обеспечивает асинхронную коммуникацию и тем самым способствует реализации многих других реактивных принципов, таких как устойчивость и гибкость.
Если клиенты взаимодействуют с системой только с помощью сообщений, это позволяет отделить время получения запроса от времени его обработки, что способствует более эффективному управлению ресурсами системы. Представители службы поддержки могут группировать сообщения или приоритизировать их на основе таких критериев, как срочность или тип проблемы, что повышает общую продуктивность.
Еще один важный аспект систем, основанных на обмене сообщениями, — обратное давление. Когда система обнаруживает, что перегружена сообщениями, она может использовать механизмы, позволяющие замедлить прием новых сообщений или отменить часть задач ради поддержания своей целостности.
Более того, модель, основанная на обмене сообщениями, допускает неблокирующие взаимодействия. После отправки сообщения его отправитель и получатель могут заниматься другими делами. Это повышает не только отзывчивость системы, но и степень ее конкурентности, поскольку несколько задач могут обрабатываться одновременно, не дожидаясь завершения других задач.
Важно и то, что архитектура, основанная на обмене сообщениями, допускает делегирование. Когда одна часть системы оказывается перегруженной, ее задачи могут быть переданы другим частям, имеющим доступные ресурсы, что делает систему более устойчивой и гибкой.
В целом принцип обмена сообщениями хорошо согласуется с другими принципами реактивных систем, делая их более отзывчивыми, устойчивыми и гибкими (эластичными).
Далее мы поговорим об инструментах и библиотеках Kotlin, позволяющих создавать системы, основанные на вышеописанных принципах. Начнем с рассмотрения коллекции в качестве статического потока данных.
Функции высшего порядка для работы с коллекциями
Мы уже говорили на эту тему в главе 1, но прежде, чем обсуждать потоки данных, я должен убедиться, что читатели, владеющие языками, не предусматривающими функции высшего порядка для работы с коллекциями, знают, что это такое, что они делают и в чем заключаются преимущества их использования.
Функции высшего порядка для работы с коллекциями — это мощная возможность, которую предоставляет Kotlin (и некоторые другие современные языки программирования). Они упрощают код, повышают его удобочитаемость и позволяют сократить количество ошибок.
В одной книге мы не сможем охватить все функции, доступные для работы с коллекциями, поэтому рассмотрим лишь самые распространенные из них.
Отображение элементов
Функция
map()
преобразует каждый элемент коллекции в элемент нового типа.Чтобы вы могли это увидеть, предположим, что у нас есть список букв и мы хотим
преобразовать их в соответствующие значения ASCII.
Для начала решим эту задачу, используя методы императивного программирования:
val letters = 'a'..'z'
val ascii = mutableListOf<Int>()
for (l in letters) {
ascii.add(l.code)
}
Даже для решения такой простой задачи данный подход требует изрядного количества кода. Кроме того, мы вынуждены определить список как изменяемый.
Теперь добьемся того же результата с помощью функции
map()
:val result: List<Int> = ('a'..'z').map { it.code }
Обратите внимание на краткость этой версии. В данном случае нет необходимости
использовать изменяемый список и цикл
for-each
.Фильтрация элементов
Фильтрация коллекции — еще одна распространенная задача, которая предполагает перебор ее элементов и добавление значений в новую коллекцию, исходя из определенных условий. Например, если у вас есть диапазон чисел от 1 до 100, то вы можете захотеть добавить в новую коллекцию только те, которые делятся на 3 или на 5.
При использовании императивного подхода код для решения этой задачи мог бы выглядеть так:
val numbers = 1..100
val notFizzbuzz = mutableListOf<Int>()
for (n in numbers) {
if (n % 3 == 0 || n % 5 == 0) {
notFizzbuzz.add(n)
}
}
Работая в стиле функционального программирования, вы могли бы добиться
того же результата, используя функцию
filter()
:val filtered: List<Int> = (1..100).filter { it % 3 == 0 || it % 5 == 0 }
Обратите внимание, насколько более оптимизированным стал код. Вместо того чтобы указывать, «как» выполнять операцию (например, с помощью
if
), вы просто описываете, «что» именно хотите сделать — отфильтровать элементы на основе определенных условий.Поиск элементов
Еще одна рутинная задача — поиск первого элемента в коллекции, удовлетворяющего определенным критериям. Например, если вам нужно найти в списке чисел первое число, кратное 3 и 5, то вы можете реализовать это следующим образом, используя императивный стиль:
fun findFizzbuzz(numbers: List<Int>): Int? {
for (n in numbers) {
if (n % 3 == 0 && n % 5 == 0) {
return n
}
}
return null
}
Однако вы можете добиться того же результата, используя функцию
find()
:val found: Int? = (1..100).find { it % 3 == 0 && it % 5 == 0 }
Как и ее императивный аналог, функция find возвращает значение
null
, если ни один элемент не удовлетворяет заданным критериям.Кроме того, существует метод
findLast()
, который служит аналогичной цели, но начинает поиск с последнего элемента коллекции.Выполнение кода для каждого элемента
Все функции высшего порядка, о которых мы говорили до сих пор, имеют одну общую черту: они возвращают поток данных (или новую коллекцию). Однако не все функции высшего порядка работают таким образом. Некоторые из них возвращают одно значение, например значение типа Unit или число. Такие функции называются прерывающими.
В качестве примера такой функции рассмотрим
forEach()
, которая возвращает тип Unit
. В языке Java аналогом типа Unit
является void
, который означает, что функция не возвращает ничего ценного. По сути, forEach()
служит той же цели, что и традиционный цикл for
:val numbers = (0..5)
numbers.map { it * it } // Можно продолжать
.filter { it < 20 } // Можно продолжать
.forEach { println(it) } // Продолжать нельзя
Кроме того, Kotlin предлагает функцию
forEachIndexed()
, которая во время итерации передает как индекс, так и фактическое значение:numbers.map { it * it }
.forEachIndexed { index, value ->
print("$index:$value, ")
}
Выполнение этого кода приводит к отображению следующего результата:
> 0:0, 1:1, 2:4, 3:9, 4:16, 5:25,
Кроме того, начиная с версии Kotlin 1.1, мы можем использовать функцию
onEach()
, которая, в отличие от forEach()
, возвращает коллекцию, позволяя продолжить создание цепочки:numbers.map { it * it }
.filter { it < 20 }
.sortedDescending()
.onEach { println(it) } // Можно продолжать
.filter { it > 5 }
Как видите, функция onEach() не прерывает цепочку.
Более подробно с книгой можно ознакомиться на нашем сайте.
» Оглавление
» Отрывок
По факту оплаты бумажной версии книги на e-mail высылается электронная книга.
Для Хаброжителей скидка 25 % по купону — Kotlin