Привет. Меня зовут Такамацу, и я отвечаю за разработку приложений для Android в Yahoo! Аукционы.
На выставке Kotlin Fest 2025, которая прошла в субботу, 1 ноября 2025 г., компания LINE Yahoo, Inc. спонсировала план Kotlin. В этой статье мы объясним каждый вопрос, который был задан в рамках «Code Review Challenge», проходившего на стенде спонсора. Независимо от того, присутствовали ли вы на мероприятии или не смогли присутствовать, прочтите это, думая о том, как вы будете его рассматривать.
Искусство неправильной обработки журналов доступа
Меня зовут Хаясака, я веб-инженер. Для первого вопроса был задан следующий код.
open class AccessLog (
open val clientIp: String,
open val path: String,
open val method: String,
)
data class LoggedInUserAccessLog (
override val clientIp: String,
override val path: String,
override val method: String,
val userId: String,
) : AccessLog(clientIp, path, method)
fun AccessLog.summarize(): String = "${this.method} '${this.path}'"
fun LoggedInUserAccessLog.summarize(): String = "@${userId}${this.method} '${this.path}'"
object LogRepository {
// フォーマット: clientIp,userId,method,path
fun fetchTodaysRawLogs(): List = listOf(
"192.168.100.12,user-456,POST,/api/update",
"192.168.100.50,,GET,/login",
"127.0.0.1,,GET,/health_check",
"192.168.5.10,user-123,GET,/search",
"192.168.120.30,user-123,POST,/api/data",
)
}
fun parseAccessLog(line: String): AccessLog? {
val parts = line.split(',')
if (parts.size < 4) return null
val clientIp = parts[0]
val userId = parts[1]
val method = parts[2].uppercase()
val path = parts[3]
// userId が含まれているものは LoggedInUserAccessLog、そうでないものは AccessLog
return when (userId) {
"" -> AccessLog(clientIp, path, method)
else -> LoggedInUserAccessLog(clientIp, path, method, userId)
}
}
class AccessLogProcessor {
val output = mutableListOf()
var counter = 0
fun process() {
val logs: List = LogRepository.fetchTodaysRawLogs()
.map { parseAccessLog(it) }
.filter { it != null }
.map { it as AccessLog }
// health check endpoint のログはスキップする
logs.forEach { log ->
if (log.path == "/health_check") {
return
}
counter += 1
output.add(log.summarize())
}
}
}
fun main() {
val processor = AccessLogProcessor()
processor.process()
println(processor.output)
println("Received ${processor.counter} requests.")
}
Объяснение
В этой задаче мы рассмотрели подводные камни функций расширения, операций сбора и синтаксиса управления в Kotlin, используя процесс анализа журналов доступа и создания сводных строк на основе содержимого. Хотя приведенный выше код компилируется, на самом деле он приводит к некоторым непредвиденным проблемам в поведении и дизайне. В день экзамена мы получили множество отзывов с самых разных точек зрения, но здесь мы представим основные моменты, которые мы имели в виду во время вопроса.
Функции расширения и статическая диспетчеризация
Целевое местоположение
fun AccessLog.summarize(): String = ...
fun LoggedInUserAccessLog.summarize(): String = ...
Проблема
Функции расширения Kotlin разрешаются на основе их статического типа на момент вызова, а не на основе виртуальных методов. В этом коде тип журналов — List.
План улучшения
Если вы ожидаете полиморфизма, определите его как функцию-член, используя открытое развлечение и переопределение. Это вызовет метод в зависимости от типа сущности во время выполнения.
open class AccessLog(...) {
open fun summarize(): String = ...
}
class LoggedInUserAccessLog(...) : AccessLog(...) {
override fun summarize(): String = ...
}
преждевременное завершение forEach с возвратом
Целевое местоположение
logs.forEach { log ->
if (log.path == "/health_check") {
return
}
...
}
Проблема
Если вы используете return в forEach, возврат будет от самой функции процесса, а не от лямбды. Таким образом, вся функция процесса завершается при появлении журнала health_check, и дальнейшие журналы не обрабатываются. Судя по комментариям, кажется, что вы хотите пропустить только определенные журналы, но реальное поведение другое.
План улучшения
Если вы хотите, чтобы эквивалентное поведение продолжалось, используйте обычный цикл for или помеченный возврат, например return@forEach.
for (log in logs) {
if (log.path == "/health_check") continue
...
}
Избыточная обработка нулей
Целевое местоположение
.map { parseAccessLog(it) }
.filter { it != null }
.map { it as AccessLog }
Проблема
parseAccessLog может возвращать значение NULL, поэтому позже потребуется проверка и приведение значения NULL. Этот способ написания работает, но есть возможности для улучшения с точки зрения сходства с Kotlin.
План улучшения
Используя mapNotNull, вы можете исключить значения NULL, одновременно определяя тип. В результате получается более короткий код и более ясные намерения.
val logs = LogRepository.fetchTodaysRawLogs()
.mapNotNull { parseAccessLog(it) }
Сохранение изменяемых переменных
предмет
2026-02-19 03:00:00
1771653600
#Kotlin #Fest #вопросы #по #проверке #кода
По теме

