Constraining Visibility
If you leave a piece of code for a few days or weeks, then come back to it, you might see a much better way to write it.
This is one of the prime motivations for refactoring, which rewrites working code to make it more readable, understandable, and thus maintainable.
There is a tension in this desire to change and improve your code. Consumers (client programmers) require aspects of your code to be stable. You want to change it, and they want it to stay the same.
This is particularly important for libraries. Consumers of a library don’t want to rewrite code for a new version of that library. However, the library creator must be free to make modifications and improvements, with the certainty that the client code won’t be affected by those changes.
Therefore, a primary consideration in software design is:
Separate things that change from things that stay the same.
To control visibility, Kotlin and some other languages provide access
modifiers. Library creators decide what is and is not accessible by the client
programmer using the modifiers public, private, protected, and internal.
This atom covers public and private, with a brief introduction to
internal. We explain protected later in the book.
An access modifier such as private appears before the definition for a class,
function, or property. An access modifier only controls access for that
particular definition.
A public definition is accessible by client programmers, so changes to that
definition impact client code directly. If you don’t provide a modifier, your
definition is automatically public, so public is technically redundant. You
will sometimes still specify public for the sake of clarity.
A private definition is hidden and only accessible from other members of the
same class. Changing, or even removing, a private definition doesn’t directly
impact client programmers.
private classes, top-level functions, and top-level properties are accessible
only inside that file:
// Visibility/RecordAnimals.kt
private var index = 0 // [1]
private class Animal(val name: String) // [2]
private fun recordAnimal( // [3]
animal: Animal
) {
println("Animal #$index: ${animal.name}")
index++
}
fun recordAnimals() {
recordAnimal(Animal("Tiger"))
recordAnimal(Animal("Antelope"))
}
fun recordAnimalsCount() {
println("$index animals are here!")
}
You can access private top-level properties ([1]), classes ([2]), and
functions ([3]) from other functions and classes within RecordAnimals.kt.
Kotlin prevents you from accessing a private top-level element from within
another file, telling you it’s private in the file:
// Visibility/ObserveAnimals.kt
fun main() {
// Can't access private members
// declared in another file.
// Class is private:
// val rabbit = Animal("Rabbit")
// Function is private:
// recordAnimal(rabbit)
// Property is private:
// index++
recordAnimals()
recordAnimalsCount()
}
/* Output:
Animal #0: Tiger
Animal #1: Antelope
2 animals are here!
*/
Privacy is most commonly used for members of a class:
// Visibility/Cookie.kt
class Cookie(
private var isReady: Boolean // [1]
) {
private fun crumble() = // [2]
println("crumble")
public fun bite() = // [3]
println("bite")
fun eat() { // [4]
isReady = true // [5]
crumble()
bite()
}
}
fun main() {
val x = Cookie(false)
x.bite()
// Can't access private members:
// x.isReady
// x.crumble()
x.eat()
}
/* Output:
bite
crumble
bite
*/
-
[1] A
privateproperty, not accessible outside the containing class. -
[2] A
privatemember function. -
[3] A
publicmember function, accessible to anyone. -
[4] No access modifier means
public. -
[5] Only members of the same class can access
privatemembers.
The private keyword means no one can access that member except other members
of that class. Other classes cannot access private members, so it’s as if
you’re also insulating the class against yourself and your collaborators. With
private, you can freely change that member without worrying whether it
affects another class in the same package. As a library designer you’ll
typically keep things as private as possible, and expose only functions and
classes to client programmers.
Any member function that is a helper function for a class can be made
private to ensure you don’t accidentally use it elsewhere in the package and
thus prohibit yourself from changing or removing that function.
The same is true for a private property inside a class. Unless you must
expose the underlying implementation (which is less likely than you might
think), make properties private. However, just because a reference to an
object is private inside a class doesn’t mean some other object can’t have a
public reference to the same object:
// Visibility/MultipleRef.kt
class Counter(var start: Int) {
fun increment() {
start += 1
}
override fun toString() = start.toString()
}
class CounterHolder(counter: Counter) {
private val ctr = counter
override fun toString() =
"CounterHolder: " + ctr
}
fun main() {
val c = Counter(11) // [1]
val ch = CounterHolder(c) // [2]
println(ch)
c.increment() // [3]
println(ch)
val ch2 = CounterHolder(Counter(9)) // [4]
println(ch2)
}
/* Output:
CounterHolder: 11
CounterHolder: 12
CounterHolder: 9
*/
-
[1]
cis now defined in the scope surrounding the creation of theCounterHolderobject on the following line. -
[2] Passing
cas the argument to theCounterHolderconstructor means that the newCounterHoldernow refers to the sameCounterobject thatcrefers to. -
[3] The
Counterthat is supposedlyprivateinsidechcan still be manipulated viac. -
[4]
Counter(9)has no other references except withinCounterHolder, so it cannot be accessed or modified by anything exceptch2.
Maintaining multiple references to a single object is called aliasing and can produce surprising behavior.
Modules
Unlike the small examples in this book, real programs are often large. It can be helpful to divide such programs into one or more modules. A module is a logically independent part of a codebase. The way you divide a project into modules depends on the build system (such as Gradle or Maven) and is beyond the scope of this book.
An internal definition is accessible only inside the module where it is
defined. internal lands somewhere between private and public—use it
when private is too restrictive but you don’t want an element to be a part of
the public API. We do not use internal in the book’s examples or exercises.
Modules are a higher-level concept. The following atom introduces packages,
which enable finer-grained structuring. A library is often a single module
consisting of multiple packages, so internal elements are available within
the library but are not accessible by consumers of that library.
Exercises and solutions can be found at www.AtomicKotlin.com.