Pokemons, Dart e Orientação a Objetos
Tags: dart, oop
Published: 2023-05-28 20:22:09

A Orientação a Objetos é um paradigma de programação que se baseia em objetos que possuem atributos e métodos. Em outras palavras, é uma forma de organizar e estruturar o código para que ele possa ser reutilizado de maneira mais eficiente.

Em Dart, podemos criar classes que representam objetos, como, por exemplo, um Pokemon. Uma classe é uma estrutura que define os atributos e métodos de um objeto.

Vamos criar uma classe Pokemon para ilustrar esses conceitos:

```
class Pokemon {
late String nome;
late String tipo;
late int nivel;
void atacar() {
print('$nome atacou!');
}
void evoluir() {
nivel++;
print('$nome evoluiu para o nível $nivel!');
}
}
```
Para utilizar null-safety na classe Pokemon em Dart 3.0, podemos modificar as variáveis nome, tipo e nivel para serem não-nulas utilizando o operador late. Dessa forma, será garantido que essas variáveis terão sempre um valor válido e não poderão ser null. Além disso, os métodos atacar e evoluir podem ser declarados com o tipo de retorno void ou com o tipo de retorno inferido automaticamente.

Nesse exemplo, modificamos as variáveis nome, tipo e nivel para serem não-nulas utilizando o operador late. Isso indica que essas variáveis serão inicializadas posteriormente e não poderão ser null. Também mantivemos os métodos atacar e evoluir, que agora possuem o tipo de retorno void ou inferido automaticamente.essa classe, temos os atributos nome, tipo e nivel, que representam as informações básicas de um Pokemon. Também temos os métodos atacar e evoluir, que representam as ações que um Pokemon pode realizar.

Agora, podemos criar objetos dessa classe e manipulá-los:

```
void main() {
var pikachu = Pokemon();
pikachu.nome = 'Pikachu';
pikachu.tipo = 'Elétrico';
pikachu.nivel = 1;
pikachu.atacar();
pikachu.evoluir();
}
```
Nesse exemplo, criamos um objeto pikachu da classe Pokemon e definimos seus atributos. Em seguida, chamamos os métodos atacar e evoluir, que manipulam o objeto de acordo com as definições da classe.

### null-safety

Em Dart, podemos utilizar o operador ! para indicar que uma variável não pode ter um valor null. Isso é conhecido como “null-safety” e é uma das principais características do Dart a partir da versão 2.12.

No exemplo anterior, podemos atualizá-lo para usar o null-safety da seguinte forma:

```
void main() {
var nomePokemon = 'Pikachu';
String? tipoPokemon = null;
tipoPokemon = 'Elétrico';
print('Nome: $nomePokemon');
print('Tipo: $tipoPokemon');
}
```
Nesse exemplo, a variável nomePokemon é do tipo String e nunca é atribuída como null, então não é necessário utilizar o operador !. Já a variável tipoPokemon é inicializada como null e só recebe um valor depois, então precisamos indicar que ela pode ser null adicionando o ? após o tipo da variável. Em seguida, a variável tipoPokemon recebe o valor ‘Elétrico’.

Por fim, utilizamos a função print para imprimir os valores das variáveis nomePokemon e tipoPokemon.

O null-safety em Dart é uma funcionalidade muito importante para evitar erros comuns como null-pointer exceptions. Ele nos obriga a lidar explicitamente com a possibilidade de uma variável ser null, o que torna o código mais seguro e menos propenso a erros

### Get e Set
Em Dart, os métodos get e set são utilizados para controlar o acesso aos atributos de uma classe. Eles permitem que os atributos sejam acessados e modificados de maneira segura, evitando que valores inválidos sejam atribuídos a eles.

O método get é utilizado para acessar o valor de um atributo, enquanto o método set é utilizado para modificar o valor de um atributo. Ambos os métodos são definidos dentro da classe, e seus nomes devem ser prefixados com as palavras-chave get e set, respectivamente.

Proponho criarmos um exemplo de classe Pokemon com os métodos get e set para o atributo nivel:

Além disso, podemos criar subclasses de uma classe para representar diferentes tipos de Pokemons. Por exemplo:

```
class Pokemon {
late String nome;
late String tipo;
late int _nivel;
int get nivel {
return _nivel;
}
set nivel(int valor) {
if (valor >= 1 && valor <= 100) {
_nivel = valor;
}
}
}
```
Nesse exemplo, temos um atributo nivel definido como privado, ou seja, só pode ser acessado dentro da própria classe. Em seguida, definimos o método get para o atributo nivel, que retorna o valor do atributo, e o método set para o atributo nivel, que define o valor do atributo, mas somente se o valor passado estiver dentro do intervalo de 1 a 100.

Dessa forma, podemos criar objetos da classe Pokemon e acessar e modificar o atributo nivel utilizando os métodos get e set:

```
void main() {
var pikachu = Pokemon();
pikachu.nome = 'Pikachu';
pikachu.tipo = 'Elétrico';
pikachu.nivel = 1;
print('Nome: ${pikachu.nome}');
print('Tipo: ${pikachu.tipo}');
print('Nível: ${pikachu.nivel}');
pikachu.nivel = 101; // Valor inválido, não será atribuído ao atributo
print('Nível: ${pikachu.nivel}');
}
```
Nesse exemplo, criamos um objeto pikachu da classe Pokemon e definimos seus atributos nome, tipo e nivel. Em seguida, utilizamos a função print para imprimir o valor do atributo nivel através do método get. Também tentamos modificar o valor do atributo nivel para um valor inválido, que não será atribuído ao atributo devido à verificação realizada no método set.

As arrow functions também podem ser utilizadas para definir os métodos get e set de forma mais concisa. Veja um exemplo:

```
class Pokemon {
late String nome;
late String tipo;
late int _nivel;
int get nivel => _nivel;
set nivel(int valor) => _nivel = valor >= 1 && valor <= 100 ? valor : _nivel;
Pokemon(this.nome, this.tipo, this._nivel);
}
```

### Construtores
O construtor é um método especial que é chamado quando um objeto é criado a partir de uma classe. Ele é responsável por inicializar os atributos do objeto e pode receber argumentos que serão utilizados nessa inicialização.

Em Dart, um construtor é definido dentro da classe, com o nome da classe seguido por parênteses que podem conter uma lista de parâmetros. Vamos criar um construtor para a classe Pokemon:

```
class Pokemon {
late String nome;
late String tipo;
late int nivel;
Pokemon(this.nome, this.tipo, this.nivel);
}
```
Nesse exemplo, o construtor da classe Pokemon recebe três argumentos: nome, tipo e nivel. Dentro do construtor, utilizamos a palavra-chave this seguida do nome do atributo para inicializá-lo com o valor do parâmetro correspondente.

Podemos criar objetos da classe Pokemon utilizando esse construtor da seguinte forma:

```
void main() {
var pikachu = Pokemon('Pikachu', 'Elétrico', 1);
print('Nome: ${pikachu.nome}');
print('Tipo: ${pikachu.tipo}');
print('Nível: ${pikachu.nivel}');
}
```
Nesse exemplo, criamos um objeto charmander da classe Pokemon passando os valores dos atributos nome, tipo e nivel diretamente no construtor. Em seguida, utilizamos a função print para imprimir esses valores.

Além do construtor padrão, é possível criar construtores nomeados em Dart. Um construtor nomeado é um construtor que tem um nome diferente do nome da classe e pode ser utilizado para criar objetos de maneira diferente do construtor padrão. Vamos criar um construtor nomeado para a classe Pokemon:

```
class Pokemon {
late String nome;
late String tipo;
late int nivel;
Pokemon.inicial(this.nome, this.tipo) {
nivel = 1;
}
}
```
Nesse exemplo, criamos um construtor nomeado inicial que recebe os argumentos nome e tipo. Dentro desse construtor, inicializamos os atributos nome e tipo com os valores passados e definimos o atributo nivel como 1.

Podemos criar objetos da classe Pokemon utilizando esse construtor nomeado da seguinte forma:

```
void main() {
var pikachu = Pokemon.inicial('Pikachu', 'Elétrico');
print('Nome: ${pikachu.nome}');
print('Tipo: ${pikachu.tipo}');
print('Nível: ${pikachu.nivel}');
}
```
Nesse exemplo, criamos um objeto squirtle da classe Pokemon utilizando o construtor nomeado inicial. Dessa forma, o atributo nivel é inicializado automaticamente como 1.

### Métodos
Em Dart, é possível definir métodos com ou sem parâmetros. Os parâmetros são informações adicionais que podem ser passadas para o método, permitindo que ele receba dados para processamento.

Vamos criar um exemplo de método sem parâmetros e um exemplo de método com parâmetros:

#### Método sem parâmetros:

```
class Pokemon {
late String nome;
void apresentar() {
print('Olá, meu nome é $nome!');
}
}
void main() {
var pikachu = Pokemon();
pikachu.nome = 'Pikachu';
pikachu.apresentar();
}
```
Nesse exemplo, temos a classe Pokemon que possui o atributo nome e um método apresentar. O método apresentar não possui parâmetros, mas utiliza o valor do atributo nome para imprimir uma mensagem na tela.

Para criar um objeto Pokemon, podemos utilizar o construtor padrão da classe e depois setar o valor da variável nome:

```
void main() {
var pikachu = Pokemon();
pikachu.nome = 'Pikachu';
pikachu.apresentar();
}
```

#### Método com parâmetros:

```
class Pokemon {
late String nome;
void apresentar(String apelido) {
print('Olá, meu nome é $nome e meu apelido é $apelido!');
}
}
void main() {
var pikachu = Pokemon();
pikachu.nome = 'Pikachu';
pikachu.apresentar('Pika');
}
```
Nesse exemplo, temos a classe Pokemon que possui o atributo nome e um método apresentar. O método apresentar recebe um parâmetro apelido do tipo String, que é utilizado juntamente com o valor do atributo nome para imprimir uma mensagem na tela.

É importante lembrar que os parâmetros dos métodos devem ser declarados entre parênteses após o nome do método, separados por vírgulas, e que a ordem dos parâmetros é importante. Além disso, os tipos dos parâmetros podem ser especificados ou não, mas é uma boa prática sempre especificá-los para evitar erros de tipo.

Os métodos com parâmetros permitem que o método receba dados externos para processamento, tornando o código mais flexível e reutilizável. Já os métodos sem parâmetros são úteis quando o método precisa apenas dos atributos da própria classe para realizar seu processamento.

Em Dart, os métodos podem retornar um valor utilizando a palavra-chave return. Esses valores podem ser de qualquer tipo, inclusive outros objetos.

Vamos criar um exemplo de método com retorno que retorna o nível do Pokemon:

#### Métodos com retorno

```
class Pokemon {
late String nome;
late String tipo;
late int nivel;
Pokemon(this.nome, this.tipo, this.nivel);
void atacar() {
print('$nome está usando um ataque!');
}
int obterNivel() {
return nivel;
}
setNivel(int novoNivel) {
nivel = novoNivel;
}
}
void main() {
var pikachu = Pokemon('Pikachu', 'Elétrico', 1);
print('Nome: ${pikachu.nome}');
print('Tipo: ${pikachu.tipo}');
print('Nível: ${pikachu.obterNivel()}');
pikachu.atacar();
pikachu.setNivel(2);
print('Novo nível: ${pikachu.obterNivel()}');
}
```
Nesse exemplo, temos a classe Pokemon que possui os atributos nome, tipo e nivel, um método atacar e um método obterNivel. O método obterNivel retorna o valor do atributo nivel como um inteiro. O método atacar e o construtor da classe não retornam nenhum valor.

Na função main, criamos um objeto pikachu da classe Pokemon, passando os valores dos atributos correspondentes. Em seguida, utilizamos a função print para imprimir os valores dos atributos do objeto e chamamos o método obterNivel do objeto pikachu, que retorna o valor do atributo nivel. Por fim, chamamos o método atacar do objeto pikachu, que imprime uma mensagem indicando que o Pokemon está usando um ataque.

Os métodos com retorno permitem que o método retorne um valor para ser utilizado em outras partes do código, tornando o código mais flexível e reutilizável. É importante lembrar que, ao utilizar um método com retorno, é necessário declarar o tipo de retorno do método na sua assinatura.

#### Herança
A herança é um dos conceitos fundamentais da programação orientada a objetos e é amplamente utilizada em Dart. Vou explicar como funciona a herança em Dart utilizando um exemplo em que a classe Pokemon é estendida para a classe Pikachu.

```
class Pokemon {
late String nome;
late String tipo;
late int nivel;
Pokemon({required this.nome, required this.tipo, required this.nivel});
void atacar() {
print('$nome está usando um ataque!');
}
}
class Pikachu extends Pokemon {
Pikachu({required String nome, required String tipo, required int nivel})
: super(nome: nome, tipo: tipo, nivel: nivel);
@override
void atacar() {
print('$nome está usando Choque do Trovão!');
}
}
```
Nesse exemplo, temos a classe Pokemon, que possui os atributos nome, tipo e nivel e um método atacar. Em seguida, temos a classe Pikachu, que estende a classe Pokemon utilizando a palavra-chave extends e herda seus atributos e métodos. A classe Pikachu também define um método atacar próprio, utilizando a anotação @override para sobrescrever o método da superclasse.

Podemos criar objetos da classe Pikachu da seguinte forma:

```
void main() {
var pikachu = Pikachu(nome: 'Pikachu', tipo: 'Elétrico', nivel: 1);
print('Nome: ${pikachu.nome}');
print('Tipo: ${pikachu.tipo}');
print('Nível: ${pikachu.nivel}');
pikachu.atacar();
}
```
Nesse exemplo, criamos um objeto pikachu da classe Pikachu, passando os valores dos atributos correspondentes. Em seguida, utilizamos a função print para imprimir os valores dos atributos do objeto. Por fim, chamamos o método atacar do objeto pikachu, que imprime uma mensagem indicando que o Pokemon está usando um ataque específico para sua espécie.

A herança é um recurso poderoso da programação orientada a objetos que permite a criação de classes mais especializadas a partir de classes já existentes, aproveitando seus atributos e métodos. A classe pai ou superclasse fornece uma estrutura básica que pode ser estendida ou modificada pela classe filha ou subclasse. Isso torna o código mais modular, mais fácil de manter e mais flexível para futuras mudanças.

### Classes Abstratas

Em Dart, as classes abstratas são classes que não podem ser instanciadas diretamente, mas servem como modelo para outras classes. Elas podem conter atributos e métodos concretos (que possuem implementação) e métodos abstratos (que não possuem implementação).

Um método abstrato é um método que não possui corpo, ou seja, não possui uma implementação definida. Ele é declarado utilizando a palavra-chave abstract antes do nome do método. As classes abstratas devem ser estendidas por outras classes que implementem seus métodos abstratos.

Vamos criar um exemplo de uma classe abstrata de Pokemons, que define a estrutura básica de um Pokemon, mas não pode ser instanciada diretamente:

```
abstract class Pokemon {
late String nome;
late String tipo;
late int nivel;
void atacar();
}
class Pikachu extends Pokemon {
Pikachu({required String nome, required String tipo, required int nivel}) : super() {
this.nome = nome;
this.tipo = tipo;
this.nivel = nivel;
}
@override
void atacar() {
print('$nome está usando Choque do Trovão!');
}
}
```
Nesse exemplo, definimos uma classe abstrata Pokemon que possui os atributos nome, tipo e nivel e um método abstrato atacar. Em seguida, criamos a classe Pikachu, que herda da classe Pokemon e implementa o método atacar, definindo a sua própria implementação para o ataque.

Observe que, na classe Pikachu, utilizamos a anotação @override antes da definição do método atacar. Isso indica que estamos sobrescrevendo (ou redefinindo) a implementação do método da classe pai. É importante lembrar que, ao herdar uma classe abstrata, é necessário implementar todos os métodos abstratos da classe pai.

Podemos criar objetos da classe Pikachu da seguinte forma:

```
void main() {
var pikachu = Pikachu(nome: 'Pikachu', tipo: 'Elétrico', nivel: 1);
print('Nome: ${pikachu.nome}');
print('Tipo: ${pikachu.tipo}');
print('Nível: ${pikachu.nivel}');
pikachu.atacar();
}
```
Nesse exemplo, criamos um objeto pikachu da classe Pikachu, passando os valores dos atributos correspondentes. Em seguida, utilizamos a função print para imprimir os valores dos atributos do objeto. Por fim, chamamos o método atacar do objeto pikachu, que imprime uma mensagem indicando que o Pokemon está usando um determinado ataque.

As classes abstratas são úteis quando queremos definir uma estrutura básica para outras classes, mas não faz sentido instanciar diretamente a classe abstrata. Elas permitem uma maior abstração e flexibilidade no design de classes.

### Interface
Em Dart, uma interface é um contrato que define um conjunto de métodos que uma classe deve implementar. Vou mostrar como podemos criar uma interface em que a evolução do Pokemon é um dos métodos a serem implementados.

```
abstract class Pokemon {
late String nome;
late String tipo;
late int nivel;
void atacar();
}
abstract class Evolucao {
void evoluir();
}
class Pikachu extends Pokemon implements Evolucao {
String evolucao = 'Raichu';
Pikachu({required String nome, required String tipo, required int nivel}) : super() {
this.nome = nome;
this.tipo = tipo;
this.nivel = nivel;
}
@override
void atacar() {
print('$nome está usando Choque do Trovão!');
}
@override
void evoluir() {
print('$nome está evoluindo para $evolucao!');
}
}
```
Nesse exemplo, temos a classe abstrata Pokemon, que possui os atributos nome, tipo e nivel e um método atacar. Em seguida, temos a interface Evolucao, que define um método evoluir. Por fim, temos a classe Pikachu, que herda de Pokemon e implementa a interface Evolucao. A classe Pikachu também define o atributo evolucao, que é uma string que indica a próxima forma do Pokemon após evoluir.

Podemos criar objetos da classe Pikachu da seguinte forma:

```
void main() {
var pikachu = Pikachu(nome: 'Pikachu', tipo: 'Elétrico', nivel: 1);
print('Nome: ${pikachu.nome}');
print('Tipo: ${pikachu.tipo}');
print('Nível: ${pikachu.nivel}');
pikachu.atacar();
pikachu.evoluir();
}
```
Nesse exemplo, criamos um objeto pikachu da classe Pikachu, passando os valores dos atributos correspondentes. Em seguida, utilizamos a função print para imprimir os valores dos atributos do objeto. Por fim, chamamos os métodos atacar e evoluir do objeto pikachu, que imprimem mensagens indicando as ações do Pokemon.

A interface Evolucao permite que outras classes implementem o método evoluir, garantindo um comportamento padrão para esse tipo de ação. Isso torna o código mais modular e mais fácil de manter. É importante lembrar que as interfaces não possuem implementação própria, mas apenas definem um conjunto de métodos que devem ser implementados pelas classes que a utilizam.

### Arrays
Em Dart, podemos criar uma lista de objetos da classe Pokemon para armazenar vários Pokemons em um único lugar. Vou mostrar como podemos adicionar objetos Pokemon em uma lista.

```
class Pokemon {
late String nome;
late String tipo;
late int nivel;
Pokemon(this.nome, this.tipo, this.nivel);
void atacar() {
print('$nome está usando um ataque!');
}
}
void main() {
var listaPokemons = <Pokemon>[];
var pikachu = Pokemon('Pikachu', 'Elétrico', 1);
var charmander = Pokemon('Charmander', 'Fogo', 1);
var squirtle = Pokemon('Squirtle', 'Água', 1);
listaPokemons.add(pikachu);
listaPokemons.add(charmander);
listaPokemons.add(squirtle);
for (var pokemon in listaPokemons) {
print('Nome: ${pokemon.nome}');
print('Tipo: ${pokemon.tipo}');
print('Nível: ${pokemon.nivel}');
pokemon.atacar();
print(' - - - - ');
}
}
```
Nesse exemplo, temos a classe Pokemon que possui os atributos nome, tipo e nivel e um método atacar. Na função main, criamos uma lista vazia listaPokemons que irá armazenar objetos Pokemon. Em seguida, criamos três objetos Pokemon: pikachu, charmander e squirtle.

Para adicionar os objetos Pokemon na lista listaPokemons, utilizamos o método add da lista e passamos os objetos como parâmetro. Por fim, utilizamos um laço de repetição for para percorrer a lista e imprimir as informações de cada objeto Pokemon. Chamamos o método atacar de cada objeto Pokemon e, após cada impressão, adicionamos uma linha horizontal para separar a exibição dos dados de cada objeto.

Esse exemplo ilustra como podemos utilizar listas em Dart para armazenar objetos de uma classe específica, como no caso dos Pokemons. As listas nos permitem manipular vários objetos ao mesmo tempo e aplicar operações em todos eles de uma só vez, tornando o código mais eficiente e reutilizável.