(java || kotlin) && devOps
367 subscribers
6 photos
1 video
6 files
312 links
Полезное про Java и Kotlin - фреймворки, паттерны, тесты, тонкости JVM. Немного архитектуры. И DevOps, куда без него
Download Telegram
Что возвращать при ошибке?

Какие есть варианты?
1) exception
2) false
3) Optional и аналоги
4) NullObject
5) null

Для начала я бы отбросил (ну или отложил для особых случаев) вариант с null. Он давно уже "проклят", как приводящий к NPE.

Оставшиеся варианты я предлагаю разделить на две группы - NullObject vs все остальные

Основная разница между ними - первый не требует немедленной обработки ошибок, другие - требуют. Отсутствие такого требования - это преимущество, т.к. дает нам гибкость. Если надо - проверили на ошибку сразу, не надо - позже. Но у гибкости есть обратная сторона - увеличивается сложность. Т.к. NullObject выглядит как настоящий объект - он может пролезть достаточно далеко по коду. Вплоть до сохранения в БД, где ошибка и произойдёт. Или не произойдёт, что видится ещё худшим вариантом. Итого - для отложенной обработки ошибок NullObject выглядит оптимальным даже учитывая описанные выше риски.

exception vs false vs Optional.

false плох тем, что не дает ничего вернуть из метода. Иногда это ок, но в общем случае - не ок. Ну и method chaining конечно же ломает.

exception vs Optional
Да начала exception бывают checked и unchecked. Первые с точки зрения обработки ошибок надёжнее - сложнее забыть обработать ошибку (если только использовать @sneakythrows и аналоги). Но checked exception имеют свои минусы, не даром нигде, кроме Java, они не прижились.

unchecked exception vs Optional
У exception есть один большой минус. Unchecked exception из чужого, плохо документированного кода, да к тому же возникающий редко - проблема, т.к. может дойти не до того уровня обработки ошибок, который планировался. Еще одна проблема - прерывание потока выполнения. Если этим злоупотреблять - имеем аналог go to, антипаттерн.С другой стороны exception  достаточно гибкий - можно отлавливать локально, на уровне сервиса или контроллера или на уровне фреймворка (Spring @ExceptionHandler). Код обработки ошибок может быть любым.
Плюсы Optional - его можно свести либо exception, либо к NullObject. Минусы - если захочешь тащить его выше - читаемость кода ухудшается. Т.е. Optional - это как правило короткоживущий объект. Он не совместим с JPA и с Java сериализацией. Еще важный минус - Optional не предоставляет средств для хранения деталей ошибки. Для меня минусы перевешивают плюсы. И судя по коду, который я вижу - не только для меня)

Если поставить вопрос: "Когда лучше обработать ошибку?" - то мой ответ: "Лучше сразу, это надёжнее".

Ну и традиционный итог: разработка - искусство  компромиссов)

#dev_compromises #null_safety #error_handling
Обработка ошибок - не только Java

Как справедливо заметил @ort_gorthaur в комментах к посту об обработке исключений в Java https://t.me/javaKotlinDevOps/440
в других языках есть интересные варианты для обработки исключений.

Try в Scala
https://www.baeldung.com/scala/exception-handling

def trySuccessFailure(a: Int, b: Int): Try[Int] = Try {
Calculator.sum(a,b)
}

val result = trySuccessFailure(-1,-2)
result match {
case Failure(e) => assert(e.isInstanceOf[NegativeNumberException])
case Success(_) => fail("Should fail!")
}


Целых два варианта в Kotlin:

Try
https://www.javacodegeeks.com/2017/12/kotlin-try-type-functional-exception-handling.html

fun divideFn(dividend: String, divisor: String): Try<Int> {
val num = Try { dividend.toInt() }
val denom = Try { divisor.toInt() }
return num.flatMap { n ->
denom.map { d -> n / d } }
}

val result = divideFn("5t", "4")
when(result) {
is Success -> println("Got ${result.value}")
is Failure -> println("An
error : ${result.e}")
}


и Result
https://www.baeldung.com/kotlin/result-class

fun divide(a: Int, b: Int): Result {
return runCatching {
a / b
}
}

val resultValid = divide(10, 2)
assertTrue(resultValid.isSuccess)
assertEquals(5, resultValid.getOrNull())


Тоже два варианта - Option и Result - в Rust
https://habr.com/ru/articles/270371/

fn extension_explicit(file_name: &str) -> Option<&str> {
match find(file_name, '.') {
None => None,
Some(i) => Some(&file_name[i+1..]),
}
}


fn double_number(number_str: &str) -> Result<i32, ParseIntError> {
match number_str.parse::<i32>() {
Ok(n) => Ok(2 * n),
Err(err) => Err(err),
}
}


Основные особенности у всех этих вариантов:
1) автоматическое оборачивание исключения в класс
2) сохранение информации об ошибке
3) сопоставление типа (class pattern matching)

Что интересно, class pattern matching появился в Java в виде JEP 406: Pattern Matching for switch, а значит можно реализовать что-то похожее. Например, вот так:
https://habr.com/ru/articles/721326/

#error_handling #null_safety #java #comparision #kotlin #scala #rust