43. Провести code review OrderService на Kotlin
Условие задачи:
Дан сервис создания и отмены заказов на Kotlin.
Необходимо провести code review: найти проблемы в работе с null, коллекциями, денежными значениями, Singleton, корутинами, SQL и дизайном класса.
Код:
class OrderService {
companion object {
var INSTANCE: OrderService? = null
fun getInstance() =
INSTANCE ?: OrderService().also {
INSTANCE = it
}
}
public var orders: MutableList<Order> =
mutableListOf()
fun createOrder(
userId: String,
items: List<Item>?,
promo: String
): Order {
val order = Order(
UUID.randomUUID().toString(),
userId
)
var total = 0.0
for (i in 0 ..< items!!.size - 1) {
total += items[i].price * items[i].quantity
}
if (promo != null && promo != "") {
if (promo.equals("SALE10")) {
total = total - total * 0.1
} else if (promo.equals("SALE20")) {
total = total - total * 0.2
}
}
order.total = total
orders.add(order)
GlobalScope.launch {
Thread.sleep(1000)
DB.exec(
"INSERT INTO orders VALUES " +
"('${order.id}', ${order.total})"
)
}
return order
}
fun cancel(id: String) {
orders.remove(
orders.find { it.id.equals(id) }
)
}
}
class Order(
var id: String,
var userId: String
) {
var total: Double = 0.0
}
Спойлеры к решению
Подсказки
items!! может привести к NullPointerException.💡 Проверь границы цикла и обрабатывается ли последний элемент списка.
💡
Double не подходит для денежных расчётов.💡
GlobalScope и Thread.sleep() плохо сочетаются с корутинами.💡 Singleton через изменяемое поле
INSTANCE не является потокобезопасным.Решение
Основные проблемы:
- Singleton через
INSTANCEне потокобезопасен; - публичный
MutableListможно изменить извне; items!!может привести кNullPointerException;- цикл не обрабатывает последний элемент;
promoобъявлен как non-nullable, поэтомуpromo != nullбессмысленно;- для денег используется
Double; - для сравнения строк в Kotlin лучше использовать
==; GlobalScopeсоздаёт неконтролируемую корутину;Thread.sleep()блокирует поток;- метод возвращает заказ до завершения записи в БД;
- ошибки асинхронной записи никак не обрабатываются;
- SQL строится через интерполяцию строки;
OrderServiceодновременно считает цену, хранит заказы и работает с БД;cancel()некорректно работает, если заказ не найден.
Цикл лучше заменить обычным обходом элементов:
for (item in items) {
total += item.price * item.quantity
}
Для денег использовать BigDecimal, а скидку определить через when:
fun createOrder(
userId: String,
items: List<Item>,
promo: String?
): Order {
require(userId.isNotBlank())
require(items.isNotEmpty())
var total = items.fold(BigDecimal.ZERO) { sum, item ->
sum + item.price.multiply(
item.quantity.toBigDecimal()
)
}
total = when (promo) {
"SALE10" -> total.multiply(BigDecimal("0.90"))
"SALE20" -> total.multiply(BigDecimal("0.80"))
else -> total
}
return Order(
id = UUID.randomUUID().toString(),
userId = userId,
total = total
)
}
Модель:
data class Order(
val id: String,
val userId: String,
val total: BigDecimal
)
Отмену можно реализовать так:
fun cancel(id: String): Boolean {
return orders.removeIf { it.id == id }
}
Вместо самописного Singleton:
companion object {
var INSTANCE: OrderService? = null
}
в обычном Kotlin можно использовать object, а если класс управляется Spring — вообще не создавать Singleton вручную.
GlobalScope.launch также лучше убрать. Сохранение заказа должно выполняться через отдельный repository, а SQL — через параметры, а не конкатенацию строк.
Главные проблемы исходного кода — items!!, ошибка в диапазоне цикла, Double для денег, небезопасный Singleton, публичное изменяемое состояние и неконтролируемая асинхронная запись в БД.