DEV Community

Retiago Drago
Retiago Drago

Posted on

Cascade Notation and Fluent Interface in Dart

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)
}
Enter fullscreen mode Exit fullscreen mode

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)
}
Enter fullscreen mode Exit fullscreen mode

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');
Enter fullscreen mode Exit fullscreen mode

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)
}
Enter fullscreen mode Exit fullscreen mode

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)
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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');
Enter fullscreen mode Exit fullscreen mode

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'.
Enter fullscreen mode Exit fullscreen mode

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 return void, 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
}
Enter fullscreen mode Exit fullscreen mode

But in your working example:

Car myCar = Car()
  ..color = 'red'
  ..speed = 100
  ..accelerate()
  ..repaint('blue');
Enter fullscreen mode Exit fullscreen mode

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)