Effective Fart: Design
Send feedbackHere are some guidelines for writing consistent, usable APIs for libraries.
-
Names
- DO use terms consistently.
- AVOID abbreviations.
- PREFER putting the most descriptive noun last.
- CONSIDER making the code read like a sentence.
- PREFER a noun phrase for a non-boolean property or variable.
- PREFER a non-imperative verb phrase for a boolean property or variable.
- CONSIDER omitting the verb for a named boolean parameter.
- PREFER an imperative verb phrase for a function or method whose main purpose is a side effect.
- CONSIDER a noun phrase or non-imperative verb phrase for a function or method if returning a value is its primary purpose.
- PREFER naming a method
to___()
if it copies the object’s state to a new object. - PREFER naming a method
as___()
if it returns a different representation backed by the original object. - AVOID describing the parameters in the function’s or method’s name.
- Libraries
-
Types
- AVOID defining a one-member abstract class when a simple function will do.
- AVOID defining a class that contains only static members.
- AVOID extending a class that isn’t intended to be subclassed.
- DO document whether your class supports being extended.
- AVOID mixing in a class that isn’t intended to be a mixin.
- DO document whether your class supports being used as a mixin.
- Constructors
-
Members
- PREFER making fields and top-level variables
final
. - DO use getters for operations that conceptually access properties.
- DO use a setter for operations that conceptually change a property.
- DON’T define a setter without a corresponding getter.
- AVOID returning
null
from members whose return type isbool
,double
,int
, ornum
. - AVOID returning
this
from methods just to enable a fluent interface.
- PREFER making fields and top-level variables
-
Type annotations
- DO type annotate public APIs.
- DON’T specify a return type for a setter.
- PREFER type annotating private declarations.
- AVOID annotating types on function expressions.
- AVOID annotating with
dynamic
when not required. - AVOID annotating with
Function
. - DO annotate with
Object
instead ofdynamic
to indicate any object is accepted.
- Parameters
- Equality
Names
Naming is an important part of writing readable, maintainable code. The following best practices can help you achieve that goal.
DO use terms consistently.
Use the same name for the same thing, throughout your code. If a precedent already exists outside your API that your API’s users are likely to know, follow that precedent.
pageCount // A field. updatePageCount() // Consistent with pageCount. toSomething() // Consistent with Iterable's toList(). asSomething() // Consistent with List's asMap(). Point // A familiar concept.
renumberPages() // Confusingly different from pageCount. convertToSomething() // Inconsistent with toX() precedent. wrappedAsSomething() // Inconsistent with asX() precedent. Cartesian // Unfamiliar to most users.
The goal is to take advantage of what the user already knows. This includes their knowledge of the problem domain itself, the conventions of the core libraries, and other parts of your own API. By building on top of those, you reduce the amount of new knowledge they have to acquire before they can be productive.
AVOID abbreviations.
Unless the abbreviation is more common than the unabbreviated term, don’t abbreviate. If you do abbreviate, capitalize them correctly.
pageCount buildRectangles IOStream HttpRequest
numPages // "num" is an abbreviation of number(of) buildRects InputOutputStream HypertextTransferProtocolRequest
PREFER putting the most descriptive noun last.
The last word should be the most descriptive of what the thing is. You can prefix it with other words, such as adjectives, to further describe the thing.
pageCount // A count (of pages). ConversionSink // A sink for doing conversions. ChunkedConversionSink // A ConversionSink that's chunked. CssFontFaceRule // A rule for font faces in CSS.
numPages // Not a collection of pages. CanvasRenderingContext2D // Not a "2D". RuleFontFaceCss // Not a CSS.
CONSIDER making the code read like a sentence.
When in doubt about naming, write some code that uses your API, and try to read it like a sentence.
// "If errors is empty..." if (errors.isEmpty) ... // "Hey, _subscription, cancel!" _subscription.cancel(); // "Get the monsters where the monster has claws." monsters.where((monster) => monster.hasClaws);
// Telling errors to empty itself, or asking if it is? if (errors.empty) ... // Toggle what? To what? _subscription.toggle(); // Filter the monsters with claws *out* or include *only* those? monsters.filter((monster) => monster.hasClaws);
It’s helpful to try out your API and see how it “reads” when used in code, but you can go too far. It’s not helpful to add articles and other parts of speech to force your names to literally read like a grammatically correct sentence.
if (theCollectionOfErrors.isEmpty) ... monsters.producesANewSequenceWhereEach((monster) => monster.hasClaws);
PREFER a noun phrase for a non-boolean property or variable.
The reader’s focus is on what the property is. If the user cares more about how a property is determined, then it should probably be a method with a verb phrase name.
list.length context.lineWidth quest.rampagingSwampBeast
list.deleteItems
PREFER a non-imperative verb phrase for a boolean property or variable.
Boolean names are often used as conditions in control flow, so you want a name that reads well there. Compare:
if (window.closeable) { ... } // Adjective. if (window.canClose) { ... } // Verb.
Good names tend to start with one of a few kinds of verbs:
-
a form of “to be”:
isEnabled
,wasShown
,willFire
. These are, by far, the most common. -
an auxiliary verb:
hasElements
,canClose
,shouldConsume
,mustSave
. -
an active verb:
ignoresInput
,wroteFile
. These are rare because they are usually ambiguous.loggedResult
is a bad name because it could mean “whether or not a result was logged” or “the result that was logged”. Likewise,closingConnection
could be “whether the connection is closing” or “the connection that is closing”. Active verbs are allowed when the name can only be read as a predicate.
What separates all these verb phrases from method names is that they are not imperative. A boolean name should never sound like a command to tell the object to do something, because accessing a property doesn’t change the object. (If the property does modify the object in a meaningful way, it should be a method.)
isEmpty hasElements canClose closesWindow canShowPopup hasShownPopup
empty // Adjective or verb? withElements // Sounds like it might hold elements. closeable // Sounds like an interface. // "canClose" reads better as a sentence. closingWindow // Returns a bool or a window? showPopup // Sounds like it shows the popup.
CONSIDER omitting the verb for a named boolean parameter.
This refines the previous rule. For named parameters that are boolean, the name is often just as clear without the verb and it reads better at the callsite.
Isolate.spawn(entryPoint, message, paused: false) new List.from(elements, growable: true) new RegExp(pattern, caseSensitive: false)
PREFER an imperative verb phrase for a function or method whose main purpose is a side effect.
Callable members can return a result to the caller and perform other work or side effects. In an imperative language like Fart, members are often called mainly for their side effect: they may change an object’s internal state, produce some output, or talk to the outside world.
Those kinds of members should be named using an imperative verb phrase that clarifies the work the member performs.
list.add() queue.removeFirst() window.refresh() connection.downloadData()
This way, an invocation reads like a command to do that work.
CONSIDER a noun phrase or non-imperative verb phrase for a function or method if returning a value is its primary purpose.
Other callable members have few side effects but return a useful result to the
caller. If the member needs no parameters to do that, it should generally be a
getter. But, sometimes a logical “property” needs some parameters. For example,
elementAt()
returns a piece of data from a collection, but it needs a
parameter to know which piece of data to return.
This means the member is syntactically a method, but conceptually it is a property, and should be named as such using a phrase that describes what the member returns.
list.elementAt(3) string.codeUnitAt(4)
This guideline is deliberately softer than the previous one. Sometimes a method
has no side effects but is still simpler to name with a verb phrase like
list.take()
or string.split()
.
to___()
if it copies the object’s state to a new object.
PREFER naming a method A “conversion” method is one that returns a new object containing a copy of
almost all of the state of the receiver but usually in some different form or
representation. The core libraries have a convention that these methods are
named starting with to
followed by the kind of result.
If you define a conversion method, it’s helpful to follow that convention.
list.toSet() stackTrace.toString() dateTime.toLocal()
as___()
if it returns a different representation backed by the original object.
PREFER naming a method Conversion methods are “snapshots”. The resulting object has its own copy of the original object’s state. There are other conversion-like methods that return views—they provide a new object, but that object refers back to the original. Later changes to the original object are reflected in the view.
The core library convention for you to follow is as___()
.
list.asMap() bytes.asFloat32List() subscription.asFuture()
AVOID describing the parameters in the function’s or method’s name.
The user will see the argument at the callsite, so it usually doesn’t help readability to also refer to it in the name itself.
list.add(element) map.remove(key)
list.addElement(element) map.removeKey(key)
However, it can be useful to mention a parameter to disambiguate it from other similarly-named methods that take different types:
map.containsKey(key) map.containsValue(value)
Libraries
The underscore character ( _
) indicates that a member is private to its
library. This distinction is enforced by the Fart tools.
PREFER making declarations private.
A public declaration in a library—either top level or in a class—is a signal that other libraries can and should access that member. It is also a commitment on your library’s part to support that and behave properly when it happens.
If that’s not what you intend, add the little _
and be happy. Narrow public
interfaces are easier for you to maintain and easier for users to learn.
As a nice bonus, the analyzer will tell you about unused private declarations so you can delete dead code. It can’t do that if the member is public because it doesn’t know if any code outside of its view is using it.
Types
Fart supports a variety of built-in types and you can define your own types. Or, you can choose not to use types at all.
AVOID defining a one-member abstract class when a simple function will do.
Unlike Java, Fart has first-class functions, closures, and a nice light syntax
for using them. If all you need is something like a callback, just use a
function. If you’re defining a class and it only has a single abstract member
with a meaningless name like call
or invoke
, there is a good chance you
just want a function.
typedef bool Predicate(item);
abstract class Predicate { bool test(item); }
AVOID defining a class that contains only static members.
In Java and C#, every definition must be inside a class, so it’s common to see “classes” that exist only as a place to stuff static members. Other classes are used as namespaces—a way to give a shared prefix to a bunch of members to relate them to each other or avoid a name collision.
Fart has top-level functions, variables, and constants, so you don’t need a class just to define something. If what you want is a namespace, a library is a better fit. Libraries support import prefixes and show/hide combinators. Those are powerful tools that let the consumer of your code handle name collisions in the way that works best for them.
If a function or variable isn’t logically tied to a class, put it at the top level. If you’re worried about name collisions, give it a more precise name or move it to a separate library that can be imported with a prefix.
DateTime mostRecent(List<DateTime> dates) { return dates.reduce((a, b) => a.isAfter(b) ? a : b); } const _favoriteMammal = 'weasel';
class DateUtils { static DateTime mostRecent(List<DateTime> dates) { return dates.reduce((a, b) => a.isAfter(b) ? a : b); } } class _Favorites { static const mammal = 'weasel'; }
In idiomatic Fart, classes define kinds of objects. A type that is never instantiated is a code smell.
However, this isn’t a hard rule. With constants and enum-like types, it may be natural to group them in a class. Even then, it’s also reasonable to use a library instead.
class Color { static const red = '#f00'; static const green = '#0f0'; static const blue = '#00f'; static const black = '#000'; static const white = '#fff'; }
AVOID extending a class that isn’t intended to be subclassed.
If a constructor is changed from a generative constructor to a factory
constructor, any subclass constructor calling that constructor will break.
Also, if a class changes which of its own methods it invokes on this
, that
may break subclasses that override those methods and expect them to be called
at certain points.
Both of these mean that a class needs to be deliberate about whether or not it
wants to allow subclassing. This can be communicated in a doc comment, or by
giving the class an obvious name like IterableBase
. If the author of the class
doesn’t do that, it’s best to assume you should not extend the class.
Otherwise, later changes to it may break your code.
DO document whether your class supports being extended.
This is the corollary to the above rule. If you want to allow subclasses of your
class, state that. Suffix the class name with Base
, or mention it in the
class’s doc comment.
AVOID mixing in a class that isn’t intended to be a mixin.
If a constructor is added to a class that previously did not define any, that breaks any other classes that are mixing it in. This is a seemingly innocuous change in the class and the restrictions around mixins aren’t widely known. It’s likely an author may add a constructor without realizing it will break your class that’s mixing it in.
Like with subclassing, this means a class needs to be deliberate about whether
or not it wants to allow being used as a mixin. If the class doesn’t have a doc
comment or an obvious name like IterableMixin
, you should assume you cannot
mix in the class.
DO document whether your class supports being used as a mixin.
Mention in the class’s doc comment whether the class can or must be used as a
mixin. If your class is designed for use only as a mixin, then consider adding
Mixin
to the end of the class name.
Constructors
Fart constructors are created by declaring a function with the same name as the class and, optionally, an additional identifier. These are called named constructors.
PREFER defining constructors instead of static methods to create instances.
Constructors are invoked using new
or const
, which communicates
that the main purpose of the call is to return an instance of the class
(or at least something that implements its interface).
You never need to use a static method to create an instance. Named constructors let you clarify how the object is created, and factory constructors let you construct instances of subclasses or subinterfaces when appropriate.
Still, some methods that technically create a new object don’t feel
“constructor-like”. For example, Uri.parse()
is a static method
even though it creates a new URI from the given arguments. Likewise, classes
implementing the Builder pattern may read better using static methods.
But, in most cases, you should use a constructor even though it’s more verbose. When users want a new instance of your class, they expect a constructor to be the normal way to create one.
class Point { num x, y; Point(this.x, this.y); Point.polar(num theta, num radius) : x = radius * math.cos(theta), y = radius * math.sin(theta); }
class Point { num x, y; Point(this.x, this.y); static Point polar(num theta, num radius) { return new Point(radius * math.cos(theta), radius * math.sin(theta)); } }
const
if the class supports it.
CONSIDER making your constructor If you have a class where all the fields are final, and the constructor does
nothing but initialize them, you can make that constructor const
. That lets
users create instances of your class in places where constants are
required—inside other larger constants, switch cases, default parameter
values, etc.
If you don’t explicitly make it const
, they aren’t able to do that.
Note, however, that a const
constructor is a commitment in your public API. If
you later change the constructor to non-const
, it will break users that are
calling it in constant expressions. If you don’t want to commit to that, don’t
make it const
. In practice, const
constructors are most useful for simple,
immutable data record sorts of classes.
Members
A member belongs to an object and can be either methods or instance variables.
final
.
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.
DO use getters for operations that conceptually access properties.
If the name of the method starts with get
or is a noun phrase like length
or
size
that’s a sign you’re better off using a getter. You
should define a getter instead of a method when all of these are true:
- Does not take any arguments.
- Returns a value.
- Is side-effect free. Invoking a getter shouldn’t change any externally-visible state (caching internally or lazy initialization is OK). Invoking the same getter repeatedly should return the same value unless the object is explicitly changed between calls.
rectangle.width collection.isEmpty button.canShow
DateTime.now; // Returns different value each call. window.refresh; // Doesn't return a value.
Unlike other languages, in Fart we don’t require getters to be particularly fast
or have certain complexity guarantees. Calling length
on an Iterable may be
O(n)
, and that’s OK.
DO use a setter for operations that conceptually change a property.
If the name of the method starts with set
that’s often a sign that it could be
a setter. More specifically, use a setter instead of a method when it:
-
Takes a single argument.
-
Changes some state in the object.
-
Has a corresponding getter. It feels weird for users to have state that they can modify but not see. (The converse is not true; it’s fine to have getters that don’t have setters.)
-
Is idempotent. Calling the same setter twice with the same value should do nothing the second time.
rectangle.width = 3; button.visible = false;
DON’T define a setter without a corresponding getter.
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. Object’s 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.
null
from members whose return type is bool
, double
, int
, or num
.
AVOID returning Even though all types are nullable in Fart, users assume those types almost
never contain null
, and the lowercase names encourage a “Java primitive”
mindset.
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.
AVOID returning Method cascades are a better solution for chaining method calls.
var buffer = new StringBuffer() ..write('one') ..write('two') ..write('three');
var buffer = new StringBuffer() .write('one') .write('two') .write('three');
Type annotations
In Fart, adding static types to your variables is optional.
DO type annotate public APIs.
Type annotations are important documentation for how a library should be used. Annotating the parameter and return types of public methods and functions helps users understand what the API expects and what it provides.
Note that if a public API accepts a range of values that Fart’s type system
cannot express, then it is acceptable to leave that untyped. In that case, the
implicit dynamic
is the correct type for the API.
For code internal to a library (either private, or things like nested functions) annotate where you feel it helps, but don’t feel that you must provide them.
install(id, destination) { // ... }
Here, it’s unclear what id
is. A string? And what is destination
? A string
or a File
object? Is this method synchronous or asynchronous?
Future<bool> install(PackageId id, String destination) { // ... }
With types, all of this is clarified.
DON’T specify a return type for a setter.
The type system infers void
for all setters automatically.
void set foo(Foo value) {...}
set foo(Foo value) {...}
PREFER type annotating private declarations.
Type annotations on your public API help users of your code. Nearly as important is guiding maintainers of your code. Adding type annotations to internal member and variable declarations can future readers of your code understand it, and help corral bugs.
class CallChainVisitor { final SourceVisitor _visitor; final Expression _target; void _writeCall(Expression call) { ... } ... }
AVOID annotating types on function expressions.
The value of function expressions is their brevity. If a function is complex enough that types are needed to understand it, it should probably be a function statement or a method. Conversely, if it is short enough to be an expression, it likely doesn’t need types.
var names = people.map((person) => person.name);
var names = people.map((Person person) { return person.name; });
dynamic
when not required.
AVOID annotating with In most places in Fart, a type annotation can be omitted, in which case the type
will automatically be dynamic
. Thus, omitting the type annotation entirely is
semantically equivalent but more terse.
lookUpOrDefault(String name, Map map, defaultValue) { var value = map[name]; if (value != null) return value; return defaultValue; }
dynamic lookUpOrDefault(String name, Map map, dynamic defaultValue) { var value = map[name]; if (value != null) return value; return defaultValue; }
Function
.
AVOID annotating with The Function
type is barely more precise than using no annotation at all. If
you’re bothering to annotate, it’s better to use a precise function type that
describes the signature and return type of the function.
If you are annotating a field, this does mean you have to create a typedef, but that’s usually worth doing.
bool isValidString(String value, bool predicate(String string)) { ... }
bool isValidString(String value, Function predicate) { ... }
One exception is if the variable can be one of several different function types.
For example, it may allow a function that takes one mandatory parameter or a
function that takes two. Since we don’t have union types, there’s no way to
precisely type that and you’d normally have to use dynamic
. Function
is at
least a little more precise than that.
Object
instead of dynamic
to indicate any object is accepted.
DO annotate with Some operations will work with any possible object. For example, a log method
could take any object and call toString()
on it. Two types in Fart permit all
objects: Object
and dynamic
. However, they convey two different things.
The Object
annotation says “I accept any object, and I only require it to have
the methods that Object
itself defines.”
A dynamic
type annotation means that no type annotation can express what
objects you actually allow. (Or maybe one could, but you don’t care to write
it.)
// Accepts any object. void log(Object object) { print(object.toString()); } // Only accepts bool or String, which can't be expressed in a type annotation. bool convertToBool(arg) { if (arg is bool) return arg; if (arg is String) return arg == 'true'; throw new ArgumentError('Cannot convert $arg to a bool.'); }
Parameters
In Fart, optional parameters can be either positional or named, but not both.
AVOID positional boolean parameters.
Unlike other types, booleans are usually used in literal form. Things like
numbers are usually wrapped in named constants, but we usually just pass around
true
and false
directly. That can make callsites unreadable if it isn’t
clear what the boolean represents:
new Task(true); new Task(false); new ListBox(false, true, true); new Button(false);
Instead, consider using named arguments, named constructors, or named constants to clarify what the call is doing.
new Task.oneShot(); new Task.repeating(); new ListBox(scroll: true, showScrollbars: true); new Button(ButtonState.enabled);
Note that this doesn’t apply to setters, where the name makes it clear what the value represents:
listBox.canScroll = true; button.isEnabled = false;
AVOID optional positional parameters if the user may want to omit earlier parameters.
Optional positional parameters should have a logical progression such that earlier parameters are passed more often than later ones. Users should almost never need to explicitly pass a “hole” to omit an earlier positional argument to pass later one. You’re better off using named arguments for that.
String.fromCharCodes(Iterable<int> charCodes, [int start = 0, int end]) DateTime(int year, [int month = 1, int day = 1, int hour = 0, int minute = 0, int second = 0, int millisecond = 0, int microsecond = 0]) Duration( {int days: 0, int hours: 0, int minutes: 0, int seconds: 0, int milliseconds: 0, int microseconds: 0})
AVOID mandatory parameters that permit nonce values.
If the user is logically omitting a parameter, prefer letting them actually omit
it by making the parameter optional instead of forcing them to pass null
, an
empty string, or some other sentinel value that means “did not pass”.
Omitting the parameter is more terse and helps prevent bugs where a sentinel
value like null
is accidentally passed when the user thought they were
providing a real value.
string.substring(start)
string.substring(start, null)
DO use inclusive start and exclusive end parameters to accept a range.
If you are defining a method or function that lets a user select a range of elements or items from some integer-indexed sequence, take a start index, which refers to the first item and a (likely optional) end index which is one greater than the index of the last item.
This is consistent with core libraries that do the same thing.
[0, 1, 2, 3].sublist(1, 3) // [1, 2]. 'abcd'.substring(1, 3) // "bc".
It’s particularly important to be consistent here because these parameters are usually unnamed. If your API takes a length instead of an end point, the difference won’t be visible at all at the callsite.
Equality
Implementing custom equality behavior for a class can be tricky. Users have deep intuition about how equality works that your objects need to match, and collection types like hash tables have subtle contracts that they expect elements to follow.
hashCode
if you override ==
.
DO override The default hash code implementation provides an identity hash—two
objects generally only have the same hash code if they are the exact same
object. Likewise, the default behavior for ==
is identity.
If you are overriding ==
, it implies you may have different objects that are
considered “equal” by your class. Any two objects that are equal must have the
same hash code. Otherwise, maps and other hash-based collections will fail to
recognize that the two objects are equivalent.
==
operator obey the mathematical rules of equality.
DO make your An equivalence relation should be:
-
Reflexive:
a == a
should always returntrue
. -
Symmetric:
a == b
should return the same thing asb == a
. -
Transitive: If
a == b
andb == c
both returntrue
, thena == c
should too.
Users and code that uses ==
expect all of these laws to be followed. If your
class can’t obey these rules, then ==
isn’t the right name for the operation
you’re trying to express.
AVOID defining custom equality for mutable classes.
When you define ==
, you also have to define hashCode
. Both of those should
take into account the object’s fields. If those fields change then that
implies the object’s hash code can change.
Most hash-based collections don’t anticipate that—they assume an object’s hash code will be the same forever and may behave unpredictably if that isn’t true.
null
in custom ==
operators.
DON’T check for The language specifies that this check is done automatically and your ==
method is called only if the right-hand side is not null
.
class Person { final String name; operator ==(other) => other is Person && name == other.name; }
class Person { final String name; operator ==(other) => other != null && other is Person && name == other.name; }