Properties
To build software that is safe and maintainable, state should be private. No entity outside of an object should be able to modify the state except through carefully designed public behaviors. If state is public, then clients may modify it in an uncontrolled manner and leave the object in an inconsistent state. Consider this JavaScript program that demonstrates how a Vector2
may be made inconsistent:
class Vector2 {
constructor(x, y) {
this.x = x;
this.y = y;
this.magnitude = Math.sqrt(x * x + y * y);
}
}
const offset = new Vector2(3, 4);
console.log(offset.magnitude); // prints 5 correctly
offset.x = 0;
console.log(offset.magnitude); // prints 5 incorrectly
Prior to 2022, JavaScript had no notion of private state, so x
could be freely modified by clients. But the magnitude would no longer correct after the modification.
To keep an object consistent, all state must be private, and any clients that need to access it must use getters and setters. The setters offer a hook for the class designer to update any associated or derived state. This revised JavaScript program updates magnitude
whenever x
is set:
class Vector2 {
constructor(x, y) {
this.x = x;
this.y = y;
this.synchronizeMagnitude();
}
getX() {
return this.x;
}
setX(value) {
this.x = value;
this.synchronizeMagnitude();
}
synchronizeMagnitude();
this.magnitude = Math.sqrt(this.x * this.x + this.y * this.y);
}
}
const offset = new Vector2(3, 4);
console.log(offset.magnitude); // prints 5 correctly
offset.setX(0);
console.log(offset.magnitude); // prints 4 correctly
Channeling access through getters and setters increases the safety of the code and the flexibility of a class's implementation. However, it also makes the client code noiser with a proliferation of method calls. Some languages provide a middle ground that offers safety, flexibility, and low noise through properties. Properties are virtual state. They look like state, but they are really behaviors. They provide the syntactic illusion of direct access to state, but the references and assignments to a property are really calls to getters and setters.
In JavaScript, virtual state is added to an object using special get
and set
methods. The get x()
method is called wherever an rvalue expression like offset.x
appears in the code. The set x(value)
method is called wherever an lvalue assignment like offset.x = value
appears in the code. The setter method gives the opportunity for the magnitude to be updated even though offset.x = 0
looks like a plain old assignment statement to just the x
variable:
class Vector2 {
constructor(x, y) {
this._x = x;
this._y = y;
this.synchronizeMagnitude();
}
get x() {
// return this.x; <- infinite recursion!
return this._x;
}
set x(value) {
this._x = value;
this.synchronizeMagnitude();
}
synchronizeMagnitude();
this.magnitude = Math.sqrt(this._x * this._x + this._y * this._y);
}
}
const offset = new Vector2(3, 4);
console.log(offset.magnitude); // prints 5 correctly
offset.x = 0;
console.log(offset.magnitude); // prints 4 correctly
The instance variables and properties cannot have the same name. Otherwise the getter and setter would recursively call themselves and overflow the stack. Here, the instance variable names have been prefixed with _
to hint that they are private state.
The syntax for properties varies wildly across languages. This Kotlin class has a property for the magnitude:
import kotlin.math.sqrt
class Vector2(var x: Double, var y: Double) {
val magnitude: Double
get() = sqrt(x * x + y * y)
}
fun main() {
val p = Vector2(3.0, 4.0)
println(p.magnitude) // prints 5
p.x = 0.0
println(p.magnitude) // prints 4
}
The magnitude is purely a behavior. There is no backing field. This is perhaps a better design than the JavaScript implementation since there's no derived and cached state that needs to be updated when x
and y
change. A setter for the magnitude could be added. It wouldn't set the magnitude, since there isn't state for it. Rather, it would scale x
and y
to reach the desired magnitude.
An object's state in Ruby is always private. Unlike Java and C++, not even other instances of the same class may access it. Instead, you expose the state through getters and setters:
class Vector2
def initialize(x, y)
@x = x
@y = y
end
def x
@x
end
def x=(value)
@x = value
end
# ...
end
Adding getters and setters for instance variables is tedious, and Ruby therefore offers the attr_accessor
helper method to generate them automatically. You call this method from just inside the class, passing the names of the instance variables as symbols:
class Vector2
attr_accessor :x, :y
def initialize(x, y)
@x = x
@y = y
end
end
The attr_reader
method generates just a getter, the attr_writer
just a setter, and attr_accessor
both a getter and a setter.
These generators only work when there's a real backing field to access. Adding a getter for a virtual magnitude is no different than adding a regular method:
class Vector2
attr_accessor :x, :y
def initialize(x, y)
@x = x
@y = y
end
def magnitude
Math.sqrt(x * x + y * y)
end
end
offset = Vector2.new(3, 4)
puts offset.magnitude # prints 5
offset.x = 0
puts offset.magnitude # prints 4