Everything else is just a tool to model your domain in a way that’s easier to read and reason about, and to produce much shorter code. Interfaces define module boundaries and can be mocked to enable simpler testing. Through classes, objects, and data structures, you can model your domain entities. Enums make a list of limited options crystal clear. Extensions allow you to add functionality to existing types, without having access to their original source code, giving advantage of better logic structure. With generics, you can write universal code, promoting reuse. And overloaded operators give that extra edge to common operations that benefit from succinct, crisp syntax.
Object types (classes)
Custom types that focus on behavior. Object types allow you to model your domain, so you can reason about it more easily. You can give things familiar names, group properties and operations, define relationships, and form as many layers of abstraction as you like. These are subjective representations of the author’s imagination. Oftentimes quite useful, as we are used to think in models of the world.
Object types (classes)
Classes have a primary constructor and can have secondary (convenience) constructors. They can have static initializer init blocks, properties stored and computed, methods.
There are public, internal, private visibility modifiers, and also protected that can be used when Inheritance is involved. protected is like private, but with access for children. public is the default.
val componentStatistics = mutableMapOf<String, Int>()
public class Component(
type: String,
alias: String = "",
private val onApply: (Component) -> Unit,
val children: Collection<Component>,
var connections: Int = 0
) {
// Convenience
constructor(
type: String,
alias: String = "",
connections: Int = 0,
onApply: (Component) -> Unit,
): this(type, alias, onApply, emptyList(), connections)
// Convenience, indirect delegation
constructor(
type: String,
connectedTo: Component,
onApply: (Component) -> Unit,
): this(type, connections = 1, onApply = onApply)
val name = if (alias.isBlank()) type else "$alias ($type)"
init {
val count = componentStatistics.getOrDefault(type, 0)
componentStatistics[type] = count + 1
}
inline val hasChildren get() = children.isNotEmpty()
internal var isActive: Boolean = false
get() = field
private set(value) {
field = value
}
fun activate() {
isActive = true
children.forEach { it.activate() }
onApply(this)
}
}
val component = Component("example", onApply = {})
component.activate()
Static (class-level) functionality is implemented via companion objects.
lateinit properties are not required to be initialized during construction. They are promised to be initialized “just in time” later by some other means, when it is not straightforward. It is possible to check if the lateinit property indeed .isInitialized already.
class Singleton private constructor() {
class Dependency
companion object {
const val COMPILE_TIME_CONSTANT = "static symbolic constant"
val shared = Singleton().apply {
dependency = Dependency()
}
private var forYourEyesOnly = ""
}
lateinit var dependency: Dependency
fun doSomething() {
check(::dependency.isInitialized)
forYourEyesOnly = "something"
}
}
objects have a single instance of their type. It’s a type definition and it’s instantiated automatically on first access. They don’t have constructors because they don’t have parameters. Think alternatives to Unit with methods and properties, and Inheritance.
object AlsoSingleton