Constraining Visibility

All good writers—including those who write software—know that a piece of work isn’t good until it’s been rewritten, often many times.

If you leave a piece of code in a drawer for a while and come back to it, you might see a much better way to do things. 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, however, in this desire to change and improve your code. Consumers (client programmers) need aspects of your code to be stable. You want to change it; they want it to stay the same. Thus a primary consideration in object-oriented design is to separate things that change from things that stay the same.

This is particularly important for libraries. Consumers of that library must rely on the parts they use. They don’t want to rewrite code for a new version of the library. On the flip side, 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.

To solve this problem, Kotlin (along with a number of other languages) provides access modifiers to allow library creators to decide what is available to the client programmer. Kotlin’s access levels include public, private, protected, and internal. This atom covers the public and private modifiers, while the remaining modifiers are explained later in the book.

An access modifier like public or private appears before the definition for a class, function or property. Each access modifier only controls the access for that particular definition.

A public definition is available to everyone, in particular to the client programmer who uses the library. Thus, any changes to a public definition will impact client code. If you don’t provide a modifier, your definition is automatically public. For clarity, you might still sometimes explicitly specify public.

Because a private definition is not available to everyone, you can generally change it without worry about how this will impact client programmers.

If you declare a class, top-level function, or property as private, it is available only inside that same 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 properties ([1]), classes ([2]), and functions ([3]) from other functions and classes in the RecordAnimals.kt file. Kotlin prevents you from accessing a private top-level declaration from another file, telling you it’s private within the file:

// Visibility/ObserveAnimals.kt fun main(args: Array<String>) { // 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! */

You can make private 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 eatUp() { // [4] isReady = true // [5] crumble() bite() } } fun main(args: Array<String>) { val x = Cookie(false) x.bite() // Can't access private members: // x.isReady // x.crumble() x.eatUp() } /* Output: bite crumble bite */

The private keyword means no one can access that member except the class that contains that member, via other members of that class. Other classes cannot access private members, so it’s as if you’re even insulating the class against yourself. On the other hand, it’s not unlikely that the code is developed by several 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 everything as private as possible, and expose only functions and classes you want client programmers to use.

Any member function that you’re certain is only a helper function for that 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 the 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(args: Array<String>) { 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 */
This phenomenon is called aliasing and can produce surprising behavior. Access modifiers are useful, but they are no panacea, and no substitute for programmer vigilance.

Previous          Next

©2018 Mindview LLC. All Rights Reserved.