Problem overview
A thermostat keeps a target temperature, and the target must always stay between 10 and 30 degrees. If any code could assign the target directly, nothing would stop it from setting 95. So the target is a private property: only the thermostat's own methods can change it, and each of them keeps it in range.
You write the class Thermostat:
- A new thermostat has a target of 20.
setTarget(t)sets the target tot. A value below 10 becomes 10 and a value above 30 becomes 30.warmer()raises the target by 1 andcooler()lowers it by 1, never leaving the range 10 to 30.getTarget()returns the target. It is the only way for other code to read it.
After setTarget(35) the target is 30. A further warmer() leaves it at 30, and cooler() brings it to 29.
Approach: one place that enforces the rule
The rule "between 10 and 30" could be checked in three places: in setTarget, in warmer and in cooler. Repeating it three times invites mistakes, and if the limits ever change you must find all three. Better to write the rule once, in setTarget, and let the other two methods go through it. warmer calls setTarget(target + 1). At 30 that asks for 31, and setTarget turns it back into 30. cooler works the same way at the bottom: at 10, the request for 9 becomes 10.
This is the point of making the property private: the class controls every way of changing it, so the rule only has to be correct in the class.
Step by step
- The property. At the top of the class, declare
private var target: Int = 20. It is avarbecause the methods change it, andprivateso that only they can. Every new thermostat starts at 20. - Too low. In
setTarget, aniffort < 10setstargetto 10. A value that is too low isn't refused; it becomes the lowest allowed target. - Too high. An
else iffort > 30setstargetto 30. - Everything else. The
elsesetstargettot. The values 10 and 30 themselves are allowed, so they end up here and are kept as they are. - warmer and cooler call
setTargetwithtarget + 1andtarget - 1. - getTarget returns
target.
The solution in Kotlin
class Thermostat {
private var target: Int = 20
fun setTarget(t: Int) {
if (t < 10) {
target = 10
} else if (t > 30) {
target = 30
} else {
target = t
}
}
fun warmer() {
setTarget(target + 1)
}
fun cooler() {
setTarget(target - 1)
}
fun getTarget(): Int {
return target
}
}
Inside the class, target is visible to every method. Outside it is not. Code like this does not compile:
fun main() {
val t = Thermostat()
t.target = 95
}
The compiler reports that it cannot access the variable target because it is private in Thermostat. The only ways in are the public methods, so the target can never leave the range. (Kotlin also lets you write properties with custom getters and setters, but this lesson uses explicit methods.)
Using the class
fun main() {
val th = Thermostat()
println(th.getTarget())
th.setTarget(35)
println(th.getTarget())
th.warmer()
println(th.getTarget())
th.cooler()
println(th.getTarget())
th.setTarget(4)
println(th.getTarget())
th.cooler()
println(th.getTarget())
th.setTarget(10)
th.warmer()
println(th.getTarget())
}
It prints:
20
30
30
29
10
10
11
Read it from the top: a new thermostat is at 20; 35 is clamped to 30; warmer() can't go past 30; cooler() gives 29; 4 is clamped to 10 and cooler() can't go lower; finally 10 is allowed as it is, and warmer() raises it to 11.
Why a private property can share its name with a getter
The property is called target and the method getTarget(). In Kotlin a public property target would get a generated getter with the same JVM name as your method, and the compiler would complain about a clash. A private property gets no generated getter, so the explicit getTarget() is fine. That is one more benefit of keeping the property private: the class owns its storage, and the methods are its whole public surface.
Testing it
Good inputs are the ones that touch the boundaries:
- Thirty-one calls to
warmer()on a new thermostat:getTarget()then returns 30. Ten calls already give 30, so the extra ones change nothing. setTarget(30)thenwarmer(): the target is 30.setTarget(10)thencooler(): the target is 10.setTarget(-1000)andsetTarget(1000), the extremes the constraints allow: 10 and 30.
Common mistakes
- Getting the limits off by one. A condition like
t < 11ort > 29would wrongly squash the allowed values 10 and 30. Test the boundary values themselves. - Refusing out-of-range values. The rule is to clamp them to the nearest limit, not to ignore the call.
- Writing
target++inwarmer. It skips the range check, so the target could reach 31. Go throughsetTarget. - Making the property public. The code would still compile and pass simple tests, but any caller could set 95. The
privatekeyword is what makes the rule binding. - Giving the property and the method the same name. Here the property is
targetand the method isgetTarget(), so they stay distinct.
