When designing a public Dart API such as a formatting helper, how do you choose between named and optional positional parameters with future changes in mind?
answer
- what does true mean here
- skipping an earlier option
- callers versus overriders
- reorder, rename, new default
- positional subject, named options
basics
~20 sIn a public Dart API, keep the obvious subject positional and make options named with constant defaults: calls stay readable, booleans explain themselves, and new named options break no caller, though overriders of a method must still add them.
solid answer
~40 sI keep the subject of the call positional, `formatAmount(amount, ...)`, and make every option named with a constant default. Effective Dart backs this: avoid positional booleans, and avoid optional positional parameters when callers might skip earlier ones. Named options also evolve better: adding one breaks no call site, their order never matters, and swapped same-typed arguments cannot slip through. The catch is overriding: an override must accept every parameter of the member it overrides, so adding even an optional parameter to an interface method breaks implementers and fakes. Renaming a named parameter or reordering positional ones is breaking, and changing a default compiles fine but silently changes behaviour, so it belongs in the release notes.
code
dart · 28 linesabstract interface class AmountFormatter {
String format(num amount, {int decimals = 2});
}
class PlainFormatter implements AmountFormatter {
@override
String format(num amount, {int decimals = 2}) =>
amount.toStringAsFixed(decimals);
}
// Adding {bool showSign = false} to AmountFormatter.format keeps every
// caller compiling, but PlainFormatter.format no longer overrides it
// validly until it declares showSign as well.
String formatAmount(
num amount, {
String currency = 'EUR',
int decimals = 2,
bool showSign = false,
}) {
final sign = showSign && amount > 0 ? '+' : '';
return '$sign${amount.toStringAsFixed(decimals)} $currency';
}
void main() {
print(formatAmount(5, showSign: true)); // +5.00 EUR
print(PlainFormatter().format(3.14159, decimals: 3)); // 3.142
}go deeper
Recall that named parameters make boolean and numeric options readable at the call site and can be skipped individually.
Explain Effective Dart's guidance: no positional booleans, no optional positional options that callers may want to skip.
Reason about evolution: which changes break callers, which break overriders and fakes, and why a changed default is a silent behaviour change.
Set versioning policy for shared packages: which parameter changes count as breaking, and when to prefer a new top-level function over growing an interface method.
## Why this is a senior question Choosing parameter kinds is easy for one call site and hard for an API that other code depends on. The questions an interviewer probes are readability at call sites and **what changes later will break** callers or implementers. ## Readability rules from Effective Dart - **Avoid positional boolean parameters.** `format(price, true, false)` tells the reader nothing; `format(price, showSign: true, grouping: false)` does. The `avoid_positional_boolean_parameters` lint encodes it (not in the default sets). - **Avoid optional positional parameters if users may want to omit earlier ones.** If a caller would have to pass a placeholder for `currency` just to set `decimals`, those options should be named. - **Avoid mandatory parameters that accept a "no argument" value.** If callers pass `null` or `''` to mean "not given", make the parameter optional instead. - **Positional still wins for the obvious subject** of the call: the amount in `formatPrice(amount, ...)`, the list in `sortBy(list, ...)`. ## How each kind evolves | Change to a public function | Existing callers | Existing overrides / implementers | |---|---|---| | Add an optional **named** parameter | still compile | must add the parameter to their override | | Add an optional **positional** parameter at the end | still compile | must add it too | | Reorder optional positional parameters | still compile if the types match, passing values to the wrong slots | still compile if the types match, with swapped meaning | | Rename a named parameter | break at compile time | break | | Add a required parameter of any kind | break | break | | Change a default value | compile, but behaviour changes | unaffected | Two consequences follow: 1. **Named parameters are the safer growth path for callers.** New options can be added without touching existing calls, and their order never matters. 2. **Methods that others override or implement are stricter.** An override must accept every parameter the overridden member declares, so adding even an optional parameter to a method in an interface or base class breaks every class that implements or overrides it. A top-level function has no implementers, which makes it easier to evolve than an instance method on a widely implemented type. ## A worked design: a formatting helper ```dart String formatAmount( num amount, { String currency = 'EUR', int decimals = 2, bool showSign = false, bool grouping = true, }) { ... } ``` - The subject, `amount`, is positional. - Every option is named with a constant default, so callers write only what differs. - Adding `String? locale` next release breaks no caller. - If the defaults ever need to come from runtime configuration, a nullable parameter with `??` keeps them overridable per call. ## Traps to name in review 1. **Positional options with the same type**: swapping `(width, height)` or `(decimals, precision)` compiles and is wrong. Named parameters make such mistakes visible. 2. **A default change is a behaviour change**: callers who relied on the old default see different output without any compile error, so treat it like an API change in release notes. 3. **Implementers are callers too**: before adding a parameter to an abstract method, check who implements it, including test fakes. 4. **Flutter widgets** follow the same logic: constructors use named parameters, and new optional ones can be added in a minor release without breaking callers. ## What an interviewer listens for - The readability argument, with the boolean example. - The asymmetry between callers and overriders when a parameter is added. - That reordering or renaming is breaking, and that a changed default is a silent behaviour change. - A concrete signature with a positional subject and named options.
- Why does adding an optional named parameter to an abstract Dart method break implementing classes?An override must accept at least every parameter the overridden member declares, so that any call valid on the interface is valid on the implementation. Once the interface gains `{bool showSign = false}`, an implementation without it is no longer a valid override and fails to compile, including test fakes. Callers are unaffected, which is the asymmetry to plan for.
- Is changing a Dart parameter's default value a breaking change?Not at compile time: every call still compiles. But callers that omitted the argument now get different behaviour, which can be worse than a compile error because nothing flags it. Treat a changed default as a behaviour change, call it out in the changelog, and consider whether a new named option would be safer.
saying these in an interview costs you the question
- Uses positional booleans like format(x, true, false)
- Thinks adding an optional parameter can never break anything
- Believes changing a default value is always safe
- Makes every parameter positional for brevity
- Ignores implementers and test fakes when changing an interface