Dart: Classes e Objetos

Carlos Costa

O Dart possui diversos recursos para escrever código orientado a objetos, classes, objetos, herança, mixins, etc….

No Dart tudo é objeto:

class Dog {}

void main(List<String> args) {

  String foo = 'hello world';
  int bar = 10;
  bool span = true;
  var rex = Dog();

  print(foo.runtimeType); // String
  print(bar.runtimeType); // int
  print(span.runtimeType); // bool
  print(rex.runtimeType); // Dog
}

Até funções são objetos.

fn(){};
print(fn is Function); //true

Construtores


Contructor é um método especial que é executado quando um objeto é criado. Através do construtor é possível definir valores iniciais para os atributos de um objeto. No Dart é possível criar vários tipos de construtores.

Construtor default


O construtor default é criado automaticamente quando uma classe é criada. Esse construtor não recebe nenhum parâmetro e não faz nada além de criar o objeto.

class Point{
  var x;
  var y;

  Point(num x, num y){
    this.x = x;
    this.y = y;
  }
}

void main(List<String> args) {
  var point = Point(10, 20);

  print(point.x); // 10
  print(point.y); // 20
  print(point.runtimeType); // Point
}

Esse construtor pode ser escrito de forma reduzida usando a keyword this.

class Point {
  var x, y;

  Point(this.x, this.y);
}

Construtor nomeado


O construtor nomeado permite criar vários construtores para a mesma classe.

import 'dart:developer';

class Point{
  var x;
  var y;

  Point(num this.x, num this.y);
  Point.string(String this.x, String this.y);
  Point.origin(){this.x = 0; this.y = 0;}
  Point.middle(){this.x = 10; this.y = 10;}
}

void main(List<String> args) {
  var p1 = Point(10, 20);
  var p2 = Point.string('23', '56');
  var origin = Point.origin();
  var middle = Point.middle();

  print(inspect(p1)); //Instance of 'Point'
  print(inspect(p2)); //Instance of 'Point'
  print(inspect(origin)); //Instance of 'Point'
  print(inspect(middle)); //Instance of 'Point'
}

Construtor e herança


Quanto temos uma situação de herança entre classes, o construtor da classe pai é executado antes do construtor da classe filho. Usamos a keyword super para acessar o construtor da classe pai.

class Person {
  String name;
  int age;

  Person(this.name, this.age);
}

class Programmer extends Person {
  var language;

  Programmer(String name, int age, String language): super(name, age){
    this.language = language;
  }
}

void main(List<String> args) {
  var JohnDoe = Programmer('John Doe', 20, 'dart');

  print(JohnDoe.runtimeType);
  print(JohnDoe is Person); // true
  print(JohnDoe is Programmer); // true
}

Construtor e validações


No dart também é permitido usar assert para fazer a validação de valores que serão passados ao construtor.

Nesse exemplo existe um assert para fazer a verificação de entrada do atributo age, caso o valor for menos que 25 ocorrerá um erro.

class Hero {
  var name, age;

  Hero({this.name, this.age}) : assert(age > 25);
}

void main(List<String> args) {
  var peter = Hero(name: 'Peter Parker', age: 23);
}

👇 Executando.

💀 app.dart': Failed assertion: line 4 pos 40: 'age > 25': is not true.

Para que o assert funcione corretamente emitindo os erros é preciso habilitar a flag --enable-asserts, exemplo:

dart run --enable-asserts app.dart

Redirecionamento de construtor


Também é possível redirecionar a responsabilidade da criação de um objeto de um construtor para o outro.

class Point{
  var x;
  var y;

  // Construtor principal
  Point(int this.x, int this.y);

  // Construtor secundário redirecionando ao construtor principal
  Point.origin() : this(0, 0);
}

void main(List<String> args) {
  var origin = Point.origin();
  print(origin);
}

Construtores constantes


Construtores constantes são construtores que criam objetos imutáveis. Declarando os atributos como final e o construtor como const o objeto não poderá ser alterado.

class Point{
  final int x;
  final int y;

  const Point(this.x, this.y);
}

Métodos


Métodos são funções que determinam o comportamento do objeto.

class Calc {
  int value = 0;

  Calc(this.value);

  add(int value) {
    this.value += value;
    return this;
  }

  sub(int value) {
    this.value -= value;
    return this;
  }

  mul(int value) {
    this.value *= value;
    return this;
  }

  div(int value) {
    this.value ~/= value;
    return this;
  }
}

void main(List<String> args) {
  Calc calc = Calc(0);

  calc.add(10).sub(2).mul(3).div(2);

  print(calc.value); //12
}

get e set são métodos especiais que permitem a leitura e escrita de atributos. Também é possível criar métodos get e set para atributos que não foram declarados. Exemplo:

class Person {
  String name;
  int age;

  Person(this.name, this.age);

  //custom getter
  String get description {
    return 'My name is $name and I am $age years old';
  }

  //custom setter
  set description(String description) {
    var split = description.split(' ');
    name = split[0];
    age = int.parse(split[1]);
  }
}

void main(List<String> args) {
  var person = Person('John', 30);
  print(person.description); //My name is John and I am 30 years old
}

Metodos de extensão


Métodos de extensão permitem adicionar novos métodos a uma classe sem precisar alterar a classe original.

// extensão para a classe int
extension IntExtension on int {
  int add(int value) => this + value;
}

// extensão para a classe String
extension StringExtension on String {
  String add(String value) => this + value;
}

class Person {
  String name;
  Person(this.name);
}

// extensão para a classe Person
extension PersonExtension on Person {
  String get nameWithPrefix => 'Mr. $name';
}

void main(List<String> args) {
  print(1.add(2)); //3

  print('Hello'.add(' World')); //Hello World

  var person = Person('John');
  print(person.nameWithPrefix); //Mr. John
}

Sobrecarga de operadores


No dart é possível definir operadores especiais para um objeto. Por exemplo: os operadores (+, -, >>) podem ser configurados para realizar operações customizadas.

Lista de operadores que podem ser sobrecarregados.

👇👇👇👇
>+|>>>
</^[]
>=~/&[]=
<=*<<~
-%>>==

Vejamos o seguinte exemplo:

class Point{
  final int x;
  final int y;

  Point(this.x, this.y);
}

Neste exemplo temos uma classe que define um objeto Point(x, y). Para somar dois pontos precisamos fazer a seguinte operação:

p1 + p2 = (p1.x + p2.x, p1.y + p2.x)

Logo, não é possível fazer essa operação com o operado de soma padrão +, precisamos criar o nosso próprio operador + para somar os pontos.

class Point{
  final int x;
  final int y;

  Point(this.x, this.y);

  // sobrecarregando o operador +
  Point operator +(Point p) => Point(p.x + x, p.y + y);
}

void main(List<String> args) {
  final p1 = Point(10, 20);
  final p2 = Point(30, 40);

  // somando os pontos de forma direta
  Point p3 = p1 + p2;

  print("${p3.x}, ${p3.y}"); //40, 60
}

Mais um exemplo usando string. Vamos criar os seguinte operadores:

  • (-) Subtrai n caracteres do final de uma string.
  • (/) Divide a string em n partes e retorna a primeira parte.
class Text{
  String str;

  Text(this.str);

  // substrai (n) caracteres da string
  Text operator -(int num) => Text(this.str.substring(0, this.str.length - 10));

  // divide a string em (n) partes
  Text operator /(int num) => Text(this.str.substring(0, (this.str.length / num).round()));
}

void main(List<String> args) {
  var text = Text("dart is the best programming language");

  var result1 = text - 10;
  var result2 = text / 3;

  print(result1.str); //dart is the best programmin
  print(result2.str); //dart is the
}

Classes abstratas


Classes abstratas são classes que servem para definir o comportamento de outras classes através da herança. Esse tipo de classe não pode instanciada e seus métodos devem apenas ser assinados não implementados.

abstract class Hero {
  String secretIdentity(String name);
}

class Person extends Hero {
  String name;

  Person(this.name);

  @override
  String secretIdentity(String identity) {
    return "${this.name} is the ${identity}";
  }
}

void main(List<String> args) {
  var peter = Person('Petter Parker');

  print(peter.secretIdentity('Spiderman'));
}

Interfaces Implicitas


Todas as classes no dart podem ser implementadas como interfaces, ou seja, toda classe no dart é uma interface implicita.

class PostgresAdapter {
  String connectionString;

  PostgresAdapter(this.connectionString);

  void connect() {
    print('Connecting to $connectionString');
  }
}

class MySqlAdapter implements PostgresAdapter {
  String connectionString;

  MySqlAdapter(this.connectionString);

  void connect() {
    print('Connecting to $connectionString');
  }
}

class Database {
  PostgresAdapter adapter;

  Database(this.adapter);

  void connect() {
    adapter.connect();
  }
}

void main(List<String> args) {
  var db1 = Database(PostgresAdapter('postgres://localhost:5432'));
  var db2 = Database(MySqlAdapter('mysql://localhost:3306'));

  db1.connect();
  db2.connect();
}

Nesse exemplo a classe MysqlAdapter implementa diretamente a classe PostgresAdapter. dessa forma é possível passar uma instância de MysqlAdapter para a class Database que, em sua definição espera uma instância de PostgresAdapter.

Herança


Herança é um recurso que permite que uma classe herde atributos e métodos de outra classe.

class Persona {
  String name;
  int age;

  Persona(this.name, this.age);
}

class Hero extends Persona {
  final String superpower;

  Hero(String name, int age, this.superpower): super(name, age);
}

void main(List<String> args) {
  var peter = Hero('Peter parker', 25, 'stick web');

  print(peter.name);
  print(peter.age);
  print(peter.superpower);
}

Um ponto a ser observado no exemplo anterior, é que o construtor(super) da classe pai pode ser integrado ao construtor(Hero) da classe filho para ser escrito de forma reduzida.

Mixins


Mixin é um recurso que permite que classes sejam extendidas sem a necessidade de herança.

mixin Info {
  void showCarInfo(Car car) {
    print('Car name: ${car.name}, Car color: ${car.color}');
  }

  void showPersonInfo(Person person) {
    print('Person name: ${person.name}, Person age: ${person.age}');
  }
}

class Person with Info {
  String name;
  int age;

  Person(this.name, this.age);
}

class Car with Info {
  String name;
  String color;

  Car(this.name, this.color);
}

Nesse exemplo duas classes distintas(Person e Car) estão sendo extendidas pelo mesmo mixin(Info).

Atributos e métodos estáticos


Atributos/métodos estáticos podem ser acessados diretamente da classe sem a necessidade de uma instância.

class Console {
  static final String info = "Console class";

  static log(String arg){
    print(arg);
  }

  static error(String arg){
    print("Error: ${arg}");
  }
}

void main(List<String> args) {
  Console.log('hello world');
  Console.log(Console.info);
}

Referências