Kotlin Fest 2025: вопросы по проверке кода

Привет. Меня зовут Такамацу, и я отвечаю за разработку приложений для 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, используя процесс анализа журналов доступа и создания сводных строк на основе содержимого. Хотя приведенный выше код компилируется, на самом деле он приводит к некоторым непредвиденным проблемам в поведении и дизайне. В день экзамена мы получили множество отзывов с самых разных точек зрения, но здесь мы представим основные моменты, которые мы имели в виду во время вопроса.

Read more:  Революция MacBook 2026 года от Apple: две модели, меняющие правила игры

Функции расширения и статическая диспетчеризация

Целевое местоположение
fun AccessLog.summarize(): String = ...
fun LoggedInUserAccessLog.summarize(): String = ...
Проблема

Функции расширения Kotlin разрешаются на основе их статического типа на момент вызова, а не на основе виртуальных методов. В этом коде тип журналов — List. даже если сущностью является LoggedInUserAccessLog, вызывается функция суммирования для AccessLog. В результате сводка, определенная для вошедших в систему пользователей, не используется, в результате чего выходные данные не включают userId. Намерение изменить поведение каждого класса не соответствует фактическому поведению.

План улучшения

Если вы ожидаете полиморфизма, определите его как функцию-член, используя открытое развлечение и переопределение. Это вызовет метод в зависимости от типа сущности во время выполнения.

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.

Read more:  они заставляют мозг решать проблемы, пока ты спишь
План улучшения

Используя mapNotNull, вы можете исключить значения NULL, одновременно определяя тип. В результате получается более короткий код и более ясные намерения.

val logs = LogRepository.fetchTodaysRawLogs()
.mapNotNull { parseAccessLog(it) }

Сохранение изменяемых переменных

предмет

2026-02-19 03:00:00


1771653600
#Kotlin #Fest #вопросы #по #проверке #кода

По теме

Leave a Comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.