KTB-09 Навести порядок в коде (оптимизация)¶
📝 Описание¶
Рефакторинг
Поговорим о важном аспекте разработки – рефакторинге кода. Возможно, твой код сейчас выглядит как монолитный блок текста, который ты отлично понимаешь. Но через месяц ты можешь столкнуться с проблемами при внесении изменений, не говоря уже о других разработчиках, которые могут работать с этим кодом. Поэтому важно сделать код более читаемым и поддерживаемым.
Основные понятия и принципы урока
Рекомендуется ознакомиться с важными аспектами разработки, связанными с рефакторингом, а также с принципами написания качественного кода.
- Понятие рефакторинга – процесс улучшения внутренней структуры кода без изменения его внешнего поведения и функциональности.
- Принципы написания качественного кода:
- KISS (Keep It Simple, Stupid) – стремление к простым и понятным решениям, избегая излишней сложности.
- DRY (Don't Repeat Yourself) – устранение дублирования кода путём вынесения повторяющейся логики в отдельные функции или модули.
- Принцип единственной ответственности – каждый класс или модуль должен отвечать за одну конкретную задачу или функциональность.
- Избегание преждевременной оптимизации – фокусирование на читаемости и простоте кода, откладывая оптимизацию до момента, когда она действительно необходима.
Практические шаги рефакторинга, которые нужно будет провести
- Разделить функциональность на логические блоки – структурировать код, выделяя отдельные функции и методы для каждой задачи.
- Вынести загрузку и сохранение данных в отдельные классы.
- Создать классы для статистики и обработки вопросов.
- Реализовать методы для проверки ответов.
- Внедрить extension-функции для улучшения читаемости кода.
// видео
🗒 Задание¶
- Привести в порядок код тренажёра, выделить понятные функции с информативными названиями.
- Разделить логику работы тренажёра и взаимодействия с консолью.
- Завершить реализацию всех функций консольного тренажёра, если остались незавершённые части.
❌ Ошибки¶
✏️ Комментарии¶
Внутри метода `getStatistics()` нам не нужен `println()`, чтобы не завязывать пользовательский интерфейс и бизнес-логику. Давай возвращать из `getStatistics()` экземпляр класса `Statistic(...)` и выводить результаты вне `LearnWordsTrainer`.
Лучше использовать максимально абстрактные типы данных (например, `List<Word>`), чтобы не допускать ненужных изменений в списках.
⌨️ Решение¶
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")
}
}
}