Sealed Classes In Dart

Sealed classes in Dart offer a way to represent restricted class hierarchies where all subclasses are known and fixed at compile time. This concept ensures that a class can only be extended by a fixed set of classes defined within the same file, providing a structured approach to modeling data types.

What are Sealed Classes in Dart?

Sealed classes in Dart restrict the inheritance hierarchy to a predefined set of classes. This means that all subclasses must be known and explicitly defined within the same file where the sealed class is declared. Sealed classes are commonly used to define a closed set of related types, allowing for exhaustive pattern matching and improved type safety.

History/Background

Sealed classes were not directly supported in the Dart language until the introduction of Dart 2.12 in February 2021. Prior to this update, developers had to rely on workarounds or external libraries to achieve similar functionality. The addition of sealed classes aimed to enhance the language's expressiveness and provide a more robust way to model data types with restricted inheritance hierarchies.

Syntax

The syntax for defining a sealed class in Dart involves using the sealed keyword followed by the class keyword. Here's a basic template:

Example

sealed class MySealedClass {
  // define subclasses here
}

class Subclass1 extends MySealedClass {
  // subclass implementation
}

class Subclass2 extends MySealedClass {
  // subclass implementation
}

In this syntax:

  • sealed keyword is used to indicate that the class is sealed.
  • MySealedClass is the sealed class that restricts its subclasses.
  • Subclass1, Subclass2, etc., are subclasses that extend MySealedClass.
  • Key Features

  • Restricts the inheritance hierarchy to a fixed set of known subclasses.
  • Enables exhaustive pattern matching for improved type safety.
  • Encourages a structured approach to defining related types within the same file.
  • Enhances code readability and maintainability by explicitly declaring all possible subclasses.
  • Example 1: Basic Usage

    Example
    
    sealed class Result {
      const Result();
    }
    
    class Success extends Result {
      final String message;
      
      const Success(this.message);
    }
    
    class Failure extends Result {
      final String errorMessage;
    
      const Failure(this.errorMessage);
    }
    
    void main() {
      final result = Success('Operation successful');
      
      if (result is Success) {
        print('Success: ${(result as Success).message}');
      } else if (result is Failure) {
        print('Failure: ${(result as Failure).errorMessage}');
      }
    }
    

Output:

Output

Success: Operation successful

Example 2: Pattern Matching

Example

sealed class Shape {
  const Shape();
}

class Circle extends Shape {
  final double radius;

  const Circle(this.radius);
}

class Rectangle extends Shape {
  final double width;
  final double height;

  const Rectangle(this.width, this.height);
}

String describeShape(Shape shape) {
  return shape.when(
    (Circle circle) => 'Circle with radius ${circle.radius}',
    (Rectangle rectangle) => 'Rectangle with dimensions ${rectangle.width}x${rectangle.height}',
  );
}

void main() {
  final shape = Circle(5.0);
  print(describeShape(shape));
}

Output:

Output

Circle with radius 5.0

Common Mistakes to Avoid

1. Ignoring Sealed Class Limitations

Problem: Many beginners believe that any class can be declared as a sealed class without understanding the implications of sealed classes, such as restricting subclassing to specific classes only.

Example

// BAD - Don't do this
sealed class Animal {}

class Dog extends Animal {}
class Cat extends Animal {}

Solution:

Example

// GOOD - Do this instead
sealed class Animal {}

class Dog extends Animal {}
class Cat extends Animal {}

Why: The mistake here is that you might think you can freely extend a sealed class from anywhere, but you must strictly adhere to subclassing rules within the same library. Sealed classes are meant to restrict subclassing to a specific set of classes, and if you try to extend them from another library, you will encounter errors. Always ensure to define your subclasses within the same file or library.

2. Forgetting to Implement All Subclasses

Problem: Beginners often forget to implement all subclasses when using sealed classes, which can lead to runtime exceptions when pattern matching.

Example

// BAD - Don't do this
sealed class Shape {
  double area();
}

class Circle extends Shape {
  final double radius;
  Circle(this.radius);
  
  @override
  double area() => 3.14 * radius * radius;
}

// Missing implementation for Rectangle

Solution:

Example

// GOOD - Do this instead
sealed class Shape {
  double area();
}

class Circle extends Shape {
  final double radius;
  Circle(this.radius);
  
  @override
  double area() => 3.14 * radius * radius;
}

class Rectangle extends Shape {
  final double width, height;
  Rectangle(this.width, this.height);
  
  @override
  double area() => width * height;
}

Why: Failing to implement all subclasses can lead to unhandled cases during pattern matching or when calling methods. This will cause runtime exceptions. Always ensure that every potential subclass of a sealed class is implemented to maintain code integrity.

3. Misunderstanding the Purpose of Sealed Classes

Problem: Some beginners use sealed classes without understanding their purpose, thinking they can just use them for any class design.

Example

// BAD - Don't do this
sealed class Configuration {
  String getConfig();
}

class DefaultConfig extends Configuration {
  @override
  String getConfig() => 'Default';
}

class CustomConfig extends Configuration {
  @override
  String getConfig() => 'Custom';
}

Solution:

Example

// GOOD - Do this instead
sealed class Configuration {
  String getConfig();
}

class DefaultConfig extends Configuration {
  @override
  String getConfig() => 'Default';
}

class CustomConfig extends Configuration {
  @override
  String getConfig() => 'Custom';
}

Why: The mistake lies in using sealed classes without understanding that they are primarily designed for a limited set of known subclasses, enhancing type safety and exhaustive checks. Use sealed classes when you have a finite number of subclasses that logically fit under a parent class.

4. Not Using `when` for Pattern Matching

Problem: A frequent mistake is failing to implement the when method for pattern matching on sealed classes.

Example

// BAD - Don't do this
void handleShape(Shape shape) {
  if (shape is Circle) {
    print('Circle with area: ${shape.area()}');
  }
  // Missing handling for Rectangle
}

Solution:

Example

// GOOD - Do this instead
void handleShape(Shape shape) {
  shape.when(
    circle: (circle) => print('Circle with area: ${circle.area()}'),
    rectangle: (rectangle) => print('Rectangle with area: ${rectangle.area()}'),
  );
}

Why: By not implementing pattern matching using when, you risk missing out on type safety and exhaustive checking. Sealed classes are meant to be used with pattern matching to ensure every subclass is accounted for. Always use the when method to handle subclasses cleanly and safely.

5. Overusing Sealed Classes

Problem: Beginners often overuse sealed classes for scenarios where they are not needed, complicating the code unnecessarily.

Example

// BAD - Don't do this
sealed class UserRole {
  String getRole();
}

class Admin extends UserRole {
  @override
  String getRole() => 'Admin';
}

class Guest extends UserRole {
  @override
  String getRole() => 'Guest';
}

// Simple enumeration would suffice

Solution:

Example

// GOOD - Do this instead
enum UserRole {
  admin,
  guest,
}

// Use it like this
void printRole(UserRole role) {
  switch (role) {
    case UserRole.admin:
      print('Role is Admin');
      break;
    case UserRole.guest:
      print('Role is Guest');
      break;
  }
}

Why: Sealed classes are best suited for complex hierarchies and behaviors. In cases where you only need a simple set of constants, using an enumeration is more appropriate. Avoid overcomplicating your design by using sealed classes where simpler structures will suffice.

Best Practices

1. Define a Finite Set of Subclasses

When using sealed classes, ensure that you define a clear and finite set of subclasses. This practice maintains type safety and ensures that all possible cases can be handled exhaustively.

2. Use Pattern Matching Effectively

Take advantage of Dart's pattern matching capabilities with sealed classes. Use the when method to destructure and handle different subclasses clearly and safely, ensuring all cases are considered.

3. Keep Sealed Classes Simple

Limit the complexity of your sealed classes. They should focus on a single responsibility or domain. Avoid adding unnecessary methods that could make them harder to maintain.

4. Document Your Sealed Classes

Provide clear documentation for your sealed classes and their intended use. This will help other developers (or future you) understand the reasoning behind the design and how to use the classes correctly.

5. Use Sealed Classes for State Management

Consider using sealed classes for managing state in your applications, especially in scenarios like state machines or managing UI states. This can make your code more maintainable and clear.

6. Avoid Deep Inheritance Trees

While sealed classes can allow for inheritance, avoid creating deep inheritance trees. Opt for composition over inheritance when appropriate to keep your code modular and easier to understand.

Key Points

Point Description
Sealed classes restrict subclassing to a predefined set of classes within the same library, enhancing type safety.
Pattern matching with sealed classes using the when method ensures that all subclasses are handled, preventing runtime exceptions.
Use sealed classes primarily when you have a finite number of subclasses to represent a specific domain or behavior.
Avoid overusing sealed classes for simple cases where enums or other simpler structures would suffice.
Document your sealed classes to clarify their purpose and usage to other developers.
Keep sealed classes focused on single responsibilities to promote maintainability and clarity.
Use sealed classes in state management scenarios for clean and maintainable code patterns.

Input Required

This code uses input(). Please provide values below: