🔄 Еволюція застарілих систем з Kotlin в гібридному середовищі

Модернізація застарілих систем — це складне завдання для багатьох організацій, особливо коли йдеться про великі моноліти, написані на Java. Одним із найбільш ефективних і безпечних підходів є поступова міграція на Kotlin. Це дозволяє працювати зі старим кодом, поки нові функціональності, рефакторинги та тести розробляються на Kotlin.

Kotlin повністю сумісний з Java, що дозволяє продовжувати використовувати старий код і бібліотеки, не переписуючи систему з нуля. Поступова адаптація дає змогу отримати переваги від сучасної мови без ризиків і проблем, пов'язаних з великою перепискою коду.

Переваги Kotlin

  • Лаконічність: Код стає зрозумілішим і менш об'ємним, що зменшує кількість шаблонного коду.
  • Безпека від null: Вдосконалена система типів допомагає уникнути проблем із NullPointerException.
  • Взаємодія з Java: Легке використання бібліотек Java і існуючого коду без складнощів.
  • Продуктивність: Завдяки сучасному синтаксису і функціям розробка прискорюється.
  • Корутіни: Легке та ефективне асинхронне програмування, яке зберігає простоту коду.
  • Функції-розширення: Дозволяють розширювати функціональність існуючих класів без змін в їх коді.
  • Потужні утилітарні функції: Функції для роботи з колекціями, такі як partition, groupBy, mapNotNull, роблять маніпуляцію даними виразнішою.
  • Комбінація ООП і функціонального програмування: Конструкції, як data class, sealed class, лямбда-вирази, допомагають зробити код більш надійним і зрозумілим.

Приклад: фільтрація та трансформація списку

Java:

List nomes = Arrays.asList("Ana", "Bruno", "Carlos", "Bea");
List resultado = nomes.stream()
.filter(nome -> nome.startsWith("B"))
.map(String::toUpperCase)
.collect(Collectors.toList());

Kotlin:

val nomes = listOf("Ana", "Bruno", "Carlos", "Bea")
val resultado = nomes.filter { it.startsWith("B") }.map { it.uppercase() }

Реальний випадок: міграція поетапно

У компанії, де я працював, ми поступово почали впроваджувати Kotlin в старий моноліт на Java. На перших етапах ми зберегли старі класи на Java і почали писати нові функціональності на Kotlin. Це дозволило нам значно покращити продуктивність, зручність читання коду і його загальну якість.

Більше організації та кращі тести

Kotlin дозволив нам значно покращити структуру коду, чітко розділяючи відповідальності класів, а також полегшив написання тестів. Створення тестових об'єктів стало набагато зручнішим завдяки синтаксису Kotlin, а особливо завдяки використанню data class.

Приклад: Java POJO vs Kotlin data class

Java:

public class Pessoa {
private String nome;
private int idade;

public Pessoa(String nome, int idade) {
this.nome = nome;
this.idade = idade;
}
// getters, setters, toString, equals, hashCode...
}

Kotlin:

data class Pessoa(val nome: String, val idade: Int)

Ми також застосовували безпечний підхід до рефакторингу: спочатку писали тести для поточної поведінки, а вже потім вносили зміни. Це дозволило підвищити надійність і уникнути регресій.

Безперервна реструктуризація

Як команда, ми погодилися, що кожного разу, коли потрібно змінити старий код на Java, ми будемо переписувати його на Kotlin, зосереджуючи увагу на полегшенні читання і зрозумілості коду. Це допомогло нам поступово скорочувати розмір великих і складних для підтримки класів.

З часом Kotlin став стандартом для нових розробок: його легше писати, підтримувати, а також значно підвищується продуктивність.

Продуктивність без ускладнень

Ще одна перевага використання Kotlin проявилася при роботі з складними ендпоінтами, що виконували кілька завдань по черзі. Замість використання черг чи важкої архітектури ми використовували корутіни для паралелізації запитів до бази даних і зовнішніх сервісів. Це дозволило зберегти чистоту коду і ефективність роботи.

Приклад: паралельні виклики

Java (CompletableFuture):

public Dashboard getDashboard() throws ExecutionException, InterruptedException {
CompletableFuture userFuture = CompletableFuture.supplyAsync(() -> getUser());
CompletableFuture ordersFuture = CompletableFuture.supplyAsync(() -> getOrders());
CompletableFuture notificationsFuture = CompletableFuture.supplyAsync(() -> getNotifications());
CompletableFuture statsFuture = CompletableFuture.supplyAsync(() -> getDashboardStats());

CompletableFuture.allOf(userFuture, ordersFuture, notificationsFuture, statsFuture).join();

User user = userFuture.get();
List orders = ordersFuture.get();
List notifications = notificationsFuture.get();
DashboardStats stats = statsFuture.get();

return new Dashboard(user, orders, notifications, stats);
}

Kotlin (корутіни):

suspend fun getDashboard(): Dashboard = coroutineScope {
val user = async { getUser() }
val orders = async { getOrders() }
val notifications = async { getNotifications() }
val stats = async { getDashboardStats() }

return Dashboard(
user = user.await(),
orders = orders.await(),
notifications = notifications.await(),
stats = stats.await()
)
}

Функції-розширення: суперсила

Багато разів ми стикалися з необхідністю додати функціональність до класів, написаних іншими командами чи сторонніми бібліотеками. З Java це зазвичай означало дублювання коду в різних місцях.

З функціями-розширеннями в Kotlin можна розширювати функціональність класів, не змінюючи їх код, що робить систему більш модульною та зручною для повторного використання.

Приклад: розширення String

Java:

public class Util {
public static String capitalize(String input) {
return input.substring(0, 1).toUpperCase() + input.substring(1);
}
}
String nome = Util.capitalize("kotlin");

Kotlin:

fun String.capitalizeFirst(): String =
replaceFirstChar { it.uppercase() }

val nome = "kotlin".capitalizeFirst()

Прощавай, NullPointerException

З Kotlin ми можемо явно вказувати, коли змінна може бути null, і компілятор одразу повідомляє нас про це на етапі розробки. Це усунуло одну з найбільш поширених помилок у Java і значно підвищило нашу впевненість у роботі програми.

Приклад: синтаксис безпеки від null

Java:

public String getUserName(User user) {
if (user != null && user.getProfile() != null) {
return user.getProfile().getName();
}
return "Desconhecido";
}

Kotlin:

fun getUserName(user: User?): String {
return user?.profile?.name ?: "Desconhecido"
}

Висновки

Міграція на Kotlin була не лише зміною мови, а й зміною способу мислення, написання та підтримки програм. Ми робили це поступово, з тестами і злагодженістю команди, і отримали надійну, модульну систему, яку легко розвивати. Це був неймовірно корисний і задовольняючий досвід.

Перекладено з: 🔄 Evoluindo Sistemas Legados com Kotlin em Ambiente Híbrido