Pular para o conteúdo principal

Defina classes e objetos

Aprenda sobre programação orientada a objetos em Dart, incluindo classes, construtores, getters e enums. Crie modelos de dados para argumentos de linha de comando.

Este capítulo aborda os fundamentos da programação orientada a objetos (POO) em Dart. Explore como criar classes, definir construtores e campos, calcular propriedades com gettersGetterUm método especial que oferece acesso de leitura às propriedades de um objeto. Saiba mais e usar enums para representar conjuntos fixos de valores.

O que você vai realizar

Defina classes para modelar dados e comportamento
Crie construtores com initializing formals e parâmetros nomeados
Use getters para calcular propriedades dinamicamente
Use enums para representar conjuntos fixos de valores

Pré-requisitos

#

Antes de começar este capítulo:

  • Complete o Capítulo 4 e tenha um ambiente de desenvolvimento Dart funcionando com o projeto dartpedia.
  • Entenda conceitos básicos de programação, como variáveis, funções e controle de fluxo.
  • Entenda pacotes e bibliotecas em Dart.

Classes e objetos em Dart

#

Classes reúnem dois componentes principais:

  • Estado (campos): Os dados que cada objeto guarda, como o name ou o texto de help de uma opção.
  • Comportamento (métodos e getters): As ações que um objeto pode executar ou as propriedades que ele calcula, como formatar uma string de usage.

Em vez de controlar strings ou mapas soltos ao longo de uma aplicação, modelar argumentos de linha de comando como classes garante segurança de tipos em tempo de compilação, autocompletar no editor e uma estrutura de dados consistente.

Tarefas

#

Uma interface de linha de comando (CLI) é definida pelos comandos, opções e argumentos digitados em um terminal.

Esta lição e a próxima constroem um framework capaz de analisar um comando como:

bash
$ dartpedia help --verbose --command=search

Cada parte tem uma função distinta:

  • dartpedia: O executável, ou o nome da aplicação.
  • help: Um comando, representando uma ação a ser executada.
  • --verbose: Uma flag (uma opção booleana sem valor), que modifica o comportamento do comando.
  • --command=search: Uma opção que aceita um valor (search).

Este capítulo constrói os modelos de dados que representam as opções e os resultados analisados.

Tarefa 1: Defina o enum OptionType

#

Opções de linha de comando se dividem em duas categorias distintas: flags (que valem true ou false) e opções comuns (que aceitam um valor de texto). Usar um enum garante que o código aceite apenas essas duas categorias válidas.

  1. Crie command_runner/lib/src/arguments.dart.

  2. Defina o enum OptionType:

    command_runner/lib/src/arguments.dart
    dart
    enum OptionType { flag, option }
    

    Esta enumeração define dois membros: flag e option. Enums oferecem segurança de tipos em tempo de compilação, garantindo que o código aceite apenas tipos de opção válidos.

Tarefa 2: Defina a classe Option

#

Classes em Dart definem blueprints para objetos, combinando estado (campos) e comportamento (métodos e getters). A classe Option modela opções de linha de comando como --verbose ou --command=search.

  1. Comece definindo uma classe Option mínima com os campos essenciais e um construtor. Adicione o código a seguir em command_runner/lib/src/arguments.dart, abaixo do enum:

    command_runner/lib/src/arguments.dart
    dart
    class Option {
      Option(this.name, {required this.type});
    ​
      final String name;
      final OptionType type;
    }
    
    • Campos (final String name;, final OptionType type;): Variáveis que guardam os dados do objeto. Declará-los como final garante que não mudem após a criação.
    • Construtor (Option(...)): Instancia novos objetos Option. A sintaxe this.name é um initializing formal, um atalho do Dart que atribui o argumento diretamente ao campo da instância antes de o corpo do construtor ser executado.
    • Parâmetros nomeados ({required this.type}): Parâmetros entre chaves {} são passados pelo nome (por exemplo, Option('verbose', type: OptionType.flag)). A palavra-chave required torna o parâmetro obrigatório.
  2. Agora, expanda a classe Option com campos de metadados opcionais e um getter usage:

    command_runner/lib/src/arguments.dart
    dart
    class Option {
      Option(
        this.name, {
        required this.type,
        this.help,
        this.abbr,
        this.defaultValue,
        this.valueHelp,
      });
    ​
      final String name;
      final OptionType type;
      final String? help;
      final String? abbr;
      final Object? defaultValue;
      final String? valueHelp;
    ​
      String get usage {
        if (abbr != null) {
          return '-$abbr,--$name: $help';
        }
    ​
        return '--$name: $help';
      }
    }
    
    • Tipos anuláveis (String?, Object?): O ponto de interrogação ? indica que um campo é opcional e pode conter null. Quando quem chama omite help ou abbr, eles assumem null.
    • Getter (get usage): Um getter calcula um valor sob demanda, quando é acessado, formatando a string de ajuda conforme exista ou não uma abreviação.

Tarefa 3: Defina a classe ArgResults

#

A classe ArgResults armazena o resultado da análise da entrada de linha de comando. Ela associa cada Option ao valor informado pelo usuário.

  1. Adicione ArgResults ao final de command_runner/lib/src/arguments.dart:

    command_runner/lib/src/arguments.dart
    dart
    class ArgResults {
      String? command;
      String? commandArg;
      Map<Option, Object?> options = {};
    ​
      // Returns true if the flag exists and is true.
      bool flag(String name) {
        for (var option in options.keys.where(
          (option) => option.type == OptionType.flag,
        )) {
          if (option.name == name) {
            return options[option] as bool;
          }
        }
        return false;
      }
    ​
      bool hasOption(String name) {
        return options.keys.any((option) => option.name == name);
      }
    ​
      ({Option option, Object? input}) getOption(String name) {
        var mapEntry = options.entries.firstWhere(
          (entry) => entry.key.name == name || entry.key.abbr == name,
        );
    ​
        return (option: mapEntry.key, input: mapEntry.value);
      }
    }
    

    Destaques do código anterior:

    • Mapa options (Map<Option, Object?>): Associa cada instância de Option à sua entrada analisada.
    • Método where() em flag(): Filtra as chaves do mapa para inspecionar apenas flags booleanas (option.type == OptionType.flag), ignorando opções que recebem argumentos de texto.
    • Cast de tipo (as bool): Informa ao verificador de tipos que o valor deve ser tratado como bool, porque flags sempre armazenam valores booleanos.
    • Tipo de retorno de record (({Option option, Object? input})): Retorna um record leve e nomeado, que agrupa a Option e o seu valor, sem precisar declarar uma classe separada.

    A classe ArgResults serve como contrato de dados para a saída analisada. O Capítulo 6 conecta essa classe ao CommandRunner completo.

Tarefa 4: Exporte os argumentos do pacote

#

Exporte arguments.dart a partir do ponto de entrada do pacote, para que outros pacotes possam importar Option e ArgResults.

  1. Abra command_runner/lib/command_runner.dart.

  2. Exporte arguments.dart junto com command_runner_base.dart:

    command_runner/lib/command_runner.dart
    dart
    /// Support for command-line parsing and execution.
    library;
    ​
    export 'src/arguments.dart';
    export 'src/command_runner_base.dart';
    

    Esta instrução export torna as declarações de arguments.dart acessíveis a qualquer pacote que importe package:command_runner/command_runner.dart.

Tarefa 5: Teste as classes em cli.dart

#

Verifique se Option é instanciada e calcula corretamente a string usage.

  1. Abra cli/bin/cli.dart.

  2. Substitua o conteúdo do arquivo pelo código de teste a seguir:

    cli/bin/cli.dart
    dart
    import 'package:command_runner/command_runner.dart';
    ​
    void main() {
      final verboseOption = Option(
        'verbose',
        type: OptionType.flag,
        abbr: 'v',
        help: 'Display extra logging information.',
      );
    ​
      print('Defined option: ${verboseOption.name}');
      print('Usage: ${verboseOption.usage}');
    }
    

    Este código chama o construtor de Option para criar uma instância e depois imprime o campo name e o getter usage calculado.

  3. Execute a aplicação a partir do diretório cli:

    bash
    dart run bin/cli.dart
    

    O console mostra:

    bash
    Defined option: verbose
    Usage: -v,--verbose: Display extra logging information.
    

    Isso confirma que a classe Option e o getter funcionam como esperado.

Revisão

#

O que você realizou

Um resumo dos conceitos e do código introduzidos nesta lição.
Definiu classes para modelar dados e comportamento

Criou as classes Option e ArgResults para estruturar os argumentos de linha de comando e a saída analisada.

Criou construtores com initializing formals e parâmetros nomeados

Usou a sintaxe this.fieldName para inicialização automática de campos e definiu parâmetros nomeados opcionais e obrigatórios com {} e required.

Usou getters para calcular propriedades dinamicamente

Implementou o getter usage para formatar o texto de ajuda da opção sob demanda, sem armazenar estado de string duplicado.

Usou enums para representar conjuntos fixos de valores

Definiu o enum OptionType para representar os tipos de opção válidos (flag e option), garantindo segurança em tempo de compilação.

Quiz

#

Check your understanding

1 / 3
No construtor Option(this.name, {required this.type}), qual é a finalidade da sintaxe this.name?
  1. Ela atribui automaticamente o argumento do construtor diretamente ao campo de instância name.

    Isso mesmo!

    No Dart, this.nomeDoCampo na lista de parâmetros de um construtor é um "parâmetro formal de inicialização" que atribui o valor do argumento diretamente ao campo antes de o corpo do construtor executar.

  2. Ela cria uma nova variável global chamada name.

    Não exatamente.

    this.name se refere especificamente ao campo de instância da classe, e não a uma variável global.

  3. Ela torna a propriedade name privada para a biblioteca.

    Não exatamente.

    A privacidade no Dart é indicada por um underscore no início (_name), e não por this.

  4. Ela marca o parâmetro como opcional.

    Não exatamente.

    Parâmetros opcionais são delimitados por colchetes [] ou chaves {}. O this.name posicional é obrigatório.

Qual é a principal vantagem de usar um getter como String get usage em vez de guardar um campo String usage?
  1. Ele calcula o valor dinamicamente sempre que é acessado, garantindo que reflita o estado atual sem armazenamento redundante.

    Isso mesmo!

    Getters se comportam como propriedades do ponto de vista de quem chama, mas executam código sob demanda para calcular o valor.

  2. Getters só podem ser acessados de dentro do mesmo arquivo.

    Não exatamente.

    Getters seguem as mesmas regras de visibilidade que campos e métodos comuns.

  3. Getters executam mais rápido do que a leitura de um campo armazenado.

    Não exatamente.

    Ler um campo armazenado costuma ser mais rápido do que executar lógica em um getter.

  4. Getters permitem que a propriedade seja modificada de fora da classe.

    Não exatamente.

    Um getter oferece acesso somente leitura, a menos que um setter correspondente (set usage(...)) também seja definido.

Quando você deve usar um enum em vez de uma classe ou de um conjunto de constantes?
  1. Quando você precisa de um tipo que pode ser apenas um de um conjunto fixo e conhecido de valores.

    Isso mesmo!

    Enums são ideais quando você tem um conjunto fechado de opções, como flag ou option, dias da semana ou códigos de status. O compilador garante que você trate todos os casos.

  2. Quando você precisa guardar uma entrada do usuário que pode ser qualquer string.

    Não exatamente.

    A entrada do usuário é imprevisível e pode ser qualquer coisa. Enums funcionam melhor quando você consegue listar todos os valores possíveis com antecedência.

  3. Quando você quer criar várias instâncias com propriedades diferentes.

    Não exatamente.

    Para isso servem as classes. Cada valor de enum é um singleton: existe apenas uma instância para cada valor.

  4. Quando você precisa fazer operações aritméticas com os valores.

    Não exatamente.

    Use int ou double para aritmética. Enums representam categorias distintas, e não valores numéricos (embora tenham um índice).

Próxima lição

#

O próximo capítulo aborda como compartilhar comportamento entre classes usando herança e classes abstratas. Defina uma classe base abstrata CliElement e construa a arquitetura Command e CommandRunner.

Encontrou essa página sem tradução ou que precisa de correção? Abra uma issue e nos ajude a manter esse site em PT-BR para ajudar pessoas como você.