Question 1
Dart documentation says:
Cascades (.., ?..) allow you to make a sequence of operations on the same object. In addition to accessing instance members, you can also call instance methods on that same object. This often saves you the step of creating a temporary variable and allows you to write more fluid code.
Consider the following code:
class Car {
String? color;
int speed = 0;
void accelerate() {
speed += 10;
}
void brake() {
speed -= 10;
}
void repaint(String newColor) {
color = newColor;
}
@override
String toString() {
return "Car(color: $color, speed: $speed)";
}
}
void main() {
Car myCar = Car()
..color = 'red'
..speed = 100
..accelerate()
..repaint('blue');
print(myCar); // Output: Car(color: blue, speed: 110)
}
The constructor, Car()
, returns a Car
object. The code that follows the cascade notation operates on this object, ignoring any values that might be returned. Here, myCar
is created and then immediately modified using several methods and property setters, all thanks to the cascade notation (..). This allows for a more compact and chained setting of properties and calling of methods on the myCar
object.
The previous example is equivalent to this code:
class Car {
String? color;
int speed = 0;
void accelerate() {
speed += 10;
}
void brake() {
speed -= 10;
}
void repaint(String newColor) {
color = newColor;
}
@override
String toString() {
return "Car(color: $color, speed: $speed)";
}
}
void main() {
Car myCar = Car();
myCar.color = 'red';
myCar.speed = 100;
myCar.accelerate();
myCar.repaint('blue');
print(myCar); // Output: Car(color: blue, speed: 110)
}
Here are the differences:
- Car myCar = Car()
- ..color = 'red'
- ..speed = 100
- ..accelerate()
- ..repaint('blue');
+ Car myCar = Car();
+ myCar.color = 'red';
+ myCar.speed = 100;
+ myCar.accelerate();
+ myCar.repaint('blue');
This reminds me of Fluent Interface after fork clean-code-php repo a while ago.
A Fluent interface is an object-oriented API that aims to improve the readability of the source code by using Method chaining.
So I asked ChatGPT for direction.
Is the concept of fluent interface the same as cascade notation in Dart?
ChatGPT Reply 1
Both the concept of a fluent interface and Dart's cascade notation (
..
) aim to make code more readable and convenient by allowing method calls or operations to be chained together. However, they are not exactly the same thing.
Fluent Interface
In a fluent interface, each method returns an instance of the object, allowing you to chain method calls together. A fluent API is designed specifically to return this
or a new instance of the same class, making it possible to chain methods.
Here's an example using a hypothetical Car
class:
class Car {
String? color;
int speed = 0;
Car accelerate() {
speed += 10;
return this;
}
Car brake() {
speed -= 10;
return this;
}
Car repaint(String newColor) {
color = newColor;
return this;
}
@override
String toString() {
return 'Car(color: $color, speed: $speed)';
}
}
void main() {
Car myCar = Car()
.accelerate()
.brake()
.repaint('blue');
print(myCar); // Output: Car(color: blue, speed: 0)
}
Cascade Notation
Dart's cascade notation allows you to perform multiple operations on a single object without the need for the methods to return this
. When you use the cascade notation, Dart knows to use the same object for each subsequent operation.
void main() {
Car myCar = Car()
..accelerate()
..brake()
..repaint('blue');
print(myCar); // Output: Car(color: blue, speed: 0)
}
Summary
- A fluent interface is a design pattern where the methods are designed to be chained together.
- Cascade notation is a language feature in Dart that allows you to chain multiple operations on an object, whether or not the methods are designed to be chained together.
Both achieve a similar goal but do it in slightly different ways. Fluent interfaces are more common in languages that don't have an equivalent to Dart's cascade notation.
Intermezzo
That's a good answer actually. So I moved on and read the next line from the Dart documentation.
If the object that the cascade operates on can be null, then use a null-shorting cascade (?..) for the first operation. Starting with ?.. guarantees that none of the cascade operations are attempted on that null object.
I'm using my own code to understand this (probably a poor example but I get what the docs trying to say)
void main() {
var myCar;
myCar
?..color = 'red'
..speed = 100
..accelerate()
..repaint('blue');
print(myCar); // Output: null
}
The previous code is equivalent to the following:
void main() {
var myCar;
myCar?.color = 'red';
myCar?.speed = 100;
myCar?.accelerate();
myCar?.repaint('blue');
print(myCar); // Output: null
}
Here are the differences:
var myCar;
- myCar
- ?..color = 'red'
- ..speed = 100
- ..accelerate()
- ..repaint('blue');
+ myCar?.color = 'red';
+ myCar?.speed = 100;
+ myCar?.accelerate();
+ myCar?.repaint('blue');
Question 2
The Dart documentation says:
Be careful to construct your cascade on a function that returns an actual object. For example, the following code fails:
var sb = StringBuffer();
sb.write('foo')
..write('bar'); // Error: method 'write' isn't defined for 'void'.
I don't really get it since my all methods are void indeed. So I asked ChatGPT again.
What do the docs mean by the void? Meanwhile, the previous snippet of code worked just fine with the void function.
ChatGPT Reply 2
The documentation is warning you that you cannot use cascade notation on a function or method that returns
void
. This is because the cascade notation works by treating the value returned by each cascaded operation as the object to perform the next operation on. In the case of methods that returnvoid
, there is no value to cascade onto.
However, you'll notice that your Dart example works because you're not actually cascading on the return value of the void
methods (accelerate
, brake
, repaint
). You're cascading on the original Car
object (myCar
) which the cascade notation keeps "in focus" throughout the chained operations. In this context, void
methods are permitted because they're not breaking the cascade; the cascade remains rooted in myCar
.
For example, this would be problematic:
void main() {
// This is an attempt to cascade on the result of accelerate(),
// but that's void, so this will result in a syntax error.
var myCar = Car().accelerate()..brake(); // Error
}
But in your working example:
Car myCar = Car()
..color = 'red'
..speed = 100
..accelerate()
..repaint('blue');
You are setting the initial object for the cascade to Car()
, and the cascade continues to operate on that object, not on the return values of the void
methods.
So, in summary, you can include void
methods within a cascade, as long as you're still rooted in an object that isn't void
.
Top comments (0)