skip to content

In Dart, when should you share behaviour across several API client classes with a mixin rather than `extends` or an extension method?

level: middleimportance: should knowfreq 45%

answer

  1. one superclass, many mixins
  2. state and overrides need a mixin
  3. extensions resolve statically
  4. is-a versus can-do
  5. no dynamic dispatch for extensions

basics

~20 s

Use a mixin when unrelated clients need the same members, possibly with state or overrides, without sharing a superclass. Use extends for a true single is-a hierarchy, and an extension method for stateless helpers on a type you cannot or should not change.

solid answer

~40 s

`extends` gives one superclass with constructors and is right when every client genuinely *is* a `BaseApi`; you only get one. A mixin adds members, including fields and overrides, to any number of classes regardless of their superclass, and the class becomes a subtype of it, so it suits cross-cutting behaviour like logging or retry. An extension method adds a statically resolved member to an existing type: it cannot hold instance state, cannot override anything, is not part of the type's interface and does not work on `dynamic` receivers, so it fits convenience helpers such as formatting a response. Rule of thumb: need state, overriding or an `is` check, use a mixin; need a helper on a type you do not own, use an extension.

code

dart · 35 lines
dart
abstract class BaseApi {
  BaseApi(this.baseUrl);
  final String baseUrl;
  Future<String> send(String path);
}

mixin RequestCounter on BaseApi {
  int requests = 0; // state: only a mixin or class can hold it

  @override
  Future<String> send(String path) {
    requests++;
    return super.send(path);
  }
}

extension ResponseSummary on String {
  // stateless helper, resolved statically
  String get summary => length > 40 ? '${substring(0, 40)}...' : this;
}

class HttpUserApi extends BaseApi {
  HttpUserApi() : super('https://api.example.com');
  @override
  Future<String> send(String path) async => 'body of $baseUrl$path';
}

class UserApi extends HttpUserApi with RequestCounter {}

Future<void> main() async {
  final api = UserApi();
  final body = await api.send('/users');
  print(body.summary);
  print(api.requests); // 1
}

go deeper

for a junior

Recall one superclass per class, many mixins per class, and that extension methods add helpers to types you do not own.

for a middle

Explain the differences in state, overriding, dispatch and type membership, and map each API-client concern to the right mechanism.

for a senior

Avoid god base classes and misuse of extensions; justify each mechanism in review and know when delegation beats a mixin for testability.

for a principal

Set team conventions on inheritance, mixins, extensions and delegation for shared client code so behaviour stays discoverable and swappable.

## Three ways to share behaviour Suppose a Flutter app has `UserApi`, `OrderApi` and `PaymentApi`. They should all log requests, all retry transient failures, and all offer a convenience for turning a response body into a short summary. Dart offers three mechanisms, and each fits a different part of that list. - **`extends`** — single inheritance from one superclass. - **`with` a mixin** — members layered onto the class, any number of times. - **An extension method** — a member added to an existing type and resolved at compile time. ## `extends`: one hierarchy A class has exactly **one superclass**. Extending `BaseApi` gives constructors (via `super(...)`), fields, concrete methods and overridable behaviour. It is the right tool when every client truly *is a* `BaseApi` — shares a base URL, an HTTP client, an authentication header. Its limit is the word *one*. If `PaymentApi` must extend a vendor-provided base class, it cannot also extend yours. Stuffing every concern into `BaseApi` produces a god class that every client inherits whether it wants logging or not. ## Mixins: many independent capabilities A **mixin** adds a bundle of members to any class that lists it after `with`, regardless of that class's superclass: ```dart class PaymentApi extends VendorClient with Logging, Retry {} ``` What a mixin can do that the others cannot combine: 1. **Hold instance state** — a request counter, a list of recent log lines. 2. **Override** members of the class it is applied to, and, with an `on` clause, **wrap** the inherited implementation through `super`. 3. **Participate in the type**: `PaymentApi() is Logging` is `true`, so a diagnostics screen can accept any `Logging` client. 4. **Stack**: several mixins apply in a defined linear order. What it cannot do: be constructed, declare a constructor, or declare an `extends` clause. It also adds members to the class's API surface, which every caller then sees. ## Extension methods: static helpers An **extension** adds members to an existing type without touching it: ```dart extension ResponseSummary on String { String get summary => length > 40 ? '${substring(0, 40)}...' : this; } ``` dart.dev states the key property: extension methods are **resolved against the static type** of the receiver, which also makes them as fast as calling a static function. Consequences: - They **cannot declare instance fields**, so they cannot carry per-object state. - They **cannot override** anything; a real member of the same name always wins. - They are **not part of the interface**: `x is ResponseSummary` is meaningless, and the type does not change. - They **do not work on `dynamic`** receivers. The mechanics of extensions — naming, conflicts, extension types — belong to the extensions topic; here the point is only where they fit. ## Side by side | Question | `extends` | mixin (`with`) | extension method | |---|---|---|---| | How many per class? | one | many | any number in scope | | Instance state? | yes | yes | no | | Can override members? | yes | yes | no | | Changes the type (`is`)? | yes | yes | no | | Needs to own the target class? | yes | yes | no | | Dispatch | dynamic | dynamic | static | | Constructors? | yes | no | no | ## Choosing for the API clients - **Shared base URL and HTTP client** that every client is built around → `extends BaseApi`. - **Logging and retry**, which need state (attempt counts) and must wrap `send` → mixins, with `on BaseApi` where they call `super`. - **Summarising a response string**, a stateless helper on a type you do not own → an extension on `String`. A common mistake is reaching for extensions to add retry: an extension cannot intercept existing calls to `send`, because it cannot override. Another is turning every concern into a superclass, which fails as soon as a client needs a different base. ## A note on composition Mixins are not the only alternative to inheritance. Passing a `RetryPolicy` object into each client (**delegation**) avoids ordering puzzles and is easy to fake in tests. Mixins win when the behaviour needs to be part of the class's own type and override its methods; delegation wins when the behaviour should be swappable at runtime.

  • In Dart, why can't an extension method add retry behaviour to an existing send method?
    An extension cannot override: if the type already has `send`, calls to `send` always hit the real member, and the extension is ignored for that name. It also cannot store an attempt counter per object. Retry that wraps `send` needs a mixin with an `on` clause, or a wrapping object.
  • In Dart, when is plain delegation better than a mixin for retry logic?
    When the policy must vary at runtime or be replaced in tests. Injecting a `RetryPolicy` object keeps the client's type unchanged, avoids with-order puzzles, and lets a test pass a no-retry policy. A mixin fixes the behaviour into the class at compile time.

saying these in an interview costs you the question

  • Extension methods can override a class's existing methods.
  • An extension can keep per-object state in an instance field.
  • A class can extend several base classes if they are abstract.
  • Mixins are just syntax sugar for extension methods.
  • Extension methods are dispatched on the runtime type.