JavaRush /Курсы /Kotlin SELF /Методы и состояние: поведение рядом с данными, this и баз...

Методы и состояние: поведение рядом с данными, this и базовые проверки

Kotlin SELF
30 уровень , 2 лекция
Открыта

1. Зачем держать поведение рядом с данными

Когда начинаешь писать программу, очень хочется держать всю логику «снаружи»: вот данные, а вот отдельно функции, которые с ними что-то делают. И это нормально — так проще стартовать. Но по мере роста кода появляется неприятный эффект: правила работы с данными начинают повторяться в разных местах, а изменения становятся похожи на игру «найди все места, где я забыл проверку».

Представьте, что у нас есть трата: название и сумма. Пока вы печатаете её один раз — всё хорошо. Но как только вы начинаете менять название, менять сумму, проверять «крупная ли трата», форматировать вывод — логика размазывается по всему проекту.

Сначала это выглядит примерно так:

fun isLargeExpense(amount: Int): Boolean {
    return amount >= 1000
}

fun main() {
    val title = "Taxi"
    val amount = 1200

    println("$title: $amount")                 // Taxi: 1200
    println(isLargeExpense(amount))            // true
}

Работает? Да. Масштабируется? Как дипломный проект на коленке — тоже да, но недолго.

И вот тут появляется идея: если «проверка крупности» — это смысловая часть траты, почему бы не сделать это методом траты?

2. Методы: объявление, вызов и доступ к свойствам

Метод класса: как объявить и как вызвать

Метод — это обычная функция, но живёт внутри класса и вызывается у объекта через точку. В Kotlin это читается очень естественно: expense.isLarge() звучит как фраза, а не как заклинание из древнего фреймворка.

Начнём с самого простого: добавим метод, который красиво печатает трату одной строкой.

class Expense(val title: String, val amount: Int) {
    fun asLine(): String {
        return "$title: $amount"
    }
}

fun main() {
    val e = Expense("Coffee", 300)
    println(e.asLine()) // Coffee: 300
}

Обратите внимание на два момента. Во-первых, метод объявляется как fun внутри фигурных скобок класса. Во-вторых, вызывается как e.asLine(): объект слева, метод справа. Это важная привычка, потому что дальше почти всё ООП в Kotlin будет «через точку».

Почему методы «видят» свойства объекта бесплатно

Самая приятная часть методов для новичка — это то, что метод уже «держит в руках» объект. То есть ему не нужно передавать отдельно title и amount, потому что они уже внутри объекта. Это выглядит как магия, но на самом деле просто логика: метод вызывается на конкретном объекте, значит он работает с его состоянием.

Сравним два подхода — функция снаружи и метод внутри.

Функция снаружи:

class Expense(val title: String, val amount: Int)

fun asLine(e: Expense): String {
    return "${e.title}: ${e.amount}"
}

fun main() {
    val e = Expense("Lunch", 900)
    println(asLine(e)) // Lunch: 900
}

То же самое, но «рядом с данными»:

class Expense(val title: String, val amount: Int) {
    fun asLine(): String = "$title: $amount"
}

fun main() {
    val e = Expense("Lunch", 900)
    println(e.asLine()) // Lunch: 900
}

Второй вариант обычно читается легче: меньше шума в вызове и меньше риск «не тот объект передал». И ещё важнее — правила форматирования живут в одном месте, а не расползаются по проекту.

3. Состояние объекта: var и «объект во времени»

Состояние — это текущие значения var-свойств объекта. Если у объекта есть изменяемые поля, значит объект может меняться во времени: был такой, стал другой. Это полезно для счётчиков, кошельков, текущих настроек и вообще всего, что «живёт» и меняется.

Сделаем мини-пример: счётчик, который можно увеличивать.

class Counter(var value: Int = 0) {
    fun inc() {
        value += 1
    }
}

fun main() {
    val c = Counter()
    c.inc()
    c.inc()
    println(c.value) // 2
}

И очень важная деталь, которая часто ломает мозг в начале: val c = Counter() запрещает переназначить ссылку (то есть нельзя сделать c = Counter()), но не запрещает менять состояние объекта, если внутри есть var.

То есть val не делает объект «замороженным», он делает переменную «непереназначаемой».

4. this: когда он не нужен и когда спасает

Слово this в Kotlin означает «текущий объект». В большинстве случаев Kotlin позволяет его не писать: если вы обращаетесь к title, компилятор понимает, что это this.title.

Но иногда this нужен, чтобы убрать двусмысленность — особенно когда у параметра метода такое же имя, как у свойства. Это частый случай при «переименовании», «обновлении суммы» и подобных операциях.

Вот пример, где this делает намерение однозначным:

class Person(var name: String) {
    fun rename(name: String) {
        this.name = name.trim()
    }
}

fun main() {
    val p = Person("Ann")
    p.rename("  Anna  ")
    println(p.name) // Anna
}

Практическое правило простое: пока нет конфликтов имён — можно не писать this. Как только появляется ощущение «что-то тут подозрительно», this помогает явно показать, что вы меняете состояние именно объекта.

5. Инварианты и проверки: держим объект в корректном состоянии

Инвариант — это простое правило корректности состояния. Смысл бытовой: «что должно быть всегда правдой, чтобы объект имел смысл». Например, у траты сумма не должна быть отрицательной, а название не должно быть пустым.

Инварианты лучше проверять там, где происходит изменение, то есть в методах. Тогда у вас появляется «одна точка контроля», и вы не обязаны помнить про проверки в каждом месте программы.

Сделаем Expense изменяемым и дадим ему методы обновления с минимальными проверками:

class Expense(var title: String, var amount: Int) {

    fun rename(newTitle: String): Boolean {
        val t = newTitle.trim()
        if (t.isBlank()) return false
        title = t
        return true
    }

    fun changeAmount(newAmount: Int): Boolean {
        if (newAmount < 0) return false
        amount = newAmount
        return true
    }
}

Здесь метод возвращает Boolean, и это очень «приземлённый» контракт: true — получилось, false — не получилось, и объект не сломан. Такой подход хорош, когда ошибка — ожидаемая часть жизни (пользователь ввёл ерунду, бывает).

Ранний return и require/check: разные сценарии

Иногда ошибка — это нормальная ситуация, а иногда — логическая катастрофа (в стиле «внутри кошелька отрицательный баланс, хотя мы обещали, что такого не бывает»). Kotlin даёт вам два популярных инструмента: мягкий стиль через return false и жёсткий стиль через require/check.

  • require(...) обычно проверяет корректность входных аргументов и при нарушении бросает IllegalArgumentException.
  • check(...) обычно проверяет состояние и при нарушении бросает IllegalStateException.

Сравним подходы на примере кошелька.

Мягкий вариант (ожидаем, что пользователь может ошибиться):

class Wallet(var balance: Int) {

    fun withdraw(amount: Int): Boolean {
        if (amount <= 0) return false
        if (amount > balance) return false
        balance -= amount
        return true
    }
}

fun main() {
    val w = Wallet(100)
    println(w.withdraw(30))  // true
    println(w.balance)       // 70
    println(w.withdraw(200)) // false
}

Жёсткий вариант (считаем, что «сюда нельзя приходить с неправильными данными»):

class Wallet(var balance: Int) {

    fun withdraw(amount: Int) {
        require(amount > 0) { "Amount must be positive: $amount" }
        check(balance >= 0) { "Balance is broken: $balance" }

        require(amount <= balance) {
            "Not enough money. Balance=$balance, amount=$amount"
        }
        balance -= amount
    }
}

Разница не в синтаксисе, а в философии.

  • Возврат false позволяет программе продолжить и спокойно попросить пользователя «ввести ещё раз».
  • require/check — это сигнал: «программа в такой ситуации не должна продолжать, это нарушение контракта».

6. Применяем на практике: обновляем Expense через методы

Мы уже можем показать правильную идею на маленьком фрагменте: внешний код не должен знать все правила изменения траты. Внешний код должен попросить объект сделать действие, а объект сам решит, что допустимо.

Вот как может выглядеть обновление названия: мы не делаем expense.title = ... напрямую, а вызываем rename(...).

class Expense(var title: String, var amount: Int) {
    fun rename(newTitle: String): Boolean {
        val t = newTitle.trim()
        if (t.isBlank()) return false
        title = t
        return true
    }
}

fun main() {
    val e = Expense("Coffee", 300)

    val ok = e.rename("   ")
    println(ok)       // false
    println(e.title)  // Coffee
}

Обратите внимание, что в случае ошибки объект остаётся в прежнем корректном состоянии. Это и есть маленькая «победа» инварианта: мы не позволили создать странный объект, с которым потом непонятно что делать.

Если вы хотите обновлять и сумму, то внешний код снова не «знает» правил — он просто вызывает метод:

class Expense(var title: String, var amount: Int) {
    fun changeAmount(newAmount: Int): Boolean {
        if (newAmount < 0) return false
        amount = newAmount
        return true
    }
}

fun main() {
    val e = Expense("Taxi", 1200)

    e.changeAmount(1500)
    println(e.amount) // 1500
}

Это выглядит как мелочь, но на реальном проекте экономит много нервов: вы не ищете по всему коду «где мы могли присвоить отрицательную сумму», потому что присваивания «снаружи» почти не делаете.

7. Типичные ошибки

Ошибка №1: класс есть, но вся логика всё равно живёт снаружи.
Новичок заводит class Expense(...), но затем продолжает писать десяток функций вида formatExpense(e), isLargeExpense(e), renameExpense(e, ...), validateExpense(e) и т.д. В итоге класс превращается в «мешок полей», а правила остаются размазанными. Если действие естественно относится к конкретному объекту, лучше сделать его методом: так меньше шансов забыть правило в одном из мест.

Ошибка №2: изменяемость (var) добавлена «на всякий случай», а не по смыслу.
Если вы сделаете все свойства var, то любой кусок кода сможет поменять объект как угодно и когда угодно. Потом вы внезапно увидите amount = -100 и начнёте подозревать, что в проект проникла тёмная магия. Начинать лучше с val, а var добавлять только тогда, когда изменение действительно нужно по смыслу сценария.

Ошибка №3: изменения состояния делают напрямую, обходя методы.
Вы написали rename() с проверками, но где-то в коде всё равно осталось expense.title = readln(). В этот момент проверки теряют смысл: часть изменений идёт через «правильный вход», а часть — через «чёрный ход». Если вы решили защищать инварианты методами, старайтесь вызывать методы, а не менять поля напрямую.

Ошибка №4: метод меняет состояние, но не сообщает, получилось ли.
Если метод может «не сработать» (например, новое название пустое), но при этом возвращает Unit и молча ничего не делает, то внешний код не понимает, что произошло. Это порождает странные сценарии: пользователь ввёл пустую строку, программа продолжила как будто всё хорошо, а изменение не применилось. Для ожидаемых ошибок удобен контракт Boolean: вызывающий код сможет показать сообщение и повторить ввод.

Ошибка №5: путаница из-за одинаковых имён и отсутствие this.
Метод rename(name: String) почти наверняка встретится в вашей жизни, и почти наверняка вы один раз напишете name = name.trim() и будете минуту смотреть в монитор, ожидая просветления. В таких местах this — не «лишние буквы», а способ сделать намерение очевидным: this.name = name.trim().

Ошибка №6: require/check используются там, где сценарий должен продолжаться.
Если вы проверяете пользовательский ввод через require(...), то при ошибке ваша программа «упадёт» исключением, потому что require именно так и устроен. Это полезно для защиты контракта, но плохо для дружелюбного CLI, где пользователь просто мог ошибиться. Для ожидаемых ошибок лучше вернуть false/null и попросить ввести заново, а require/check оставлять для ситуаций «так быть не должно».

1
Задача
Kotlin SELF, 30 уровень, 2 лекция
Недоступна
Визитка профиля
Визитка профиля
1
Задача
Kotlin SELF, 30 уровень, 2 лекция
Недоступна
Шаги героя
Шаги героя
1
Задача
Kotlin SELF, 30 уровень, 2 лекция
Недоступна
Никнейм без мусора
Никнейм без мусора
1
Задача
Kotlin SELF, 30 уровень, 2 лекция
Недоступна
Кошелёк правил
Кошелёк правил
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ