Exploring Dart 3.0's New Class Modifiers with Real-World Examples

Exploring Dart 3.0's New Class Modifiers with Real-World Examples

Dart 3.0 introduces a set of powerful class modifiers that provide developers with more control over inheritance, implementation, and type hierarchy. These modifiers, namely base, interface, final, and sealed, complement the existing modifiers abstract and mixin to offer distinct guarantees and restrictions when applied to classes or mixins. In this article, we will delve into these modifiers using real-world object examples, showcasing their practical applications and benefits.

The base Modifier

The base modifier ensures the enforcement of inheritance and implementation within a class or mixin's own library. It offers several guarantees such as:

  • The base class constructor is invoked whenever an instance of a subtype is created.

  • All private members implemented in the base class are inherited by subtypes.

  • The addition of newly implemented members in the base class does not break existing subtypes unless a subtype already declares a member with the same name and incompatible signature.

To illustrate this, let's consider the example of a Vehicle class defined in the library base_vehicle.dart. The base modifier is applied to ensure that the Vehicle class is extended within its own library, disallowing implementation outside of it.

// Library base_vehicle.dart
base class Vehicle {
  void moveForward(int meters) {
    // ...
  }
}

// Library test.dart
import 'base_vehicle.dart';

// Example usage
Vehicle myVehicle = Vehicle(); // Can be constructed

base class Car extends Vehicle {
  int passengers = 4;
  // ...
}

// ERROR: Cannot be implemented
base class Truck implements Vehicle {
  @override
  void moveForward() {
    // ...
  }
}

The interface Modifier

With the interface modifier, developers can define interfaces in Dart. These interfaces can be implemented by libraries outside their defining library but cannot be extended. The key guarantees provided by the interface modifier are:

  • Instance methods within the interface always invoke known implementations from the same library.

  • Other libraries cannot override methods in a way that could cause unexpected behaviour within the interface class.

Let's consider an example where a Vehicle interface is defined in vehicle_interface.dart and implemented in another library, test.dart.

// Library vehicle_interface.dart
interface class Vehicle {
  void moveForward(int meters) {
    // ...
  }
}

// Library test.dart
import 'vehicle_interface.dart';

// Example usage
Vehicle myVehicle = Vehicle(); // Can be constructed

// ERROR: Cannot be inherited
class Car extends Vehicle {
  int passengers = 4;
  // ...
}

// Can be implemented
class Truck implements Vehicle {
  @override
  void moveForward(int meters) {
    // ...
  }
}

The final Modifier

The final modifier closes the type hierarchy, preventing subtyping from external classes. It disallows both inheritance and implementation, ensuring that the API remains stable and instance methods are not overwritten by third-party subclasses. However, final classes can still be extended or implemented within the same library. It is worth noting that the final modifier encompasses the effects of the base modifier, requiring any subclasses to be marked as base, final, or sealed.

Consider the following example where a final Vehicle class is defined in final_vehicle.dart, and its usage is demonstrated in test.dart:

// Library final_vehicle.dart
final class Vehicle {
  void moveForward(int meters) {
    // ...
  }
}

// Library test.dart
import 'final_vehicle.dart';

// Example usage
Vehicle myVehicle = Vehicle(); // Can be constructed

// ERROR: Cannot be inherited
class Car extends Vehicle {
  int passengers = 4;
  // ...
}

class Truck implements Vehicle {
  // ERROR: Cannot be implemented
  @override
  void moveForward(int meters) {
    // ...
  }
}

The sealed Modifier

The sealed modifier allows developers to create a known and enumerable set of subtypes, ensuring exhaustive handling through switch statements. Sealed classes cannot be extended or implemented outside their defining library but can have factory constructors and define constructors for their subclasses. Subclasses of sealed classes, however, are not implicitly abstract.

Let's explore a practical example of a sealed Vehicle class with several subclasses:

sealed class Vehicle {}

class Car extends Vehicle {}

class Truck implements Vehicle {}

class Bicycle extends Vehicle {}

// ERROR: Cannot be instantiated
Vehicle myVehicle = Vehicle();

// Subclasses can be instantiated
Vehicle myCar = Car();

String getVehicleSound(Vehicle vehicle) {
  // ERROR: The switch is missing the Bicycle subtype or a default case.
  return switch (vehicle) {
    Car() => 'vroom',
    Truck() => 'VROOOOMM',
  };
}

What have we learned?

Dart 3.0's new class modifiers, including base, interface, final, and sealed, will empower developers to design more robust and maintainable code. By using real-world examples, we have demonstrated how these modifiers offer practical solutions for enforcing inheritance, implementing interfaces, closing the type hierarchy, and ensuring exhaustive handling of subtypes. Incorporating these modifiers into your Dart projects will enhance code clarity, prevent unexpected behaviours, and contribute to overall code quality and reliability.

Other information

I used another free credit at STOCKIMG.AI to generate a horizontal poster using disco diffusion for the article poster image. This time I used the image from my second post as the primer, with some words about class modifiers, inheritance, type hierarchy and interfaces. The image above is the generated result. Again, I was expecting something similar to the second image and the result didn't disappoint. With all three articles being in the same vein, I decided to use it.