Перейти к содержанию

KTB-09 Навести порядок в коде (оптимизация)

📝 Описание

Рефакторинг

Поговорим о важном аспекте разработки – рефакторинге кода. Возможно, твой код сейчас выглядит как монолитный блок текста, который ты отлично понимаешь. Но через месяц ты можешь столкнуться с проблемами при внесении изменений, не говоря уже о других разработчиках, которые могут работать с этим кодом. Поэтому важно сделать код более читаемым и поддерживаемым.

Основные понятия и принципы урока

Рекомендуется ознакомиться с важными аспектами разработки, связанными с рефакторингом, а также с принципами написания качественного кода.

  • Понятие рефакторинга – процесс улучшения внутренней структуры кода без изменения его внешнего поведения и функциональности.
  • Принципы написания качественного кода:
    • KISS (Keep It Simple, Stupid) – стремление к простым и понятным решениям, избегая излишней сложности.
    • DRY (Don't Repeat Yourself) – устранение дублирования кода путём вынесения повторяющейся логики в отдельные функции или модули.
    • Принцип единственной ответственности – каждый класс или модуль должен отвечать за одну конкретную задачу или функциональность.
    • Избегание преждевременной оптимизации – фокусирование на читаемости и простоте кода, откладывая оптимизацию до момента, когда она действительно необходима.

Практические шаги рефакторинга, которые нужно будет провести

  • Разделить функциональность на логические блоки – структурировать код, выделяя отдельные функции и методы для каждой задачи.
  • Вынести загрузку и сохранение данных в отдельные классы.
  • Создать классы для статистики и обработки вопросов.
  • Реализовать методы для проверки ответов.
  • Внедрить extension-функции для улучшения читаемости кода.

// видео

🗒 Задание

  1. Привести в порядок код тренажёра, выделить понятные функции с информативными названиями.
  2. Разделить логику работы тренажёра и взаимодействия с консолью.
  3. Завершить реализацию всех функций консольного тренажёра, если остались незавершённые части.

❌ Ошибки

✏️ Комментарии

Давай вынесем функции после `main()`, тогда читать удобнее: `main() { do_1() do_2() do_3() }`.
Внутри метода `getStatistics()` нам не нужен `println()`, чтобы не завязывать пользовательский интерфейс и бизнес-логику. Давай возвращать из `getStatistics()` экземпляр класса `Statistic(...)` и выводить результаты вне `LearnWordsTrainer`.
Лучше использовать максимально абстрактные типы данных (например, `List<Word>`), чтобы не допускать ненужных изменений в списках.
Лучше формировать вопрос внутри `LearnWordsTrainer`, тогда интерфейс можно будет легче изменять.

⌨️ Решение

main.kt

data class Word(
    val original: String,
    val translate: String,
    var correctAnswersCount: Int = 0,
)

fun Question.asConsoleString(): String {
    val variants = this.variants
        .mapIndexed { index: Int, word: Word -> " ${index + 1}${word.translate}" }
        .joinToString(
            separator = "\n",
            prefix = "\n${this.correctAnswer.original}\n",
            postfix = "\n ----------\n 0 - Меню",
        )
    return variants
}

fun main() {

    val trainer = LearnWordsTrainer()

    while (true) {
        println(
            """
            Меню:
            1 – Учить слова
            2 – Статистика
            0 – Выход
        """.trimIndent()
        )
        val input = readln()
        when (input) {
            "1" -> {
                while (true) {
                    val question = trainer.getNextQuestion()
                    /* Перенесено в RA-09 в getNextQuestion()
                    val notLearnedList = trainer.dictionary.filter { it.correctAnswersCount < 3 }
                    */
                    if (question == null) { // if (notLearnedList.isEmpty()) {
                        println("Все слова в словаре выучены")
                        continue
                    } else {

                        /* Перенесено в RA-09 в getNextQuestion()
                        val questionWords = notLearnedList.shuffled().take(4)
                        val correctAnswer = questionWords.random()
                        */

                        // Вариант 1 (вызовет падение, если слов для изучения меньше 4)
//                    println(
//                        """
//                        ${correctAnswer.original}
//                        1 - ${questionWords[0].translate}
//                        2 - ${questionWords[1].translate}
//                        3 - ${questionWords[2].translate}
//                        4 - ${questionWords[3].translate}
//                        ----------
//                        0 - Меню
//                    """.trimIndent()
//                    )

                        // Вариант 2 (рекомендуемый в комментариях)
                        /*
                        val variants = question.variants
                            .mapIndexed { index: Int, word: Word -> " ${index + 1} – ${word.translate}" }
                            .joinToString(
                                separator = "\n",
                                prefix = "\n${question.correctAnswer.original}\n",
                                postfix = "\n ----------\n 0 - Меню",
                            )
                        println(variants)
                        */
                        println(question.asConsoleString())

                        val userAnswerInput = readln().toIntOrNull()
                        if (userAnswerInput == 0) break

                        if (trainer.checkAnswer(userAnswerInput?.minus(1))) {
                            println("Правильно!")
                            /* Перенесено в RA-09 в checkAnswer()
                            question.correctAnswer.correctAnswersCount++
                            trainer.saveDictionary(trainer.dictionary)
                            */
                        } else {
                            println("Неправильно! ${question.correctAnswer.original} – это ${question.correctAnswer.translate}")
                            continue
                        }

                        /* Перенесено в RA-09 в checkAnswer()
                        val correctAnswerId = question.variants.indexOf(question.correctAnswer)
                        if (userAnswerInput?.minus(1) == correctAnswerId) {
                            println("Правильно!")
                            question.correctAnswer.correctAnswersCount++
                            trainer.saveDictionary(trainer.dictionary)
                        } else {
                            println("Неправильно! ${question.correctAnswer.original} – это ${question.correctAnswer.translate}")
                            continue
                        }
                        */
                    }

                }
            }

            "2" -> {
                val statistics = trainer.getStatistics()
                println("Выучено ${statistics.learnedCount} из ${statistics.totalCount} слов | ${statistics.percent}%\n")
                continue
            }

            "0" -> {
                return
            }
        }

    }

}

LearnWordsTrainer.kt

import java.io.File

class Statistics(
    val totalCount: Int,
    val learnedCount: Int,
    val percent: Int,
)

data class Question(
    val variants: List<Word>,
    val correctAnswer: Word,
)

class LearnWordsTrainer {

    private var question: Question? = null
    private val dictionary = loadDictionary()

    fun getStatistics(): Statistics {
        val totalCount = dictionary.size
        val learnedCount = dictionary.filter { it.correctAnswersCount >= 3 }.size
        val percent = learnedCount * 100 / totalCount
        return Statistics(totalCount, learnedCount, percent)
    }

    fun getNextQuestion(): Question? {
        val notLearnedList = dictionary.filter { it.correctAnswersCount < 3 }
        if (notLearnedList.isEmpty()) return null
        val questionWords = notLearnedList.shuffled().take(4)
        val correctAnswer = questionWords.random()
        question = Question(
            variants = questionWords,
            correctAnswer = correctAnswer,
        )
        return question
    }

    fun checkAnswer(userAnswerIndex: Int?): Boolean {
        return question?.let {
            val correctAnswerId = it.variants.indexOf(it.correctAnswer)
            if (correctAnswerId == userAnswerIndex) {
                it.correctAnswer.correctAnswersCount++
                saveDictionary(dictionary)
                true
            } else {
                false
            }
        } ?: false
    }

    private fun loadDictionary(): List<Word> {

        try {
            val dictionary = mutableListOf<Word>()
            val wordsFile = File("words.txt")

            for (string in wordsFile.readLines()) {
                val split = string.split("|")
                val word = Word(
                    original = split[0],
                    translate = split[1],
                    correctAnswersCount = split[2].toIntOrNull() ?: 0
                )
                dictionary.add(word)
            }
            return dictionary
        } catch (e: Exception) {
            throw IllegalStateException("Некорректный файл: $e")
        }
    }

    private fun saveDictionary(words: List<Word>) {
        val wordsFile = File("words.txt")
        wordsFile.writeText("")
        for (word in words) {
            wordsFile.appendText("${word.original}|${word.translate}|${word.correctAnswersCount}\n")
        }
    }

}