🎯Modificadores de Classes no Dart

O que são Modificadores de Classe em Dart 3

Os modificadores de classe em Dart 3 controlam como uma classe ou mixin pode ser usada dentro de sua própria biblioteca ou fora dela. Uma biblioteca em Dart é uma coleção de código que pode ser importada em outros códigos.

Ou seja... os modificadores funcionam como regras de acesso que determinam o que outros desenvolvedores podem fazer com suas classes quando eles importam sua biblioteca. Isso ajuda a proteger seu código e garantir que ele seja usado da maneira que você pretende.

Os modificadores aparecem antes da declaração de uma classe ou mixin. O conjunto completo de modificadores inclui: abstract, base, final, interface, sealed e mixin.

Classes Normais (Sem Modificador)

  • Uma classe sem modificador permite permissões irrestritas

  • Pode ser instanciada/construída de qualquer biblioteca

  • Pode ser estendida/herdada de qualquer biblioteca

  • Pode ser implementada de qualquer biblioteca

  • Pode ser usada com mixin ou como uma classe mixin

Ou seja... uma classe normal sem modificadores não tem restrições - qualquer pessoa que importe sua biblioteca pode fazer qualquer coisa com ela: criar instâncias, estender para criar novas classes, ou implementar sua interface.

Exemplo:

// arquivo: vehicle.dart
class Vehicle {
  void move() {
    print("O veículo está se movendo");
  }
}

// arquivo: main.dart (fora da biblioteca)
import 'vehicle.dart';

// Instanciação - PERMITIDO
var meuVeiculo = Vehicle();

// Herança - PERMITIDO
class Car extends Vehicle {
  @override
  void move() {
    print("O carro está dirigindo");
  }
}

// Implementação - PERMITIDO
class Drone implements Vehicle {
  @override
  void move() {
    print("O drone está voando");
  }
}

Diferença entre extend e implement:

  • Extend (estender): Usado para herança, onde uma subclasse herda propriedades e métodos da superclasse. Subclasses podem sobrescrever métodos para fornecer implementações personalizadas.

  • Implement (implementar): Usado para adotar uma interface, onde uma classe concorda em implementar métodos específicos. A classe deve fornecer todos os métodos especificados pela interface.

Ou seja... com extends, você herda implementações existentes e pode escolher sobrescrevê-las. Com implements, você apenas concorda com um contrato e deve implementar tudo do zero.

Classes Abstract

  • Propósito: Definir comportamentos que podem ser herdados por outras classes

  • Características: Não requerem implementação concreta completa de sua interface

  • Métodos: Frequentemente têm métodos abstratos (sem corpo/implementação)

  • Restrições:

    • Não podem ser instanciadas/construídas de nenhuma biblioteca

    • Podem ser estendidas/herdadas de qualquer biblioteca

    • Podem ser implementadas de qualquer biblioteca

Ou seja... classes abstratas são como "plantas baixas" ou "contratos parcialmente preenchidos" - elas definem uma estrutura, podem ter alguns métodos já implementados, mas também podem ter métodos abstratos que as subclasses precisam implementar. Você não pode criar objetos diretamente de classes abstratas.

Exemplo:

// arquivo: vehicle.dart
abstract class Vehicle {
  // Método abstrato (sem corpo)
  void move();
  
  // Método concreto (com implementação)
  void honk() {
    print("Beep beep!");
  }
}

// arquivo: main.dart (fora da biblioteca)
import 'vehicle.dart';

// Instanciação - NÃO PERMITIDO
// var meuVeiculo = Vehicle(); // Isso dará erro!

// Herança - PERMITIDO
class Car extends Vehicle {
  @override
  void move() {
    print("O carro está dirigindo");
  }
  // honk() é herdado com implementação
}

// Implementação - PERMITIDO
class Drone implements Vehicle {
  @override
  void move() {
    print("O drone está voando");
  }
  
  @override
  void honk() {
    print("Bzzzz!");
  }
  // Deve implementar TODOS os métodos
}

Classes Base

  • Propósito: Forçar a herança da implementação de uma classe ou mixin

  • Restrições:

    • Podem ser instanciadas/construídas de qualquer biblioteca

    • Podem ser estendidas/herdadas de qualquer biblioteca

    • Não podem ser implementadas fora de sua própria biblioteca

  • Regra importante: Qualquer classe que implementa ou estende uma classe base deve ser marcada como base, final ou sealed

Ou seja... classes base garantem que sua implementação seja respeitada quando alguém estender sua classe. Outras pessoas podem criar instâncias e estender suas classes base, mas não podem apenas implementar sua interface ignorando sua implementação. Isso protege a integridade do seu design.

Exemplo:

// arquivo: vehicle.dart
base class Vehicle {
  void move() {
    print("O veículo está se movendo");
  }
}

// arquivo: main.dart (fora da biblioteca)
import 'vehicle.dart';

// Instanciação - PERMITIDO
var meuVeiculo = Vehicle();

// Herança - PERMITIDO (mas deve ser base, final ou sealed)
base class Car extends Vehicle {
  @override
  void move() {
    print("O carro está dirigindo");
  }
}

// Implementação - NÃO PERMITIDO
// class Drone implements Vehicle { // Isso dará erro!
//   @override
//   void move() {
//     print("O drone está voando");
//   }
// }

Classes Interface

  • Propósito: Fornecer uma interface a ser implementada

  • Restrições:

    • Podem ser instanciadas/construídas de qualquer biblioteca

    • Não podem ser estendidas/herdadas fora de sua própria biblioteca

    • Podem ser implementadas de qualquer biblioteca

  • Interface pura: Combinando modificadores interface e abstract, cria-se uma interface pura

Ou seja... classes interface permitem que outros implementem seu "contrato" (métodos), mas não podem estender diretamente seu código. Isso é útil quando você quer que outros sigam sua API, mas não quer que herdem sua implementação específica.

Exemplo:

// arquivo: vehicle.dart
interface class Vehicle {
  void move() {
    print("O veículo está se movendo");
  }
}

// Para uma interface pura:
abstract interface class PureInterface {
  void doSomething();
}

// arquivo: main.dart (fora da biblioteca)
import 'vehicle.dart';

// Instanciação - PERMITIDO
var meuVeiculo = Vehicle();

// Herança - NÃO PERMITIDO
// class Car extends Vehicle { // Isso dará erro!
//   @override
//   void move() {
//     print("O carro está dirigindo");
//   }
// }

// Implementação - PERMITIDO
class Drone implements Vehicle {
  @override
  void move() {
    print("O drone está voando");
  }
}

// Com a interface pura:
class Task implements PureInterface {
  @override
  void doSomething() {
    print("Fazendo algo importante");
  }
}

Classes Final

  • Propósito: Evitar tanto herança quanto implementação fora da biblioteca

  • Restrições:

    • Podem ser instanciadas/construídas de qualquer biblioteca

    • Não podem ser estendidas/herdadas fora de sua própria biblioteca

    • Não podem ser implementadas fora de sua própria biblioteca

Ou seja... classes finais são completamente "fechadas" - outros desenvolvedores só podem usar instâncias delas exatamente como você as criou. Elas não podem ser modificadas, estendidas ou reimplementadas. Isso é útil para classes que precisam manter total controle sobre seu comportamento.

Exemplo:

// arquivo: vehicle.dart
final class Vehicle {
  void move() {
    print("O veículo está se movendo");
  }
}

// arquivo: main.dart (fora da biblioteca)
import 'vehicle.dart';

// Instanciação - PERMITIDO
var meuVeiculo = Vehicle();

// Herança - NÃO PERMITIDO
// class Car extends Vehicle { // Isso dará erro!
//   @override
//   void move() {
//     print("O carro está dirigindo");
//   }
// }

// Implementação - NÃO PERMITIDO
// class Drone implements Vehicle { // Isso dará erro!
//   @override
//   void move() {
//     print("O drone está voando");
//   }
// }

Classes Sealed

  • Propósito: Representar um conjunto finito de possíveis estados ou tipos

  • Utilidade: Verificação de exaustividade em declarações switch

  • Restrições:

    • Não podem ser instanciadas/construídas de nenhuma biblioteca

    • Não podem ser estendidas/herdadas fora de sua própria biblioteca

    • Não podem ser implementadas fora de sua própria biblioteca

    • Suas subclasses podem ser instanciadas de qualquer biblioteca

Ou seja... classes sealed são perfeitas para criar hierarquias fechadas como estados de uma máquina de estados ou tipos de mensagens em um sistema. O compilador pode verificar se você tratou todos os casos possíveis em um switch, o que evita bugs quando novos tipos são adicionados.

Exemplo:

// arquivo: shape.dart
sealed class Shape {}

// Subclasses definidas na mesma biblioteca
class Circle extends Shape {
  final double radius;
  Circle(this.radius);
}

class Rectangle extends Shape {
  final double width;
  final double height;
  Rectangle(this.width, this.height);
}

class Triangle extends Shape {
  final double base;
  final double height;
  Triangle(this.base, this.height);
}

// Função que usa verificação de exaustividade
String getShapeDescription(Shape shape) {
  // O compilador garantirá que todos os tipos de Shape sejam tratados
  return switch (shape) {
    Circle c => 'Um círculo com raio ${c.radius}',
    Rectangle r => 'Um retângulo ${r.width} x ${r.height}',
    Triangle t => 'Um triângulo com base ${t.base} e altura ${t.height}'
    // Se adicionarmos uma nova classe Pentagon que estende Shape,
    // o compilador nos obrigará a adicionar um caso aqui
  };
}

// arquivo: main.dart (fora da biblioteca)
import 'shape.dart';

void main() {
  // Instanciação direta de Shape - NÃO PERMITIDO
  // var forma = Shape(); // Isso dará erro!
  
  // Instanciação das subclasses - PERMITIDO
  var circulo = Circle(5.0);
  var retangulo = Rectangle(4.0, 3.0);
  
  // Herança de Shape - NÃO PERMITIDO
  // class Pentagon extends Shape { // Isso dará erro!
  //   final int sides = 5;
  // }
  
  print(getShapeDescription(circulo)); // Um círculo com raio 5.0
}

Mixins

  • Propósito: Reutilizar código de classe em várias hierarquias de classe sem usar herança tradicional

  • Uso: Usado com a palavra-chave with seguida por um ou mais nomes de mixin

  • Vantagem: Permite herdar comportamentos sem criar uma hierarquia profunda de herança

Ou seja... mixins são como "pacotes de funcionalidade" que você pode adicionar a qualquer classe. Diferente da herança, que limita você a uma única superclasse, você pode aplicar vários mixins a uma classe, o que torna seu código mais modular e reutilizável.

Exemplo:

// arquivo: abilities.dart
mixin Swimming {
  void swim() {
    print("Nadando na água");
  }
}

mixin Flying {
  void fly() {
    print("Voando no ar");
  }
}

// Uma classe Mixin (pode ser usada como classe regular ou como mixin)
mixin class Running {
  void run() {
    print("Correndo no chão");
  }
}

// arquivo: main.dart
import 'abilities.dart';

// Classe base
class Animal {
  String name;
  Animal(this.name);
}

// Usando um único mixin
class Fish extends Animal with Swimming {
  Fish(String name) : super(name);
}

// Usando múltiplos mixins
class Duck extends Animal with Swimming, Flying {
  Duck(String name) : super(name);
}

// Usando mixin class como mixin
class Human extends Animal with Running {
  Human(String name) : super(name);
}

// Usando mixin class como classe regular
var runner = Running();

void main() {
  var fish = Fish("Nemo");
  fish.swim(); // Nadando na água
  
  var duck = Duck("Donald");
  duck.swim(); // Nadando na água
  duck.fly();  // Voando no ar
  
  var human = Human("João");
  human.run(); // Correndo no chão
  
  runner.run(); // Correndo no chão
}

Resumo das Permissões Fora da Biblioteca

Modificador
Instanciar
Herdar
Implementar

Sem modificador

abstract

base

interface

final

sealed

Ou seja... esta tabela é um guia rápido para entender o que cada modificador permite ou proíbe quando sua classe é usada por outros desenvolvedores que importam sua biblioteca.

Last updated