Problem overview
A thermostat keeps a target temperature, and the target must always stay between 10 and 30 degrees. If any code could assign to the target directly, nothing would stop it from setting 95. So the target is a private field, written #target, and only the thermostat's own methods can change it. Every one of them keeps it between 10 and 30.
new Thermostat()starts at 20.setTarget(t)sets the target tot, but a value below 10 becomes 10 and a value above 30 becomes 30.warmer()raises the target by 1 andcooler()lowers it by 1, but neither takes it outside 10 to 30.getTarget()returns the target. It is the only way for other code to read it.
A new thermostat reports 20. Two calls to warmer() make it 22, and cooler() brings it to 21. After setTarget(35) it reports 30, and warmer() leaves it at 30.
Approach
The rule "stay between 10 and 30" has to hold in every method that changes the target. Writing the check three times invites mistakes, so it lives in one place, setTarget. warmer and cooler compute the new value and pass it to setTarget, which clamps it. At 30, warmer() asks for 31, and setTarget turns that back into 30. All methods run in O(1) time.
Step by step
- Declare the private field. At the top of the class, write
#target;. The#is part of the name. Code outside the class can't use it. - Start at 20. In the constructor, assign 20 to
this.#target. - Too low. In
setTarget, aniffort < 10sets the target to 10. The value isn't refused: the target becomes the lowest one allowed. - Too high. An
else iffort > 30sets it to 30. - Everything else. The
elsebranch storest. The values 10 and 30 end up here, because they are allowed. - Warmer and cooler. Call
this.setTarget(this.#target + 1)andthis.setTarget(this.#target - 1). - Read it.
getTarget()returnsthis.#target.
The solution in JavaScript
class Thermostat {
#target;
constructor() {
this.#target = 20;
}
setTarget(t) {
if (t < 10) {
this.#target = 10;
} else if (t > 30) {
this.#target = 30;
} else {
this.#target = t;
}
}
warmer() {
this.setTarget(this.#target + 1);
}
cooler() {
this.setTarget(this.#target - 1);
}
getTarget() {
return this.#target;
}
}
Using the class
let t = new Thermostat();
console.log(t.getTarget());
t.warmer();
t.warmer();
console.log(t.getTarget());
t.cooler();
console.log(t.getTarget());
t = new Thermostat();
t.setTarget(35);
console.log(t.getTarget());
t.warmer();
console.log(t.getTarget());
t.cooler();
console.log(t.getTarget());
t = new Thermostat();
t.setTarget(4);
console.log(t.getTarget());
t.cooler();
console.log(t.getTarget());
t.setTarget(10);
t.warmer();
console.log(t.getTarget());
The three groups print:
20
22
21
30
30
29
10
10
11
What # protects
In JavaScript, a name starting with # is truly private. It must be declared in the class body, and using t.#target outside the class is a SyntaxError, not just a bad habit. A plain property such as this.target could be assigned from anywhere. Outside the class, t.target and t._target are both undefined, because the field's name is not an ordinary property name.
Assigning t.target = 95 from outside creates a new, unrelated property on the object. It doesn't touch the private field, and t.getTarget() still returns the real target.
Common mistakes
- Forgetting the declaration. A private field must be declared with
#target;in the class body beforethis.#targetis used. - Writing
this.targetinstead ofthis.#target. That is a different, public property, and the class would no longer protect the value. - Checking the limits in only one place. If
warmerdoesthis.#target++directly, nothing stops it from reaching 31. Going throughsetTargetkeeps the rule in one place. - Off-by-one at the edges. 10 and 30 are both allowed, so the checks use
<and>, not<=and>=.
Testing the edges
The interesting inputs are the ones at and beyond the limits. On one thermostat, calling setTarget with each of these values gives:
| setTarget(t) | getTarget() |
|---|---|
| -1000 | 10 |
| 9 | 10 |
| 10 | 10 |
| 30 | 30 |
| 31 | 30 |
| 1000 | 30 |
The same holds for the step methods. From 29, warmer() gives 30, and a second warmer() still gives 30. From 11, cooler() gives 10, and a second cooler() still gives 10. The extreme inputs in the constraints, -1000 and 1000, need no special handling: they are just values below 10 and above 30.
Why a getter and not a public field
Reading the target through getTarget() looks like extra work, but it is the price of the guarantee. Code outside can see the value, but the only ways to change it are setTarget, warmer and cooler, and all three keep it in range. If the rule ever changes, say to 5 through 35, only the class changes, and no code that uses a thermostat has to be touched.
Another way: clamping with Math.min and Math.max
The three-branch if can be written in one line. Math.max(10, t) lifts a too-small value to 10, and wrapping it in Math.min(30, ...) cuts a too-large value down to 30:
setTarget(t) {
this.#target = Math.min(30, Math.max(10, t));
}
With this version, calling setTarget with -1000, 9, 10, 30, 31 and 1000 leaves the target at 10, 10, 10, 30, 30 and 30, the same as the table above. The lesson's if chain is easier to read while you learn, and the clamp is a common way to write the same rule once you know it.
