DO use getters for operations that conceptually access properties.
Deciding when a member should be a getter versus a method is a challenging, subtle, but important part of good API design, hence this very long guideline. Some other language’s cultures shy away from getters. They only use them when the operation is almost exactly like a field—it does a miniscule amount of calculation on state that lives entirely on the object. Anything more complex or heavyweight than that gets
() after the name to signal “computation goin’ on here!” because a bare name after a
. means “field”.
Dart is not like that. In Dart, all dotted names are member invocations that may do computation. Fields are special—they’re getters whose implementation is provided by the language. In other words, getters are not “particularly slow fields” in Dart; fields are “particularly fast getters”.
Even so, choosing a getter over a method sends an important signal to the caller. The signal, roughly, is that the operation is “field-like”. The operation, at least in principle, could be implemented using a field, as far as the caller knows. That implies:
- The operation does not take any arguments and returns a result.
- The caller cares mostly about the result. If you want the caller to worry about how the operation produces its result more than they do the result being produced, then give the operation a verb name that describes the work and make it a method.
This does not mean the operation has to be particularly fast in order to be a getter.
O(n), and that’s OK. It’s fine for a getter to do significant calculation. But if it does a surprising amount of work, you may want to draw their attention to that by making it a method whose name is a verb describing what it does.
connection.nextIncomingMessage; // Does network I/O. expression.normalForm; // Could be exponential to calculate.
- The operation does not have user-visible side effects. Accessing a real field does not alter the object or any other state in the program. It doesn’t produce output, write files, etc. A getter shouldn’t do those things either.
The “user-visible” part is important. It’s fine for getters to modify hidden state or produce out of band side effects. Getters can lazily calculate and store their result, write to a cache, log stuff, etc. As long as the caller doesn’t care about the side effect, it’s probably fine.
stdout.newline; // Produces output. list.clear; // Modifies object.
The operation is idempotent. “Idempotent” is an odd word that, in this context, basically means that calling the operation multiple times produces the same result each time, unless some state is explicitly modified between those calls. (Obviously,
list.lengthproduces different results if you add an element to the list between calls.)
“Same result” here does not mean a getter must literally produce an identical object on successive calls. Requiring that would force many getters to have brittle caching, which negates the whole point of using a getter. It’s common, and perfectly fine, for a getter to return a new future or list each time you call it. The important part is that the future completes to the same value, and the list contains the same elements.
In other words, the result value should be the same in the aspects that the caller cares about.
DateTime.now; // New result each time.
The resulting object doesn’t expose all of the original object’s state. A field exposes only a piece of an object. If your operation returns a result that exposes the original object’s entire state, it’s likely better off as a
If all of the above describe your operation, it should be a getter. It seems like few members would survive that gauntlet, but surprisingly many do. Many operations just do some computation on some state and most of those can and should be getters.
rectangle.area; collection.isEmpty; button.canShow; dataSet.minimumValue;
DO use setters for operations that conceptually change properties.
For a setter, “field-like” means:
The operation takes a single argument and does not produce a result value.
The operation changes some state in the object.
The operation is idempotent. Calling the same setter twice with the same value should do nothing the second time as far as the caller is concerned. Internally, maybe you’ve got some cache invalidation or logging going on. That’s fine. But from the caller’s perspective, it appears that the second call does nothing.
rectangle.width = 3; button.visible = false;
PREFER making fields and top-level variables
State that is not mutable—that does not change over time—is easier for programmers to reason about. Classes and libraries that minimize the amount of mutable state they work with tend to be easier to maintain.
Of course, it is often useful to have mutable data. But, if you don’t need it, your default should be to make fields and top-level variables
final when you can.
null from members whose return type is
It can be occasionally useful to have a “nullable primitive” type in your API, for example to indicate the absence of a value for some key in a map, but these should be rare.
If you do have a member of this type that may return
null, document it very clearly, including the conditions under which
null will be returned.
this from methods just to enable a fluent interface.
var buffer = StringBuffer() ..write('one') ..write('two') ..write('three');
var buffer = StringBuffer() .write('one') .write('two') .write('three');
Users think of getters and setters as visible properties of an object. A “dropbox” property that can be written to but not seen is confusing and confounds their intuition about how properties work. For example, a setter without a getter means you can use
= to modify it, but not
This guideline does not mean you should add a getter just to permit the setter you want to add. Objects shouldn’t generally expose more state than they need to. If you have some piece of an object’s state that can be modified but not exposed in the same way, use a method instead.
Exception: An Angular component class may expose setters that are invoked from a template to initialize the component. Often, these setters are not intended to be invoked from Dart code and don’t need a corresponding getter. (If they are used from Dart code, they should have a getter.)