Estruture apps com herança e classes abstratas
Aprenda sobre herança, classes abstratas, sobrescrita de métodos e encapsulamento em Dart. Construa um framework extensível para apps CLI.
Este capítulo se baseia nas classes criadas na lição anterior, explorando herança e classes abstratas em Dart. Aprenda como compartilhar comportamento entre classes usando herança, definir contratos usando classes abstratas e proteger o estado interno com encapsulamentoLibrary-privateAcessibilidade restrita à [biblioteca](#library) onde é definida. Saiba mais.
O que você vai realizar
Pré-requisitos
#Antes de começar este capítulo:
-
Conclua o Capítulo 5 e tenha um
ambiente de desenvolvimento Dart funcionando com o projeto
dartpedia. - Entenda conceitos básicos de orientação a objetos em Dart, como definir classes, construtores, campos e getters.
- Entenda pacotes e bibliotecas em Dart.
Classes abstratas e herança
#
No Dart, uma classe abstrata é uma classe que não pode ser instanciada
diretamente (chamar CliElement() causa um erro de compilação).
Em vez disso, ela serve como um molde ou contrato que outras classes estendem.
Em uma CLI, opções (--verbose) e comandos (help) compartilham
características comuns, como um nome e uma descrição de ajuda,
mas um "elemento de CLI" genérico não faz sentido por si só.
Declarar CliElement e Command como abstratas
garante que o código
só instancie classes específicas, como Option e HelpCommand.
┌──────────────┐
│ CliElement │ (abstract: blueprint for all CLI elements)
└──────┬───────┘
│
┌───────┴───────┐
▼ ▼
┌───────────┐ ┌───────────┐
│ Option │ │ Command │ (abstract: blueprint for commands)
└───────────┘ └─────┬─────┘
│
▼
┌─────────────┐
│ HelpCommand │ (can be instantiated and run)
└─────────────┘
Compartilhe comportamento com herança
#
Herança permite que uma classe adote
propriedades e comportamentos de uma classe pai.
Como tanto as opções (Option) quanto os comandos (Command) compartilham um
name,
um texto de help e uma mensagem formatada de usage,
herdar de um pai CliElement comum elimina código duplicado
em várias classes.
A herança também possibilita o polimorfismo: a capacidade de o executor de comandos
tratar qualquer comando ou opção de forma uniforme
pela interface compartilhada CliElement.
Tarefas
#
No Capítulo 5, as classes Option e ArgResults estabeleceram
estruturas de dados básicas para opções de CLI e a saída analisada.
Este capítulo estabelece uma hierarquia compartilhada entre comandos e opções,
e expande o CommandRunner provisório do Capítulo 4 para
um analisador de comandos completo.
As classes e a lógica das tarefas a seguir criam a base para analisar e executar os comandos da CLI.
Tarefa 1: Defina a classe abstrata CliElement e atualize Option
#
Tanto as opções quanto os comandos compartilham atributos centrais, como name,
o texto de help e uma string formatada de usage.
Definir uma classe base abstrata estabelece um único contrato para ambos.
Abra
command_runner/lib/src/arguments.dart.-
Defina uma
abstract classchamadaCliElementlogo abaixo do enumOptionType:command_runner/lib/src/arguments.dartdartabstract class CliElement { String get name; String? get help; // In the case of flags, the default value is a bool. // In other options and commands, the default value is a String. // NB: flags are just Option objects that don't take arguments Object? get defaultValue; String? get valueHelp; String get usage; }A palavra-chave
abstractmarcaCliElementcomo uma classe base que não pode ser instanciada diretamente (CliElement()). Getters sem corpo (String get name;) definem propriedades obrigatórias que toda subclasse precisa implementar. O getterdefaultValueusa o tipoObject?para poder retornar tanto umbool(para flags) quanto umaString(para opções que aceitam valores). -
Atualize a classe
Optionpara estenderCliElement:command_runner/lib/src/arguments.dartdartclass Option extends CliElement { Option( this.name, { required this.type, this.help, this.abbr, this.defaultValue, this.valueHelp, }); @override final String name; final OptionType type; @override final String? help; final String? abbr; @override final Object? defaultValue; @override final String? valueHelp; @override String get usage { if (abbr != null) { return '-$abbr,--$name: $help'; } return '--$name: $help'; } }A palavra-chave
extendsestabelece uma relação de herança em queOptionpassa a ser um subtipo deCliElement. No Dart, todo campo tem um getter implícito. Declarar@override final String name;satisfaz o getter abstratoString get name;declarado emCliElement. Os camposabbretypecontinuam específicos deOption.
Tarefa 2: Defina a classe abstrata Command
#
Comandos representam ações que os usuários podem executar, como help ou search.
Como os comandos compartilham propriedades com Option (como name
e usage),
eles também estendem CliElement.
-
Adicione os imports necessários no topo de
command_runner/lib/src/arguments.dart:command_runner/lib/src/arguments.dartdartimport 'dart:async'; import 'dart:collection'; import 'command_runner_base.dart'; -
Comece definindo a classe abstrata central
Commandcom suas propriedades e a referência ao executor:command_runner/lib/src/arguments.dartdartabstract class Command extends CliElement { @override String get name; String get description; bool get requiresArgument => false; late CommandRunner runner; @override String? help; @override String? defaultValue; @override String? valueHelp; }abstract class Command extends CliElement: EstabeleceCommandcomo um subtipo abstrato deCliElement, servindo de modelo para que todos os comandos específicos sigam.late CommandRunner runner;: Um comando precisa de uma referência aoCommandRunnerque o executa, para acessar o estado global do executor. A palavra-chavelatepromete ao Dart que essa variável não nula será atribuída antes de ser lida (ao registrar o comando comcommand.runner = this;).
-
Em seguida, adicione o armazenamento encapsulado de opções e os métodos auxiliares em
Command:command_runner/lib/src/arguments.dartdartabstract class Command extends CliElement { // ... existing properties ... final List<Option> _options = []; UnmodifiableSetView<Option> get options => UnmodifiableSetView(_options.toSet()); void addFlag( String name, { String? help, String? abbr, String? valueHelp, }) { _options.add( Option( name, help: help, abbr: abbr, defaultValue: false, valueHelp: valueHelp, type: OptionType.flag, ), ); } void addOption( String name, { String? help, String? abbr, String? defaultValue, String? valueHelp, }) { _options.add( Option( name, help: help, abbr: abbr, defaultValue: defaultValue, valueHelp: valueHelp, type: OptionType.option, ), ); } }- Encapsulamento com
_options: Prefixar_optionscom um sublinhado (_) o torna privado da biblioteca, impedindo que código fora dearguments.dartmodifique a lista diretamente. UnmodifiableSetView: Expõe uma visualização somente leitura das opções do comando, garantindo que quem chama não consiga alterar o estado interno diretamente.addFlageaddOption: Fornecem métodos controlados para criar e registrar instâncias válidas deOptionno comando.
- Encapsulamento com
-
Por fim, adicione o método abstrato
rune o getterusagepara completarCommand:command_runner/lib/src/arguments.dartdartabstract class Command extends CliElement { // ... existing properties and helper methods ... FutureOr<Object?> run(ArgResults args); @override String get usage { return '$name: $description'; } }FutureOr<Object?> run(...): Define o método abstrato onde fica a lógica de execução de um comando.FutureOrpermite que o método retorne tanto um valor síncrono direto quanto umFuture, para operações assíncronas (retomando o Capítulo 3).- Getter
usage: Formata o nome e a descrição do comando para a saída de ajuda da CLI.
-
Atualize a classe
ArgResults, no final decommand_runner/lib/src/arguments.dart, para referenciarCommand:command_runner/lib/src/arguments.dartdartclass ArgResults { Command? command; String? commandArg; Map<Option, Object?> options = {}; // ... existing flag, hasOption, and getOption methods ... }No Capítulo 5,
ArgResults.commandera umString?, poisCommandainda não existia. Agora queCommandexiste, mudar o tipo paraCommand?permite que o executor de comandos armazene e execute diretamente o objetoCommandresolvido.
Tarefa 3: Atualize a classe CommandRunner
#
O Capítulo 4 criou um CommandRunner provisório em
command_runner/lib/src/command_runner_base.dart
que apenas imprimia os argumentos.
Agora, substitua esse provisório pelo coordenador de comandos de verdade.
Abra
command_runner/lib/src/command_runner_base.dart.-
Substitua o conteúdo do arquivo pelo seguinte código:
command_runner/lib/src/command_runner_base.dartdartimport 'dart:collection'; import 'dart:io'; import 'arguments.dart'; class CommandRunner { final Map<String, Command> _commands = <String, Command>{}; UnmodifiableSetView<Command> get commands => UnmodifiableSetView<Command>(<Command>{..._commands.values}); Future<void> run(List<String> input) async { final ArgResults results = parse(input); if (results.command != null) { Object? output = await results.command!.run(results); print(output.toString()); } } void addCommand(Command command) { _commands[command.name] = command; command.runner = this; } ArgResults parse(List<String> input) { var results = ArgResults(); results.command = _commands[input.first]; return results; } String get usage { final exeFile = Platform.script.path.split('/').last; return 'Usage: dart bin/$exeFile <command> [commandArg?] [...options?]'; } }Destaques do código anterior:
- Operador spread (
..._commands.values): Desempacota os valores do mapa privado_commandsem um novo conjunto, impedindo que quem chama modifique o mapa original. command.runner = this;: Atribui a instância do executor ao comando no momento do registro, cumprindo a promessa feita pela palavra-chavelateemCommand.- Operador de asserção nula (
!): Emresults.command!.run(results), o!afirma quecommandnão é nulo, por causa da verificaçãoif (results.command != null)anterior.
- Operador spread (
-
Abra
command_runner/lib/command_runner.darte atualize as exportações:command_runner/lib/command_runner.dartdart/// Support for command-line parsing and execution. library; export 'src/arguments.dart'; export 'src/command_runner_base.dart'; export 'src/help_command.dart';Essas declarações
exporttornamarguments.dart,command_runner_base.dartehelp_command.dartparte da API pública do pacotecommand_runner.
Tarefa 4: Crie um HelpCommand
#Crie um HelpCommand que estende Command e imprime as informações de uso.
Crie
command_runner/lib/src/help_command.dart.-
Adicione o seguinte código:
command_runner/lib/src/help_command.dartdartimport 'dart:async'; import 'arguments.dart'; class HelpCommand extends Command { HelpCommand() { addFlag( 'verbose', abbr: 'v', help: 'When true, prints each command and its options.', ); addOption( 'command', abbr: 'c', help: 'Prints verbose usage for the specified command.', ); } @override String get name => 'help'; @override String get description => 'Prints usage information to the command line.'; @override String? get help => 'Prints this usage information'; @override FutureOr<Object?> run(ArgResults args) async { var usage = runner.usage; for (var command in runner.commands) { usage += '\n ${command.usage}'; } return usage; } }O construtor de
HelpCommandchama os métodos auxiliares herdadosaddFlageaddOptionpara configurar as opções suportadas. O métodorunlêrunner.usagee percorrerunner.commandspara montar e retornar dinamicamente a mensagem completa de uso da CLI.
Tarefa 5: Atualize o cli.dart para usar o CommandRunner
#Conecte o CommandRunner e o HelpCommand no ponto de entrada executável.
Abra
cli/bin/cli.dart.-
Substitua o conteúdo do arquivo pelo seguinte código:
cli/bin/cli.dartdartimport 'package:command_runner/command_runner.dart'; const version = '0.0.1'; void main(List<String> arguments) { var commandRunner = CommandRunner()..addCommand(HelpCommand()); commandRunner.run(arguments); }A notação em cascata
..addCommand(...)chamaaddCommandnoCommandRunnerrecém-criado e retorna essa mesma instância, permitindo encadear métodos de forma concisa antes de passarargumentspararun().
Tarefa 6: Execute o aplicativo
#Teste o CommandRunner e o HelpCommand.
-
No diretório
cli, execute:bashdart run bin/cli.dart helpO console mostra:
bashUsage: dart bin/cli.dart <command> [commandArg?] [...options?] help: Prints usage information to the command line.Isso confirma que o
CommandRunnerdireciona a execução para oHelpCommande imprime a saída de uso esperada.
Revisão
#O que você realizou
Um resumo dos conceitos e do código introduzidos nesta lição.Projetou e entendeu classes abstratas
Criou as classes abstratas CliElement e Command como contratos base que não podem ser instanciados diretamente.
Estendeu classes pai e sobrescreveu métodos
Usou extends para criar os subtipos Option, Command e HelpCommand, usando
@override para fornecer as implementações obrigatórias.
Protegeu o estado interno com encapsulamento
Armazenou as opções em listas privadas (_options, _commands) e as expôs por meio de
UnmodifiableSetView, para evitar modificações indesejadas.
Construiu um framework extensível de executor de comandos
Aplicou princípios de orientação a objetos para implementar um framework de CLI polimórfico, capaz de despachar comandos dinamicamente.
Quiz
#Confira seu entendimento
1 / 3Option, qual é a finalidade da anotação @override?
-
Fornecer uma implementação específica para um método ou propriedade definido em uma classe pai.
Isso mesmo!
@overrideindica que você está fornecendo uma implementação concreta para um membro abstrato ou substituindo uma implementação herdada. -
Criar um novo método que não existe na classe pai.
Não exatamente.
@overridenão cria novos métodos. Novos métodos são criados simplesmente ao defini-los na classe. -
Indicar que um método é opcional.
Não exatamente.
@overridenão afeta se um método é opcional. Parâmetros opcionais usam uma sintaxe totalmente diferente. -
Tornar uma propriedade privada.
Não exatamente.
A privacidade é indicada por um underscore no início (
_), e não por anotações.@overridetem outra finalidade, relacionada à herança.
abstract e uma classe comum em Dart?
-
Uma classe
abstractnão pode ser instanciada diretamente.Isso mesmo!
Classes abstratas servem como modelos que outras classes estendem. Você não pode criar instâncias de uma classe abstrata diretamente.
-
Uma classe
abstractnão pode ter nenhum método.Não exatamente.
Classes abstratas podem ter tanto métodos abstratos (sem implementação) quanto métodos concretos (com implementação).
-
Uma classe
abstractsó pode ter métodos privados.Não exatamente.
Classes abstratas podem ter métodos com qualquer nível de visibilidade. A palavra-chave
abstractnão restringe a visibilidade. -
Não há diferença entre uma classe
abstracte uma classe comum.Não exatamente.
Há uma diferença significativa. Tente escrever
var x = MinhaClasseAbstrata();e veja o que acontece.
Command expõe options por meio de um UnmodifiableSetView em vez de permitir o acesso direto a _options?
-
Para impedir que código externo modifique diretamente a lista interna de opções do comando.
Isso mesmo!
Encapsular a lista e expor uma visão não modificável impede que quem chama adicione, remova ou reordene opções sem usar os métodos auxiliares do comando.
-
Porque o Dart não permite que classes contenham listas mutáveis.
Não exatamente.
Classes Dart podem conter listas mutáveis. A visão não modificável é uma escolha de design intencional para encapsulamento.
-
Para tornar as opções acessíveis de fora da biblioteca.
Não exatamente.
Getters públicos já tornam os dados acessíveis; o wrapper não modificável protege especificamente a coleção contra mutações externas.
-
Para ordenar as opções automaticamente em ordem alfabética.
Não exatamente.
UnmodifiableSetViewnão ordena coleções; ele apenas impede mutações.
Próxima lição
#
O próximo capítulo aborda o tratamento de erros e exceções em Dart.
Você vai criar uma classe de exceção personalizada e
adicionar tratamento de erros ao CommandRunner para tornar a aplicação mais robusta.
Salvo indicação em contrário, a documentação neste site reflete Dart 3.13.7. Página atualizada pela última vez em 2026-11-12. Ver código-fonte ou relatar um problema.
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ê.