Visibility
In all of the classes we’ve seen so far, the properties and methods have
had public visibility, meaning that any other code has access to them.
This level of visibility is the default, which is why we didn’t use the
public keyword with any of the definitions.
But other levels of visibility are possible in classes. For example, we
can declare members of a class using the private keyword. If you make a
property or method private, then it can be used only within the class that
defines that property or method. Code outside the class will not be able
to access it.
The ability to hide class members by making them private is important and useful. Let’s consider an example where public visibility of a class member causes problems, and examine how making that member private fixes things.
An Example
Imagine that you have a class Dataset, representing numbers read from
a file:
class Dataset {
val values = mutableListOf<Double>()
fun loadData(filename: String) { ... }
}
The property that stores all the values is a mutable list because we need to add values to it one at a time, as we read them from the file.
A Dataset object is initially empty. We populate it with values by
invoking the loadData() method, providing the name of the file containing
the data:
val dataset = Dataset()
dataset.loadData("data.txt")
After loading data, we can do things like query the size of the dataset, test its first value, or iterate over the values in order to print them all:
println(dataset.values.size)
if (dataset.values[0] < 0.0) {
println("Dataset begins with negative value")
}
for (value in dataset.values) {
println(value)
}
These operations are all useful—although having to access the values
property explicitly each time is a little inconvenient.
However, it is also possible to replace values, remove values, or add values:
dataset.values[1] = 0.0 // zeroes second value
dataset.values.removeAt(0) // removes value at index 0
dataset.values.add(1.5) // adds 1.5 to end
This is a big problem! A Dataset object is supposed to represent the data
read from a file. If we allow the contents of values to be modified in any
way, then it will no longer represent the contents of the file accurately.
Fixing Things
The solution to the issues noted above is to declare values as private
and then provide controlled access to it via additional computed properties
and methods:
class Dataset {
private val values = mutableListOf<Double>()
fun loadData(filename: String) { ... }
val size get() = values.size
operator fun get(index: Int) = values.get(index)
operator fun iterator() = values.iterator()
}
This new version of Dataset has
-
A computed property
size, that simply returns thesizeproperty ofvalues -
An element access operator that gives read-only access to elements of
values, using[]and an integer index -
An iterator function that allows a
Datasetto be used as the subject of aforloop
No other parts of the list API are exposed to users of Dataset. Thus
it is no longer possible for users of the class to replace anything in
values, remove anything in values or add anything to values.
It is still possible to query dataset size, read an individual value or iterate over all values—with the added bonus that these things can now be done with slightly simpler syntax:
println(dataset.size)
if (dataset[0] < 0.0) {
println("Dataset begins with negative value")
}
for (value in dataset) {
println(value)
}
API vs Implementation
When you define the properties and methods of a class, think carefully about whether you want those properties and methods to be part of the public API of the class, or whether they should be part of the private implementation.
Every class will need a public API of some sort, but the public API becomes hard to change as soon as other code starts using that class, as changing a public feature is likely to break that code. Implementation details, on the other hand, are free to change whenever you need them to, without breaking any code that uses the class.
For example, the computed property size is part of the public API of
Dataset. If we changed its name to length, this would probably break
code that uses the class.
The values property, on the other hand, is no longer part of the public
API. It is now an implementation detail. If we later decide that a different
type of collection would be better for storing the individual values of a
dataset, we can make that change without breaking any code.