Classes and Mixins Design

Domains: Dart

DO

DO document if your class supports being extended.

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.

DO use mixin to define a mixin type.

Dart originally didn’t have a separate syntax for declaring a class intended to be mixed in to other classes. Instead, any class that met certain restrictions (no non-default constructor, no superclass, etc.) could be used as a mixin. This was confusing because the author of the class might not have intended it to be mixed in.

Dart 2.1.0 added a mixin keyword for explicitly declaring a mixin. Types created using that can only be used as mixins, and the language also ensures that your mixin stays within the restrictions. When defining a new type that you intend to be used as a mixin, use this syntax.

mixin ClickableMixin implements Control {
  bool _isDown = false;

  void click();

  void mouseDown() {
    _isDown = true;
  }

  void mouseUp() {
    if (_isDown) click();
    _isDown = false;
  }
}

You might still encounter older code using class to define mixins, but the new syntax is preferred.

DO document if your class supports being used as an interface.

If your class can be used as an interface, mention that in the class’s doc comment.

AVOID

AVOID defining a one-member abstract class when a simple function will do.

Unlike Java, Dart 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 Predicate<E> = bool Function(E element);
abstract class Predicate<E> {
  bool test(E element);
}

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.

Dart 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';

In idiomatic Dart, 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.

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.

AVOID implementing a class that isn’t intended to be an interface.

Implicit interfaces are a powerful tool in Dart to avoid having to repeat the contract of a class when it can be trivially inferred from the signatures of an implementation of that contract.

But implementing a class’s interface is a very tight coupling to that class. It means virtually any change to the class whose interface you are implementing will break your implementation. For example, adding a new member to a class is usually a safe, non-breaking change. But if you are implementing that class’s interface, now your class has a static error because it lacks an implementation of that new method.

Library maintainers need the ability to evolve existing classes without breaking users. If you treat every class like it exposes an interface that users are free to implement, then changing those classes becomes very difficult. That difficulty in turn means the libraries you rely on are slower to grow and adapt to new needs.

To give the authors of the classes you use more leeway, avoid implementing implicit interfaces except for classes that are clearly intended to be implemented. Otherwise, you may introduce a coupling that the author doesn’t intend, and they may break your code without realizing it.

AVOID mixing in a type that isn’t intended to be a mixin.

For compatibility, Dart still allows you to mix in classes that aren’t defined using mixin. However, that’s risky. If the author of the class doesn’t intend the class to be used as a mixin, they might change the class in a way that breaks the mixin restrictions. For example, if they add a constructor, your class will break.

If the class doesn’t have a doc comment or an obvious name like IterableMixin, assume you cannot mix in the class if it isn’t declared using mixin.

Similar pages

Page structure
Terms

Dart

Classes

Functions

Constructor

Methods

Types

bool

Variables

List

Factory constructor