In Dart, when should you share behaviour across several API client classes with a mixin rather than `extends` or an extension method?
answer
- one superclass, many mixins
- state and overrides need a mixin
- extensions resolve statically
- is-a versus can-do
- no dynamic dispatch for extensions
basics
~20 sUse 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 linesabstract 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
Recall one superclass per class, many mixins per class, and that extension methods add helpers to types you do not own.
Explain the differences in state, overriding, dispatch and type membership, and map each API-client concern to the right mechanism.
Avoid god base classes and misuse of extensions; justify each mechanism in review and know when delegation beats a mixin for testability.
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.