139 lines
5.0 KiB
Plaintext
139 lines
5.0 KiB
Plaintext
== Why is this an issue?
|
||
|
||
It is a common pattern to validate required preconditions at the beginning of a function or block. There are two different kinds of preconditions:
|
||
|
||
* Preconditions about argument values. An example is the assertion that a function argument lies within a specific range. An `IllegalArgumentException` should be thrown if these preconditions are violated.
|
||
* Preconditions about the state of the owner or the execution context of the function. An example is when a specific method, such as `open`, `init` or `prepare`, must be called before the current method can be executed. An `IllegalStateException` should be thrown if these preconditions are violated.
|
||
|
||
The Kotlin standard library provides the functions `check()`, `require()`, `checkNotNull()` and `requireNotNull()` for this purpose.
|
||
They should be used instead of directly throwing an `IllegalArgumentException` or an `IllegalStateException`.
|
||
|
||
=== What is the potential impact?
|
||
|
||
==== Readability and Understanding
|
||
|
||
This change makes it easier to understand the code
|
||
because the semantics of `check()`, `require()`, `checkNotNull()` and `requireNotNull()`,
|
||
as well as the fact that this is a preconditions check,
|
||
are evident to the reader.
|
||
When developers share common standards and idioms, they need to spend less effort understanding each other's code.
|
||
|
||
==== Code Redundancy
|
||
|
||
Using a built-in language feature or a standard API is always better than a custom implementation,
|
||
because the reimplementation of something that already exists is unnecessary.
|
||
|
||
==== Consistency
|
||
|
||
When `check()`, `require()`, `checkNotNull()` and `requireNotNull()` are used in an idiomatic way,
|
||
there is more consistency in what kind of exception is thrown in which situation.
|
||
|
||
== How to fix it
|
||
|
||
[cols="3,2"]
|
||
|===
|
||
| **Replace**
|
||
| **With**
|
||
|
||
| `if (!condition) throw IllegalArgumentException()`
|
||
| `require(condition)`
|
||
|
||
| `if (reference == null) throw IllegalArgumentException()`
|
||
| `requireNotNull(reference)`
|
||
|
||
| `require(reference != null)`
|
||
| `requireNotNull(reference)`
|
||
|
||
| `if (!condition) throw IllegalStateExceptionException()`
|
||
| `check(condition)`
|
||
|
||
| `if (reference == null) throw IllegalStateException()`
|
||
| `checkNotNull(reference)`
|
||
|
||
| `check(reference != null)`
|
||
| `checkNotNull(reference)`
|
||
|
||
| `throw IllegalStateException()`
|
||
| `error()`
|
||
|===
|
||
|
||
A constructor function for the exception message can be provided
|
||
as an optional argument
|
||
for `check()`, `require()`, `checkNotNull()` and `requireNotNull()`.
|
||
This means the message is constructed only if the exception is thrown.
|
||
For the `error()` function, an optional error message parameter can be provided directly.
|
||
That is, without a parameter, because an exception is unconditionally thrown by `error()`.
|
||
|
||
=== Code examples
|
||
|
||
==== Noncompliant code example
|
||
|
||
[source,kotlin,diff-id=1,diff-type=noncompliant]
|
||
----
|
||
fun argumentPreconditions(argument: Int?, limit: Int?) {
|
||
if (argument == null) throw IllegalArgumentException() // Noncompliant, replace with requireNotNull
|
||
require(limit != null) // Noncompliant, replace with requireNotNull
|
||
if (argument < 0) throw IllegalArgumentException() // Noncompliant, replace with require
|
||
if (argument >= 0) throw IllegalArgumentException("Argument < $limit") // Noncompliant, replace with require
|
||
}
|
||
----
|
||
|
||
==== Compliant solution
|
||
|
||
[source,kotlin,diff-id=1,diff-type=compliant]
|
||
----
|
||
fun argumentPreconditions(argument: Int?, limit: Int?) {
|
||
requireNotNull(argument) // Compliant
|
||
requireNotNull(limit) // Compliant
|
||
require(argument >= 0) // Compliant
|
||
require(argument < limit) {"Argument < $limit"} // Compliant
|
||
}
|
||
----
|
||
|
||
==== Noncompliant code example
|
||
|
||
[source,kotlin,diff-id=2,diff-type=noncompliant]
|
||
----
|
||
fun statePreconditions() {
|
||
if (state == null) throw IllegalStateException() // Noncompliant, replace with checkNotNull
|
||
check(ioBuffer != null) // Noncompliant, replace with checkNotNull
|
||
if (state < 0) throw IllegalStateException() // Noncompliant, replace with check
|
||
if (state == 42) throw IllegalStateException("Unknown question") // Noncompliant, replace with check
|
||
|
||
when(state) {
|
||
0..10 -> processState1()
|
||
11..1000 -> processState2()
|
||
else -> throw IllegalStateException("Unexpected state $state") // Noncompliant, replace with error
|
||
}
|
||
}
|
||
----
|
||
|
||
==== Compliant solution
|
||
|
||
[source,kotlin,diff-id=2,diff-type=compliant]
|
||
----
|
||
fun statePreconditions() {
|
||
checkNotNull(state) // Compliant
|
||
checkNotNull(ioBuffer) // Compliant
|
||
check(state >= 0) // Compliant
|
||
check(state != 42) {"Unknown question"} // Compliant
|
||
|
||
when(state) {
|
||
0..10 -> processState1()
|
||
11..1000 -> processState2()
|
||
else -> error("Unexpected state $state") // Compliant
|
||
}
|
||
}
|
||
----
|
||
|
||
== Resources
|
||
|
||
=== Documentation
|
||
|
||
* https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/require.html[Kotlin API Docs, require]
|
||
* https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/check.html[Kotlin API Docs, check]
|
||
|
||
=== Articles & blog posts
|
||
|
||
* https://bignerdranch.com/blog/write-better-code-using-kotlins-require-check-and-assert/[Jeremy W. Sherman, Write Better Code Using Kotlin’s Require, Check and Assert]
|